From cc664b59182b40551f70010d6df20e3f70f28a0c Mon Sep 17 00:00:00 2001 From: motius Date: Wed, 3 Jun 2020 23:08:26 +0200 Subject: [PATCH] [article] add examples to dynamic enum --- .../creer-un-enum-python-dynamiquement.md | 104 +++++++++++++++++- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/content/posts/creer-un-enum-python-dynamiquement.md b/content/posts/creer-un-enum-python-dynamiquement.md index 7f8df5b..40eee41 100644 --- a/content/posts/creer-un-enum-python-dynamiquement.md +++ b/content/posts/creer-un-enum-python-dynamiquement.md @@ -11,6 +11,59 @@ 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 : +```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 : +```python +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 : +```python +>>> 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 ? +```python +# 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. @@ -105,8 +158,10 @@ elif state is MonEnumCustom.COMMIT: print("Validé") elif state is MonEnumCustom.SEND: print("Aucune modification possible") -else: +elif ...: ... +else: + raise UnknownEnumStateValue("Code is not in sync with API") ``` # Un petit plus... avec un mixin @@ -140,12 +195,53 @@ class EnumMixin: Avec le mixin de conversion, le code de génération d'`enum` devient : ```python -# ceci n'a pas changé +# inchangé enum_mapping = EnumMappingHelper(d_enum_values) -# cela si +# 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. +```python +# 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 : +```python +# 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. @@ -155,6 +251,8 @@ Cela me permet de combiner les avantages de toutes les solutions à l'exception, 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