J'ai essayé de lire des questions sur les importations par les frères et même documentation du paquet , mais je n'ai pas encore trouvé de réponse.
Avec la structure suivante:
├── LICENSE.md
├── README.md
├── api
│ ├── __init__.py
│ ├── api.py
│ └── api_key.py
├── examples
│ ├── __init__.py
│ ├── example_one.py
│ └── example_two.py
└── tests
│ ├── __init__.py
│ └── test_one.py
Comment les scripts des répertoires examples
et tests
peuvent-ils être importés à partir du module api
et exécutés à partir de la ligne de commande?
Aussi, je voudrais éviter le laid sys.path.insert
hack pour chaque fichier. Cela peut sûrement être fait en Python, non?
Depuis que j'ai écrit la réponse ci-dessous, modifier sys.path
Reste un truc rapide qui fonctionne bien pour les scripts privés, mais plusieurs améliorations ont été apportées.
setup.cfg
Pour stocker le métadonnées)-m
et s'exécuter en tant que paquet fonctionne également (mais s'avérera un peu gênant si vous voulez convertir votre répertoire de travail en un paquet installable).sys.path
Pour vousDonc, cela dépend vraiment de ce que vous voulez faire. Dans votre cas, cependant, puisqu'il semble que votre objectif soit de créer un package correct à un moment donné, l'installation via pip -e
Est probablement votre meilleur pari, même s'il n'est pas encore parfait.
Comme déjà indiqué ailleurs, la terrible vérité est que vous devez faire de vilains stratagèmes pour autoriser les importations de modules de frères et sœurs ou de paquetages parents à partir d'un module __main__
. La question est détaillée dans PEP 366 . PEP 3122 a tenté de traiter les importations de manière plus rationnelle, mais Guido l’a rejeté
Le seul cas d'utilisation semble être l'exécution de scripts vivant dans le répertoire d'un module, ce que j'ai toujours considéré comme un anti-modèle.
( ici )
Bien que j’utilise régulièrement ce motif avec
# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __== "__main__" and __package__ is None:
from sys import path
from os.path import dirname as dir
path.append(dir(path[0]))
__package__ = "examples"
import api
Ici path[0]
Est le dossier parent de votre script en cours d'exécution et dir(path[0])
votre dossier de niveau supérieur.
Je n'ai toujours pas pu utiliser les importations relatives avec cela, cependant, mais cela autorise les importations absolues à partir du niveau supérieur (dans votre exemple, le dossier parent de api
).
Il y a beaucoup de sys.path.append
_ -hacks disponible, mais j’ai trouvé un autre moyen de résoudre le problème: The setuptools . Je ne suis pas sûr s'il existe des cas Edge qui ne fonctionnent pas bien avec cela. Ce qui suit est testé avec Python 3.6.5, (Anaconda, version 4.5.1), ordinateur Windows 10.
Le point de départ est la structure de fichier que vous avez fournie, encapsulée dans un dossier appelé myproject
.
.
└── myproject
├── api
│ ├── api_key.py
│ ├── api.py
│ └── __init__.py
├── examples
│ ├── example_one.py
│ ├── example_two.py
│ └── __init__.py
├── LICENCE.md
├── README.md
└── tests
├── __init__.py
└── test_one.py
Je vais appeler le .
le dossier racine, et dans mon exemple, il se trouve à l'adresse C:\tmp\test_imports\
.
En guise de test, utilisons ce qui suit ./api/api.py
def function_from_api():
return 'I am the return value from api.api!'
from api.api import function_from_api
def test_function():
print(function_from_api())
if __== '__main__':
test_function()
PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
File ".\myproject\tests\test_one.py", line 1, in <module>
from api.api import function_from_api
ModuleNotFoundError: No module named 'api'
En utilisant from ..api.api import function_from_api
aboutirait à
PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
File ".\tests\test_one.py", line 1, in <module>
from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package
Le contenu de la setup.py
serait*
from setuptools import setup, find_packages
setup(name='myproject', version='1.0', packages=find_packages())
Si vous connaissez bien les environnements virtuels, activez-en un et passez à l'étape suivante. L'utilisation des environnements virtuels n'est pas absolument nécessaire, mais ils vont vraiment vous aider à long terme (lorsque vous avez plus d'un projet en cours..). Les étapes les plus élémentaires sont (exécutées dans le dossier racine)
python -m venv venv
source ./venv/bin/activate
(Linux, macOS) ou ./venv/Scripts/activate
(Win)Pour en savoir plus à ce sujet, il suffit de sortir Google du "didacticiel python virtual env" ou similaire. Vous n'avez probablement jamais besoin d'autres commandes que la création, l'activation et la désactivation.
Une fois que vous avez créé et activé un environnement virtuel, votre console doit donner le nom de l’environnement virtuel entre parenthèses.
PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>
et votre arborescence de dossiers devrait ressembler à ceci **
.
├── myproject
│ ├── api
│ │ ├── api_key.py
│ │ ├── api.py
│ │ └── __init__.py
│ ├── examples
│ │ ├── example_one.py
│ │ ├── example_two.py
│ │ └── __init__.py
│ ├── LICENCE.md
│ ├── README.md
│ └── tests
│ ├── __init__.py
│ └── test_one.py
├── setup.py
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
Installez votre paquet de niveau supérieur myproject
en utilisant pip
. L'astuce consiste à utiliser le -e
drapeau lors de l’installation. De cette façon, il est installé dans un état éditable et toutes les modifications apportées aux fichiers .py seront automatiquement incluses dans le package installé.
Dans le répertoire racine, exécutez
pip install -e .
_ (notez le point, il signifie "répertoire actuel")
Vous pouvez également voir qu'il est installé en utilisant pip freeze
(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
myproject.
dans vos importationsNotez que vous devrez ajouter myproject.
uniquement dans les importations qui ne fonctionneraient pas autrement. Les importations qui ont fonctionné sans le setup.py
& pip install
fonctionnera toujours bien. Voir un exemple ci-dessous.
Maintenant, testons la solution en utilisant api.py
défini ci-dessus, et test_one.py
défini ci-dessous.
from myproject.api.api import function_from_api
def test_function():
print(function_from_api())
if __== '__main__':
test_function()
(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!
* Voir le setuptools docs pour plus d'exemples détaillés de setup.py.
** En réalité, vous pouvez placer votre environnement virtuel n'importe où sur votre disque dur.
Voici une autre alternative que j'insère en haut du dossier Python du dossier tests
:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Vous n'avez pas besoin et ne devriez pas pirater sys.path
sauf si c'est nécessaire et dans ce cas, ce n'est pas le cas. Utilisation:
import api.api_key # in tests, examples
Exécuter à partir du répertoire du projet: python -m tests.test_one
.
Vous devriez probablement déplacer tests
(s’ils sont unittests d’api) à l’intérieur de api
et exécuter python -m api.test
pour exécuter tous les tests (en supposant qu'il y ait __main__.py
) ou python -m api.test.test_one
courir test_one
au lieu.
Vous pouvez également supprimer __init__.py
de examples
(ce n'est pas un package Python)) et exécutez les exemples dans un virtualenv où api
est installé, par exemple, pip install -e .
dans un virtualenv installerait le paquet inplace api
si vous avez le bon setup.py
.
Je ne dispose pas encore de la compréhension de la pythonologie nécessaire pour comprendre le moyen envisagé de partager le code entre des projets indépendants, sans bidouille d'importation entre frères et soeurs. Jusqu'à ce jour, c'est ma solution. Pour examples
ou tests
importer des éléments de ..\api
, cela ressemblerait à:
import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
Pour les importations de paquets de frères et soeurs, vous pouvez utiliser soit le insert soit le append méthode du module [sys.path] [2] :
if __== '__main__' and if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
Cela fonctionnera si vous lancez vos scripts comme suit:
python examples/example_one.py
python tests/test_one.py
D'autre part, vous pouvez également utiliser l'importation relative:
if __== '__main__' and if __package__ is not None:
import ..api.api
Dans ce cas, vous devrez lancer votre script avec le '- m' argument (notez que, dans ce cas, vous ne devez pas donner le '. Py' extension):
python -m packageName.examples.example_one
python -m packageName.tests.test_one
Bien sûr, vous pouvez mélanger les deux approches pour que votre script fonctionne, peu importe comment il s'appelle:
if __== '__main__':
if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
else:
import ..api.api
TLDR
Cette méthode ne nécessite ni setuptools, ni hacks de chemins, ni arguments de ligne de commande supplémentaires, ni spécification du niveau supérieur du package dans chaque fichier de votre projet.
Il suffit de faire un script dans le répertoire parent de ce que vous appelez pour être votre __main__
et tout courir à partir de là. Pour plus d'explications, continuez à lire.
Explication
Cela peut être accompli sans pirater un nouveau chemin ensemble, sans argument supplémentaire en ligne de commande ou sans ajouter de code à chacun de vos programmes pour reconnaître ses frères et sœurs.
La raison pour laquelle cela échoue, comme je crois mentionné précédemment, est que les programmes appelés ont leur __name__
défini comme __main__
. Lorsque cela se produit, le script appelé accepte de se placer au niveau supérieur du package et refuse de reconnaître les scripts des répertoires frères.
Cependant, tout ce qui se trouve sous le niveau supérieur du répertoire reconnaîtra toujours TOUT AUTRE sous le niveau supérieur. Cela signifie que SEULEMENT == ===================================================== Pour obtenir des fichiers dans les répertoires frères pour se reconnaître/utiliser est de les appeler à partir d'un script dans leur répertoire parent.
Proof of Concept Dans un répertoire avec la structure suivante:
.
|__Main.py
|
|__Siblings
|
|___sib1
| |
| |__call.py
|
|___sib2
|
|__callsib.py
Main.py
contient le code suivant:
import sib1.call as call
def main():
call.Call()
if __== '__main__':
main()
sib1/call.py contient:
import sib2.callsib as callsib
def Call():
callsib.CallSib()
if __== '__main__':
Call()
et sib2/callsib.py contient:
def CallSib():
print("Got Called")
if __== '__main__':
CallSib()
Si vous reproduisez cet exemple, vous remarquerez que l'appel de Main.py
entraînera l’impression "Got Called" comme défini dans sib2/callsib.py
même si sib2/callsib.py
a été appelé par sib1/call.py
. Cependant, si l'on appelait directement sib1/call.py
(après avoir apporté les modifications appropriées aux importations), une exception est levée. Même si cela fonctionnait lorsqu'il était appelé par le script dans son répertoire parent, cela ne fonctionnerait pas s'il se croyait au premier niveau du paquet.
Juste au cas où quelqu'un utilisant Pydev sur Eclipse se retrouverait ici: vous pouvez ajouter le chemin parent du frère (et donc le parent du module appelant) en tant que dossier de bibliothèque externe en utilisant Projet-> Propriétés et en mettant Bibliothèques externes dans le menu de gauche Pydev-PYTHONPATH. Ensuite, vous pouvez importer de votre frère, e. g. from sibling import some_class
.
Vous devez vérifier comment les instructions d'importation sont écrites dans le code associé. Si examples/example_one.py
utilise la déclaration d'importation suivante:
import api.api
... alors il s'attend à ce que le répertoire racine du projet se trouve dans le chemin système.
Le moyen le plus simple de prendre en charge ceci sans aucun piratage (comme vous le dites) serait d’exécuter les exemples à partir du répertoire de niveau supérieur, comme ceci:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
J'ai créé un exemple de projet pour montrer comment j'ai géré cela, ce qui est en fait un autre hack de sys.path, comme indiqué ci-dessus. Exemple d'importation de frères et soeurs Python , qui repose sur:
if __== '__main__': import os import sys sys.path.append(os.getcwd())
Cela semble assez efficace tant que votre répertoire de travail reste à la racine du projet Python. Si quelqu'un le déploie dans un environnement de production réel, il serait bon de savoir s'il fonctionne là-bas. ainsi que.