• chevron_right

      Shift Happens is a beautifully designed history of how keyboards got this way

      news.movim.eu / ArsTechnica · Tuesday, 3 October, 2023 - 11:30 · 1 minute

    Photos of an Olivetti Praxis 48 electric typewriter

    Enlarge / Marcin Wichary's photos of an Olivetti Praxis 48 electric typewriter. (credit: Marcin Wichary)

    It's the 150th anniversary of the QWERTY keyboard, and Marcin Wichary has put together the kind of history and celebration this totemic object deserves. Shift Happens is a two-volume, 1,200-plus-page work with more than 1,300 photos, researched over seven years and cast lovingly into type and photo spreads that befit the subject.

    You can preorder it now , and orders before October 4 (Wednesday) can still be shipped before Christmas, while orders on October 5 or later will have to wait until December or January. Preorders locked in before Wednesday also get a 160-page "volume of extras."

    Wichary, a designer, engineer, and writer who has worked at Google, Medium, and Figma, has been working in public to get people excited about type, fonts, and text design for some time now. He told the Twitter world about his visit to an obscure, magical Spanish typewriter museum in 2016. He put a lot of work into crafting the link underlines at Medium and explaining font fallbacks at Figma . Shift Happens reads and looks like Wichary's chance to tell the bigger story around all the little things that fascinate him and to lock into history all the strange little stories he loves.

    Read 39 remaining paragraphs | Comments

    • At chevron_right

      Créer un enum Python dynamiquement

      motius · pubsub.gugod.fr / atomtest · Monday, 1 June, 2020 - 22:00 · 8 minutes

    Bonjour ! Aujourd'hui, je vous propose un micro tutoriel pour créer un enum dynamiquement en Python. Je vous accorde qu'on ne fait pas ça tous les jours, c'est d'ailleurs la première fois que je suis tombé sur ce cas. Un peu de contexte En général, un enum, ça ressemble à ça en Python : class MonEnu...

    Bonjour !

    Aujourd'hui, je vous propose un micro tutoriel pour créer un enum dynamiquement en Python. Je vous accorde qu'on ne fait pas ça tous les jours, c'est d'ailleurs la première fois que je suis tombé sur ce cas.

    Un peu de contexte

    En général, un enum, ça ressemble à ça en Python :

    class MonEnumCustom(enum.Enum):
        """docstring
        """
        VALIDATION  = "VALIDATION"
        SEND        = "SEND"

    Si l'enum contient un grand nombre de valeurs, chacune correspondant à un état, on peut aisément les formater avec un programme, afin d'écrire un enum comme ci-dessus. Ça fait simplement un enum comme ci-dessus, mais en plus gros :

    class MonEnumCustom(enum.Enum):
        """docstring
        """
        VALIDATION  = "VALIDATION"
        SEND        = "SEND"
        COMMIT      = "COMMIT"
        REVIEW      = "REVIEW"
        ... # une centaine d'états supplémentaires disponibles

    Mais que faire quand on récupère les valeurs dynamiquement depuis un appel à une API, qui retourne l'ensemble des états possibles sous forme d'une collection, comme ceci :

    >>> ls_remote_states = fetch_all_states(...)
    >>> print(ls_remote_states)
    [
        "VALIDATION",
        "SEND",
        "COMMIT",
        "REVIEW",
        ...
    ]

    et que l'on souhaite créer un enum à partir de ladite collection afin d'écrire du code qui dépendra de valeurs d'états prise dans l'enum comme ci-dessous ?

    # pseudo-code
    ls_remote_states = fetch_all_states(...)
    MonEnumCustom = create_enum_type_from_states(ls_remote_states)
    
    # dans le code
    if state is MonEnumCustom.VALIDATION:
        print("En cours de validation")
    elif state is MonEnumCustom.COMMIT:
        ...
    else:
        raise

    C'est le sujet de la discussion qui suit, qui va présenter les options disponibles, et quelques considérations quant à leurs usages.

    Le scénario

    Je souhaitais pouvoir m'interfacer avec un service qui définit un grand nombre (un peu plus d'une centaine) de constantes qu'il me renvoie sous forme d'une collection de chaînes de caractères. Trois choix s'offraient ainsi à moi :

    • utiliser les constantes telles quelles dans une collection (list, dict...)

      Avantages :

      • rapide & facile

      Inconvénients :

      • sémantiquement pauvre
      • la liste des états valides n'est jamais écrite dans le code
    • faire un copié collé et du formatage (avec vim, c'est facile et rapide)

      Avantages :

      • rapide & facile
      • l'intégralité des valeurs connues de l'enum peuvent être lues dans le programme à la seule lecture du code

      Inconvénients :

      • duplique un enum dont je ne suis pas responsable
    • générer dynamiquement l'enum

      Avantages :

      • résout les inconvénients des solutions précédentes
      • évite la duplication de code inutile. Il arrive assez souvent que l'on doive définir un même enum à plusieurs endroits dans un système informatique, par exemple en base de donnée dans Postgre, puis en Java / Python, et enfin en Typescript si vous faites du développement sur toute la stack. Mais dans ce cas-ci, j'aurais redéfini un enum sans que mon code ne soit la source d'autorité sur celui-ci, ce qui contrevient au principe du Single Source of Truth, c'est donc plus gênant que le simple problème de duplication de code.
      • Plus facile à adapter lors de l'évolution de l'enum. Ce dernier point est vrai dans mon cas où j'ai l'assurance que l'API ne fera qu'augmenter les états et n'invalidera jamais un état existant. Dans le cas contraire, il faudra changer les fonctions qui utilisent les états définis dans l'enum, avec ou sans la génération d'enum automatique.

      Inconvénients :

      • à nouveau, la liste des états valides n'est jamais écrite dans le code
      • Un tantinet plus long, surtout si tout ne va pas sur des roulettes du premier coup.

    Et si je mentionne les roulettes, c'est que vous imaginez bien que ça n'a pas marché du premier coup (sinon je n'écrirais pas cet article).

    La théorie

    Selon la documentation officielle, il suffit de passer 3 paramètres à la fonction type afin de créer une classe dynamiquement. Si vous voulez mon avis, on est là dans la catégorie des fonctionnalités de Python vraiment puissantes pour le prototypage (à côté de eval et exec, même si ces derniers devraient être quasiment interdits en production).

    Chouette, me dis-je en mon fort intérieur, ceci devrait marcher :

    # récupération du dictionnaire des valeurs
    d_enum_values: dict = fetch_all_states(...)
    # création dynamique de l'enum
    MonEnumCustom = type("MonEnumCustom", (enum.Enum,), d_enum_values)

    La pratique

    Ne vous fatiguez pas, ça ne marche pas. La solution que j'ai trouvée est un contournement dégoûtant (dû justement au module enum de Python).

    Je contourne les problèmes de création d'enum à l'aide d'un mapper qui ressemble à ça :

    class EnumMappingHelper(dict): # héritage pour contourner type
        def __init__(self, mapping: dict = None):
            self._mapping = mapping
    
        def __getitem__(self, item):
            if item == "_ignore_": # contournement d'enum
                return []
            return self._mapping[item]
    
        def __delitem__(self, item):
            return item # contournement d'enum
    
        def __getattr__(self, key): # Accès au clefs de l'enum par attribut
            return self._mapping[key]
    
        @property
        def _member_names(self): # contournement d'enum
            return dict(self._mapping)
    
    enum_mapping = EnumMappingHelper(d_enum_values)
    MonEnumCustom = type("MonEnumCustom", (enum.Enum,), enum_mapping)

    Je m'en sors donc en torturant un peu un classe personnalisée que j'appelle EnumMappingHelper afin que celle-ci se comporte d'une façon qui soit acceptable à la fois pour la fonction type et pour le module enum.

    Je peux utiliser les variantes de mon enum ainsi :

    if state is MonEnumCustom.VALIDATION:
        print("En cours de validation")
    elif state is MonEnumCustom.COMMIT:
        print("Validé")
    elif state is MonEnumCustom.SEND:
        print("Aucune modification possible")
    elif ...:
        ...
    else:
        raise UnknownEnumStateValue("Code is not in sync with API")

    Un petit plus... avec un mixin

    Je dispose bien entendu d'une fonction qui permet de mapper un état sous forme de chaîne de caractères à la valeur représentée dans l'enum de référence. Moralement et en très très gros, elle ressemble à ça :

    @functools.lru_cache
    def map_str_to_custom_enum(s: str) -> MonEnumCustom:
        """docstring
        """
        return dict((v.value, v) for v in MonEnumCustom)[s]

    L'idée étant donc de faire le mapping inverse de celui fourni par 'enum, i.e. de générer automatiquement et efficacement une variante d'enum à partir de sa représentation sous forme de chaîne de caractères.

    Sauf qu'en réalité, la fonction n'est pas du tout implémentée ainsi. À la place, j'utilise un mixin, ce qui me permet d'avoir la même fonctionnalité sur tous les enum. La fonction ci-dessus est remplacée par une méthode de classe. Le cache lru_cache est remplacé par un attribut de classe de type dictionnaire, ce qui évite toutes sortes d'inconvénients.

    class EnumMixin:
        _enum_values = {}
        def __init__(self, *args, **kwargs):
            self.__class__._enum_values[args[0]] = self
    
        @classmethod
        def convert_str_to_enum_variant(cls, value: str):
            cls._enum_values
            return cls._enum_values(value, object())

    Avec le mixin de conversion, le code de génération d'enum devient :

    # inchangé
    enum_mapping = EnumMappingHelper(d_enum_values)
    # MonEnumCustom hérite en premier du mixin EnumMixin
    MonEnumCustom = type("MonEnumCustom", (EnumMixin, enum.Enum), enum_mapping)

    Gestion de version de l'API pour les variantes de l'enum

    Enfin en ce qui concerne la gestion de version de l'API, il est possible de rajouter un assert dans le code qui vérifie toutes les variantes de l'enum, afin de lever une exception le plus tôt possible et de ne pas faire tourner du code qui ne serait pas en phase avec la version de l'API utilisée.

    # je définis la variable suivante en copiant directement les valeurs
    # récupérées par un appel à l'API au moment du développement :
    ls_enum_variantes_copie_statiquement_dans_mon_code = [
        "VALIDATION",
        "SEND",
        "COMMIT",
        "REVIEW",
        ... # et une centaine d'états supplémentaires
    ]
    # si la ligne suivante lance une AssertionError, l'API a mis à jour
    # les variantes de l'enum
    assert fetch_all_states(...) == ls_enum_variantes_copie_statiquement_dans_mon_code

    Dans mon cas, je suis plus souple, car ayant la garantie que les variantes publiées de mon enum ne seront pas dépréciées par de nouvelles versions de l'API, je vérifie simplement cette assertion :

    # idem
    ls_enum_variantes_copie = [
        "VALIDATION",
        "SEND",
        "COMMIT",
        "REVIEW",
        ... # et une centaine d'états supplémentaires
    ]
    # si la ligne suivante lance une AssertionError, l'API n'a pas honoré son
    # contrat de ne pas déprécier les variantes de l'enum.
    assert set(fetch_all_states(...)).issuperset(set(ls_enum_variantes_copie))

    Notez que si je souhaite être strict et n'autoriser aucun changement des variantes de l'API, alors il est préférable de copier coller les variantes directement dans le code et de comparer ces valeurs à l'exécution. Le code en est rendu plus lisible. Mais ce n'est pas le cas de figure dans lequel je me trouve.

    Conclusion

    Seule la solution utilisant un copier coller permettait de voir la liste des valeurs de l'enum dans le code. Je combine les avantages de cette solution et de la troisième que j'ai implémentée en copiant les valeurs prises par l'enum dans la docstring de sa classe et en mentionnant la version du service distant associé. Cela me permet de combiner les avantages de toutes les solutions à l'exception, bien sûr, d'un peu de temps passé.

    L'inconvénient inattendu de ce code, c'est celui de devoir créer une classe d'aide dont le seul but soit de forcer un comportement afin d'obtenir le résultat escompté. Si vous avez des suggestions pour améliorer ce bazar, je prends.

    Joyeux code !

    Motius