Forum Programmation.python Deuxième mouture script multi clock

Posté par . Licence CC by-sa.
4
1
fév.
2019

Bonjour,

Comme décris dans un précédent post https://linuxfr.org/forums/programmation-python/posts/mon-premier-code-python , j'ai fait mon premier script python ( et accessoireent, j'ai découvert le programmation objet également) , qui n'était pas beau du tout

J'y ai donc bossé, en fait, en repartant généralement from scratch (je connais l'algo , donc le réécrire n'était pas très compliqué)

Et , effectivement, j'ai bien vu ma lacune concernant l'utilisation de self dans les classes
Donc, j'ai essayé qu'il passe pylint, ce qui est le cas (je suisparti de -60 à 10 , chouette)
Ensuite, j'ai mis la classe clock dans un module
et merger les classe App et la classe qui gérait le menu :
un menu n'est il pas l'application sans les clocks ?

Voilà ce que ça donne (j'ai conscience qu'il n'est pas super, mais j'ai aussi conscience qu'il est moins moche qu'auparavant), j'aurai aussi une question à la fin

import sys
from tkinter import (
    ACTIVE,
    Menu,
    Toplevel,
    Listbox,
    Button,
    Tk,
    Scrollbar,
    Frame,
)
from tkinter.colorchooser import askcolor
import os
import pytz
from Modules import clock

class App:
    """
    Main App
    """
    def __init__(self):
        self.conf = dict()
        self.conffile = dict()
        self.conf["timezone"] = []
        self.conf["colors"] = []
        self.obj = dict()
        self.root = Tk()
        self.root.title("Clocks- 土圭")
        bin_path = os.path.dirname(os.path.realpath(__file__))
        self.conffile["tz"] = "{}/tz.conf".format(bin_path)
        self.conffile["colors"] = "{}/colors.conf".format(bin_path)
        self.readconftz()
        print("timezones initialization...OK")
        print(self.conf["timezone"])
        self.readconfcolor()
        print("Colors initialization...OK")
        print(self.conf["colors"])
        self.create_clock()
        self.create_menu()
        self.root.mainloop()
    def create_clock(self):
        """
        creation of Clock
        """
        self.obj["local"] = clock.Clock(
            window=self.root,
            tz="local",
            place=[0, 1],
            color=["#880088", "white", "black", "yellow"],
        )
        print("local clock created")
        self.conf["col"] = 1
        self.conf["row"] = 1
        for i in self.conf["timezone"]:
            self.add_clock(i)
            print("clock for {} created".format(i))
            if self.conf["col"] < 9:
                self.conf["col"] += 1
            else:
                self.conf["col"] = 0
                self.conf["row"] += 1
    def create_menu(self):
        menu_bar = Menu(self.root)
        ope = Menu(menu_bar, tearoff=0)
        ope.add_command(label="Add Clock", command=self.popup_add_clock)
        colors = Menu(menu_bar, tearoff=0)
        for i in self.conf["colors"]:
            colors.add_command(
                label=i, background=i, command=lambda x=i: self.setcolors(x)
            )
        self.remove = Menu(ope, tearoff=0)
        ope.add_cascade(label="Remove Clock", menu=self.remove)
        ope.add_command(
            label="Refresh", command=lambda: os.execv(__file__, sys.argv)
        )
        menu_bar.add_cascade(label="Add/Remove", menu=ope)
        menu_bar.add_cascade(label="Set Colors", menu=colors)
        self.root.config(menu=menu_bar)
        for i in self.conf["timezone"]:
            self.remove.add_command(
                label=i, command=lambda x=i: self.removetz(x)
            )
    def removetz(self, city):
        self.obj[city].remove()
        self.conf["timezone"].remove(city)
        with open(self.conffile["tz"], "w") as conffile:
            for i in self.conf["timezone"]:
                conffile.write("{}\n".format(i))
    def setcolors(self, colors):
        newcolors = askcolor(colors)[1]
        if newcolors == None:
            pass
        else:
            newcolor = [
                newcolors if x == colors else x for x in self.conf["colors"]
                ]
            print(self.conf["colors"])
            print(newcolor)
            with open(self.conffile["colors"], "w") as conffile:
                for i in newcolor:
                    conffile.write("{}\n".format(i))
            os.execv(__file__, sys.argv)
    def popup_add_clock(self):
        def addtz():
            t_z = listbox.get(ACTIVE)
            with open(self.conffile["tz"], "a") as conffile:
                conffile.write("{}\n".format(t_z))
            self.add_clock(listbox.get(ACTIVE))
            self.remove.add_command(
                label=t_z, command=lambda x=t_z: self.removetz(x)
            )
        list_tz = [
            x
            for x in list(pytz.all_timezones)
            if x not in self.conf["timezone"]
        ]
        print(list_tz)
        popup = Toplevel()
        frame = Frame(popup)
        frame.grid(column=0, row=0, columnspan=2)
        listbox = Listbox(frame)
        listbox.grid(column=0, row=0)
        listbox.insert("end", *list_tz)
        scrollbar = Scrollbar(frame, orient="vertical", command=listbox.yview)
        scrollbar.grid(column=2, row=0, sticky="ns")
        listbox.config(yscrollcommand=scrollbar.set)
        Button(popup, text="Add it", command=addtz).grid(column=0, row=1)
        Button(popup, text="Close", command=popup.destroy).grid(
            column=1, row=1
        )
    def add_clock(self, i):
        if (self.conf["col"] + self.conf["row"]) % 2 == 0:
            color = [
                self.conf["colors"][1],
                self.conf["colors"][0],
                self.conf["colors"][3],
                self.conf["colors"][2],
            ]
        else:
            color = self.conf["colors"]
        self.obj[i] = clock.Clock(
            window=self.root,
            tz=i,
            place=[self.conf["col"], self.conf["row"]],
            color=color,
        )

    def readconftz(self):
        """
        initialize clocks to create
        """
        try:
            with open(self.conffile["tz"], "r") as conffile:
                timezones = conffile.readlines()
                timezones = list(map(lambda s: s.strip(), timezones))
            temp_timezones = [x.split("/") for x in timezones]
            timezones = []
            for lists in temp_timezones:
                lists.reverse()
            temp_timezones.sort()
            for lists in temp_timezones:
                lists.reverse()
                self.conf["timezone"].append("/".join(lists))
            for i in self.conf["timezone"]:
                if not i :
                    self.conf["timezone"].remove(i)
        except FileNotFoundError:
            self.conf["timezone"] = ["Etc/UTC"]
        except PermissionError:
            self.conf["timezone"] = ["Etc/UTC"]
        if not self.conf["timezone"]:
            self.conf["timezone"] = ["Etc/UTC"]
    def readconfcolor(self):
        """
        initialize colors
        """
        try:
            with open(self.conffile["colors"], "r") as conf_file:
                colors = conf_file.readlines()
                self.conf["colors"] = list(map(lambda s: s.strip(), colors))
            for i in self.conf["colors"]:
                if not i :
                    self.conf["colors"][self.conf["colors"].index(i)]='white'
                if i=='None' :
                    self.conf["colors"][self.conf["colors"].index(i)]='white'
        except FileNotFoundError:
            self.conf["colors"] = ["bisque", "maroon", "black", "green"]
        except PermissionError:
            self.conf["colors"] = ["bisque", "maroon", "black", "green"]
        except IndexError:
            self.conf["colors"] = ["bisque", "maroon", "black", "green"]
def main(*args):
    """
    main
    """
    print("Launching the App")
    App()
    return args
if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Puis le module clock:

from tkinter import Frame, Label, Entry, StringVar, Button
from tkinter import messagebox
import datetime
import pytz


class Clock:
    """
    For displaying the clocks
    """
    def __init__(self, **kwargs):
        """
        init variables for one Clock
        """
        self.dict_clock = dict()
        self.dict_clock["hourvar"] = StringVar()
        self.dict_clock["t_z"] = kwargs.get("tz")
        self.dict_clock["place"] = kwargs.get("place")
        self.dict_clock["color"] = kwargs.get("color")
        self.root = kwargs.get("window")
        self.dict_clock["separator"] = [".", ":"]
        self.dict_clock["old_day"] = 32
        self.dict_clock["old_year"] = 100000
        self.init_display()
        self.update()
    def init_display(self):
        """
        create widgets
        """
        framebg = [self.dict_clock["color"][0], self.dict_clock["color"][1]]
        clockbg = [self.dict_clock["color"][2], self.dict_clock["color"][3]]
        col = self.dict_clock["place"][0]
        row = self.dict_clock["place"][1]
        self.dict_clock["frme"] = Frame(self.root, bg=framebg[0], width=15)
        self.dict_clock["frme"].grid(row=row, column=col)
        self.dict_clock["label"] = Label(
            self.dict_clock["frme"],
            bg=framebg[0],
            fg=framebg[1],
            font=("Helvetica", 14, "underline"),
            width=15,
            relief="groove",
            borderwidth=1,
            highlightcolor=framebg[0],
        )
        self.dict_clock["label"].grid(row=0, column=0)
        self.dict_clock["entry"] = Entry(
            self.dict_clock["frme"], bg=clockbg[0], fg=clockbg[1]
        )
        self.dict_clock["entry"].config(
            textvariable=self.dict_clock["hourvar"],
            width=5,
            font=("Sans", 22, "bold"),
        )
        self.dict_clock["entry"].grid(row=1, column=0)
        self.dict_clock["labelday"] = Label(
            self.dict_clock["frme"], bg=framebg[0], fg=framebg[1]
        )
        self.dict_clock["labelday"].config(
            width=10, height=2, font=("Courier", 10, "bold")
        )
        self.dict_clock["labelday"].grid(row=2, column=0)
    def update(self):
        """
        update the widgets
        """
        if self.dict_clock["t_z"] != "local":
            self.timezone = pytz.timezone(self.dict_clock["t_z"])
            self.date = datetime.datetime.now(self.timezone)
            table = self.dict_clock["t_z"].split("/")
            city = table[-1]

        else:
            self.date = datetime.datetime.now()
            city = "local"
        minute = str(self.date.minute).zfill(2)
        day = str(self.date.day).zfill(2)
        if int(self.dict_clock["old_day"]) < int(day):
            messagebox.showinfo("info", "{} changed day".format(city))
        month = str(self.date.month).zfill(2)
        year = str(self.date.year).zfill(2)
        if int(self.dict_clock["old_year"]) < int(year):
            messagebox.showinfo("info", "HAPPY NEW YEAR {}".format(city))
        hour = str(self.date.hour).zfill(2)
        self.dict_clock["separator"] = self.dict_clock["separator"][::-1]
        self.dict_clock["label"].config(text=city)
        self.dict_clock["labelday"].config(
            text="{}-{}-{}".format(day, month, year)
        )
        self.dict_clock["hourvar"].set(
            "{}{}{}".format(hour, self.dict_clock["separator"][0], minute)
        )
        self.dict_clock["old_day"] = day
        self.dict_clock["old_year"] = year
        self.root.after(500, self.update)
    def remove(self):
        self.dict_clock["frme"].grid_forget()

Ma question est la suivante: il ne peux pas y avoir plus de 10 (de mémoire) variable de classe, que j'ai contourner en créeant des dictionnaires. N'est ce pas unpythonic de le permettre ?
Le code devient moins lisible, concrètement, cela ne change pas grand chose. Les docstrings ne sont pas super précis , mais c'est plus par flegme que pour autre chose

J'ai aussi lu que lambda, map etc.. étaient unpythonic, mais comment mapper les options des commandes si un menu est réalisé par une boucle ? (ici le menu pour supprimer des horloges)
```

  • # Du python !

    Posté par . Évalué à 3 (+2/-0).

    Ma question est la suivante: il ne peux pas y avoir plus de 10 (de mémoire) variable de classe, que j'ai contourner en créeant des dictionnaires. N'est ce pas unpythonic de le permettre ?
    Le code devient moins lisible, concrètement, cela ne change pas grand chose. Les docstrings ne sont pas super précis , mais c'est plus par flegme que pour autre chose

    Non les dictionnaires c'est bien l'idée, quand tu dois trimballer ta conf d'un fonction/méthode à une autre, tout est groupé dans ton objet, c'est plus facile. Il faut regarder plus loin que la règle "bête", souvent quand on en arrive là c'est qu'il y a moyen de découper/réorganiser de manière plus logique, sur des éléments plus simples à appréhender.

    Dans ton cas, si tu dois initialiser tout un dictionnaire, peut-être que cela mérite une méthode dédiée. self.ma_conf = init_ma_conf() # retourne un dict().

    J'ai aussi lu que lambda, map etc.. étaient unpythonic, mais comment mapper les options des commandes si un menu est réalisé par une boucle ? (ici le menu pour supprimer des horloges)

    Avec les list comprehensions : https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions

    for i in self.conf["timezone"]:
        self.remove.add_command(
            label=i, command=lambda x=i: self.removetz(x)
        )

    devient

    [self.removetz(tz) for tz in self.conf["timezone"]]
  • # Source d'information ?

    Posté par (page perso) . Évalué à 6 (+4/-0).

    il ne peux pas y avoir plus de 10 (de mémoire) variable de classe, que j'ai contourner en créeant des dictionnaires.

    Je veux bien que tu nous donnes la source de cette information, je j'ai jamais rien vu de tel (les espaces de noms en Python sont des dictionnaires, la limite est la mémoire).

    J'ai aussi lu que lambda, map etc.. étaient unpythonic, mais comment mapper les options des commandes si un menu est réalisé par une boucle ? (ici le menu pour supprimer des horloges)

    Pour lambda, ça reste la seule façon de faire des fonctions anonymes, sinon tu peux faire des fonctions locales nommées (déclarées dans une autre fonction) et les utiliser — tu pourras faire de la capture de contexte (ou fermeture ou closure).

    def additionneur(n):
        def ajoute_n(x):
            return x + n
        return ajoute_n

    Pour map et filter, ça peut se réécrire avec des expressions de liste/générateur en compréhension.

    [ fonction_map(x) for x in iterable if condition_filter(x) ]

    Ceci dit, map/filter/reduce sont encore dispos en Python 3 (juste que reduce est dans le module functools, et map/filter retournent des itérateurs).

    Python 3 - Apprendre à programmer en Python avec PyZo et Jupyter Notebook → https://www.dunod.com/sciences-techniques/python-3

  • # Plusieurs bugs

    Posté par . Évalué à 1 (+0/-0).

    La première des choses à revoir, est le relancement de l'application lors des mises à jour des couleurs, non seulement ce n'est pas propre, mais en plus cela provoque des erreurs comme.

    Exception in Tkinter callback
    Traceback (most recent call last):
    File "/usr/local/lib/python3.6/tkinter/init.py", line 1699, in call
    return self.func(*args)
    File "main.py", line 69, in
    label=i, background=i, command=lambda x=i: self.setcolors(x)
    File "main.py", line 102, in setcolors
    os.execv(file, sys.argv)
    PermissionError: [Errno 13] Permission denied

    Il n'est pas bien difficile de mettre à jour en temps réel les couleurs des widgets.
    Puis c'est plus de l'ordre du gadget que de pouvoir changer les couleurs des horloges.
    Ce qui serait intéressant serait que tu génères une couleur en t'appuyant sur le string du timezone, ou alors plus simplement des couleurs aléatoires selon des tons de couleurs prédéfinis.

    • Garde tkinter dans sa boîte, c'est pas bien plus complexe et verbeux d'écrire tk.Menu que Menu, dans le premier cas on sait d'où sort la classe Menu dans le second non, dans ton petit script on ne s'y perd pas, mais dans des scripts plus conséquents, on peut vite s'y perdre et être source de collisions d'identifiants. Importer un ou deux trucs d'un module, cela passe, mais au-delà mieux vaut garder le module importé dans son namespace.

    • Généralement les utilisateurs n'apprécient pas trop les fenêtres qui changent de dimensions, je suis dans ce cas, ce que j'aurais fait à ta place est de créer une fenêtre ou l'on pourrait placer par exemple 4 horloges au maximum (pouvant être pourquoi pas défini via une constante ou un argument facultatif de la classe App pour en mettre plus), le fait de cliquer sur un emplacement vide permettrait d'ajouter une nouvelle horloge, l'ajout d'un petit bouton sur chaque horloge serait suffisant pour les supprimer.

    • Le menu des timezones disponibles est trop long (scroll interminable), il faudrait faire quelque chose de plus élaboré, on choisirait d'abord le continent, puis ensuite le pays/ville, pour cela je te conseille d'utiliser la classe Listbox de tkinter.

    • Il y a des bugs lors des ajouts et suppressions d'horloges (pas pris en compte en temps réel).

    • Je ne comprends pas bien comment fonctionne ton fichier de couleurs, ce que j'aurais fait personnellement est de grouper timezone et couleurs en un unique dictionnaire ou autre structure de données.

    • Les secondes ne variant pas d'une horloge à l'autre, j'aurais pour ma part utilisé une unique variable commune à toutes les horloges afin de garder une synchro parfaite entre les horloges.

    • Les appels en ms étant toujours imprécis, le 1000ms peut très bien être 1002ms effectif, cela varie d'un poste à un autre et en fonction des tâches en cours, ce qui fait qu'au bout d'un long moment il y aura des décalages (même en essayant d'ajuster les ms en se basant sur les microsecondes de datetime, je ne suis pas arrivé à obtenir une synchro aux petits oignons).
      N'est-ce pas d'ailleurs pour cette raison que tu as enlevé les secondes des horloges et que tu fais des appels toutes les 500ms ?
      Je pense que pour obtenir une synchronisation plus élaborée il faudrait utiliser un thread séparé, et ce n'est pas facile à faire.

    • Dans ta classe App, il faudrait répartir ce qui peut l'être dans différentes classes, chargement et enregistrement des timezones, fenêtre de création d'un timezone. Il en va de même pour ta classe Clock, tu pourrais répartir chaque zone d'affichage en d'autres classes (date, timer, timezone).

    • Focalise-toi d'abord sur l'essentiel, les choses comme souhaiter la bonne année, ou changer les couleurs sont d'importances secondaires.

    Bonne continuation !

    • [^] # Re: Plusieurs bugs

      Posté par . Évalué à 2 (+1/-0).

      Super, merci, j'ai bien cortigé l'ajout des timezones dans le menu. Mais l'idée du bouton est excellente. J'ai ausdi mis le temps local dans un label. Sur un autre script, j'ai en effet réussi à modifier les couleurs en live, je vais donc ou essayer de modifier le script ou de supprimer l' option qui était là surtout pour me familiariser avec askcolor

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.