Automatisation de l'importation Python

ÀAprès
import math import os.path import requests # 100500 other imports print(math.pi) print(os.path.join('my', 'path')) print(requests.get) 
 import smart_imports smart_imports.all() print(math.pi) print(os_path.join('my', 'path')) print(requests.get) 
Il se trouve que depuis 2012, je développe un navigateur open source, étant le seul programmeur. En Python par lui-même. Le navigateur n'est pas la chose la plus simple, maintenant dans la partie principale du projet, il y a plus de 1000 modules et plus de 120 000 lignes de code Python. Au total, ce sera une fois et demie de plus avec des projets satellites.

À un moment donné, j'étais fatigué de jouer avec les planchers des importations au début de chaque fichier, et j'ai décidé de régler ce problème une fois pour toutes. La bibliothèque smart_imports est donc née ( github , pypi ).

L'idée est assez simple. Tout projet complexe finit par former son propre accord sur tout nommer. Si cet accord est transformé en règles plus formelles, alors toute entité peut être importée automatiquement par le nom de la variable qui lui est associée.

Par exemple, vous n'aurez pas besoin d'écrire d' import math pour accéder à math.pi - nous pouvons math.pi comprendre que dans ce cas, les math sont un module de la bibliothèque standard.

Les importations intelligentes prennent en charge Python> = 3.5. La bibliothèque est entièrement couverte par des tests, couverture> 95% . Je l'utilise moi-même depuis un an maintenant.

Pour plus de détails, je vous invite à Cat.

Comment ça marche en général?


Ainsi, le code de l'image d'en-tête fonctionne comme suit:

  1. Lors d'un appel à smart_imports.all() bibliothèque construit l' AST du module à partir duquel l'appel est effectué;
  2. Trouver des variables non initialisées;
  3. Nous exécutons le nom de chaque variable via une séquence de règles qui essaient de trouver le module (ou l'attribut de module) qui est nécessaire pour l'importation par nom. Si une règle a trouvé l'entité requise, les règles suivantes ne sont pas vérifiées.
  4. Les modules trouvés sont chargés, initialisés et placés dans l'espace de noms global (ou les attributs nécessaires de ces modules y sont placés).

Les variables non initialisées sont recherchées dans tout le code, y compris la nouvelle syntaxe.

L'importation automatique est activée uniquement pour les composants de projet qui appellent explicitement smart_imoprts.all() . En outre, l'utilisation d'importations intelligentes n'interdit pas l'utilisation d'importations conventionnelles. Cela vous permet d'implémenter la bibliothèque progressivement et de résoudre les dépendances cycliques complexes.

Un lecteur méticuleux remarquera que le module AST est construit deux fois:

  • CPython le construit pour la première fois lors de l'importation du module;
  • La deuxième fois, smart_imports le construit lors d'un appel à smart_imports.all() .

AST ne peut vraiment être construit qu'une seule fois (pour cela, vous devez intégrer dans le processus d'importation des modules à l'aide de crochets d'importation mis en œuvre dans PEP-0302 , mais cette solution ralentit l'importation.

Pourquoi le pensez-vous?
En comparant les performances de deux implémentations (avec et sans crochets), je suis arrivé à la conclusion que lors de l'importation d'un module, CPython construit AST dans ses structures de données internes (C-shh). Les convertir en structures de données Python coûte plus cher que de construire un arbre à partir de la source à l'aide du module ast .

Bien sûr, l'AST de chaque module n'est construit et analysé qu'une seule fois par lancement.

Règles d'importation par défaut


La bibliothèque peut être utilisée sans configuration supplémentaire. Par défaut, il importe les modules selon les règles suivantes:

  1. Par coïncidence exacte du nom, il recherche le module à côté du module actuel (dans le même répertoire).
  2. Vérifie les modules de la bibliothèque standard:
    • par correspondance exacte du nom des packages de niveau supérieur;
    • pour les packages et modules imbriqués, vérifie les noms composés, en remplaçant les points par des traits de soulignement. Par exemple, os.path sera importé si la variable os_path est os_path .
  3. Par correspondance exacte du nom, il recherche les packages tiers installés. Par exemple, les demandes de package bien connues.

Performances


Les importations intelligentes n'affectent pas les performances du programme, mais augmentent le temps de lancement.

En raison de la reconstruction de l'AST, le temps de la première exécution augmente d'environ 1,5 à 2 fois. Pour les petits projets, cela n'est pas significatif. Dans les grands projets, le temps de démarrage souffre de la structure de dépendance entre les modules plutôt que du temps d'importation d'un module particulier.

Lorsque les importations intelligentes deviennent populaires, je réécris le travail d'AST en C - cela devrait réduire considérablement les coûts de démarrage.

Pour accélérer le chargement, les résultats du traitement des modules AST peuvent être mis en cache sur le système de fichiers. La mise en cache est activée dans la configuration. Bien sûr, le cache est désactivé lorsque vous modifiez la source.

Le temps de démarrage est affecté à la fois par la liste des règles de recherche de module et par leur séquence. Étant donné que certaines règles utilisent la fonctionnalité Python standard pour rechercher des modules. Vous pouvez exclure ces dépenses en indiquant explicitement la correspondance des noms et des modules à l'aide de la règle «Noms personnalisés» (voir ci-dessous).

La configuration


La configuration par défaut a été décrite précédemment. Il devrait suffire de travailler avec la bibliothèque standard dans de petits projets.

Configuration par défaut
 { "cache_dir": null, "rules": [{"type": "rule_local_modules"}, {"type": "rule_stdlib"}, {"type": "rule_predefined_names"}, {"type": "rule_global_modules"}] } 


Si nécessaire, une configuration plus complexe peut être mise sur le système de fichiers.

Un exemple de configuration complexe (à partir d'un navigateur).

Lors d'un appel à smart_import.all() bibliothèque détermine la position du module appelant sur le système de fichiers et commence à rechercher le fichier smart_imports.json dans la direction du répertoire en cours à la racine. Si un tel fichier est trouvé, il est considéré comme la configuration du module actuel.

Vous pouvez utiliser plusieurs configurations différentes (en les plaçant dans différents répertoires).

Il n'y a pas beaucoup d'options de configuration maintenant:

 { //     AST. //     null —   . "cache_dir": null|"string", //       . "rules": [] } 

Importer des règles


L'ordre de spécification des règles dans la configuration détermine l'ordre de leur application. La première règle qui a fonctionné arrête la poursuite de la recherche d'importations.

Dans les exemples de configs, la règle rule_predefined_names apparaîtra souvent rule_predefined_names - rule_predefined_names , il est nécessaire que les fonctions intégrées (par exemple, print ) soient correctement reconnues.

Règle 1: Noms prédéfinis


La règle vous permet d'ignorer les noms prédéfinis comme __file__ et les fonctions intégrées comme print .

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}] # } import smart_imports smart_imports.all() #        __file__ #        print(__file__) 

Règle 2: Modules locaux


Vérifie s'il existe un module avec le nom spécifié à côté du module actuel (dans le même répertoire). S'il y en a, l'importe.

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules"}] # } # #    : # # my_package # |-- __init__.py # |-- a.py # |-- b.py # b.py import smart_imports smart_imports.all() #    "a.py" print(a) 

Règle 3: modules globaux


Tente d'importer un module directement par son nom. Par exemple, le module des demandes .

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_global_modules"}] # } # #    # # pip install requests import smart_imports smart_imports.all() #    requests print(requests.get('http://example.com')) 

Règle 4: Noms personnalisés


Correspond au nom d'un module particulier ou à son attribut. La conformité est indiquée dans la configuration de la règle.

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_custom", # "variables": {"my_import_module": {"module": "os.path"}, # "my_import_attribute": {"module": "random", "attribute": "seed"}}}] # } import smart_imports smart_imports.all() #       #        print(my_import_module) print(my_import_attribute) 

Règle 5: modules standard


Vérifie si le nom est un module de bibliothèque standard. Par exemple math ou os.path qui se transforme en os_path .

Il fonctionne plus rapidement que la règle d'importation de modules globaux, car il vérifie la présence d'un module dans une liste mise en cache. Les listes pour chaque version de Python proviennent d'ici: github.com/jackmaney/python-stdlib-list

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_stdlib"}] # } import smart_imports smart_imports.all() print(math.pi) 

Règle 6: Importation par préfixe


Importe un module par son nom, à partir du package associé à son préfixe. Il est pratique à utiliser lorsque plusieurs packages sont utilisés dans le code. Par exemple, les modules du package utils sont accessibles avec le préfixe utils_ .

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_prefix", # "prefixes": [{"prefix": "utils_", "module": "my_package.utils"}]}] # } # #    : # # my_package # |-- __init__.py # |-- utils # |-- |-- __init__ # |-- |-- a.py # |-- |-- b.py # |-- subpackage # |-- |-- __init__ # |-- |-- c.py # c.py import smart_imports smart_imports.all() print(utils_a) print(utils_b) 

Règle 7: Le module du package parent


Si vous avez des sous-packages du même nom dans différentes parties du projet (par exemple, des tests ou des migrations ), vous pouvez leur permettre de rechercher des modules à importer par nom dans les packages parents.

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules_from_parent", # "suffixes": [".tests"]}] # } # #    : # # my_package # |-- __init__.py # |-- a.py # |-- tests # |-- |-- __init__ # |-- |-- b.py # b.py import smart_imports smart_imports.all() print(a) 

Règle 8: liaison à un autre package


Pour les modules d'un package spécifique, il permet la recherche d'importations par nom dans d'autres packages (spécifiés dans la configuration). Dans mon cas, cette règle était utile pour les cas où je ne voulais pas étendre le travail de la règle précédente (Module du package parent) à l'ensemble du projet.

Exemple
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules_from_namespace", # "map": {"my_package.subpackage_1": ["my_package.subpackage_2"]}}] # } # #    : # # my_package # |-- __init__.py # |-- subpackage_1 # |-- |-- __init__ # |-- |-- a.py # |-- subpackage_2 # |-- |-- __init__ # |-- |-- b.py # a.py import smart_imports smart_imports.all() print(b) 

Ajout de vos propres règles


Ajouter votre propre règle est assez simple:

  1. Nous héritons de la classe smart_imports.rules.BaseRule .
  2. Nous réalisons la logique nécessaire.
  3. Enregistrez une règle à l'aide de la méthode smart_imports.rules.register
  4. Ajoutez la règle à la configuration.
  5. ???
  6. Bénéfice

Un exemple peut être trouvé dans la mise en œuvre des règles actuelles.

Bénéfice


Les listes multilignes d'importations au début de chaque source ont disparu.

Le nombre de lignes a diminué. Avant que le navigateur ne passe aux importations intelligentes, il avait 6688 lignes responsables de l'importation. Après la transition, 2084 sont restés (deux lignes de smart_imports par fichier + 130 importations, appelées explicitement depuis des fonctions et des endroits similaires).

Un bon bonus était la standardisation des noms dans le projet. Le code est devenu plus facile à lire et à écrire. Il n'est pas nécessaire de penser aux noms des entités importées - il existe des règles claires qui sont faciles à suivre.

Plans de développement


J'aime l'idée de définir les propriétés du code par des noms de variables, je vais donc essayer de le développer à la fois dans les importations intelligentes et dans d'autres projets.

Concernant les importations intelligentes, je prévois:

  1. Ajoutez la prise en charge des nouvelles versions de Python.
  2. Explorez la possibilité de s'appuyer sur les pratiques communautaires actuelles en matière d'annotation de type de code.
  3. Explorez la possibilité de faire des importations paresseuses.
  4. Implémentez des utilitaires pour la génération automatique d'une configuration à partir des codes source et la refactorisation des sources pour l'utilisation de smart_imports.
  5. Réécrivez une partie du code C pour accélérer le travail avec l'AST.
  6. Développer l'intégration avec les linters et les IDE si ceux-ci ont des problèmes avec l'analyse de code sans importations explicites.

De plus, je suis intéressé par votre avis sur le comportement par défaut de la bibliothèque et les règles d'importation.

Merci d'avoir maîtrisé cette feuille de texte :-D

Source: https://habr.com/ru/post/fr459930/


All Articles