Как организовать проект python, который содержит несколько пакетов, чтобы каждый файл в пакете можно было запускать отдельно?
TL; DR
Вот пример репозитория, который настроен, как описано в первой диаграмме (ниже): https://github.com/Poddster/package_problems
Если вы могли бы сделать так, чтобы он выглядел как вторая диаграмма с точки зрения организации проекта и все еще может выполнять следующие команды, то вы ответили на вопрос:
$ git clone https://github.com/Poddster/package_problems.git
$ cd package_problems
<do your magic here>
$ nosetests
$ ./my_tool/my_tool.py
$ ./my_tool/t.py
$ ./my_tool/d.py
(or for the above commands, $ cd ./my_tool/ && ./my_tool.py is also acceptable)
Альтернативно: дайте мне другую структуру проекта, которая позволяет мне группировать связанные файлы ( "пакет" ), запускать все файлы по отдельности, импортировать файлы в другие файлы в одном пакете и импортировать пакеты/файлы в другие файлы пакетов.
Текущая ситуация
У меня есть куча файлов python. Большинство из них полезны при вызове из командной строки, то есть все они используют argparse и if __name__ == "__main__"
для создания полезных вещей.
В настоящее время у меня есть эта структура каталогов, и все работает нормально:
.
├── config.txt
├── docs/
│ ├── ...
├── my_tool.py
├── a.py
├── b.py
├── c.py
├── d.py
├── e.py
├── README.md
├── tests
│ ├── __init__.py
│ ├── a.py
│ ├── b.py
│ ├── c.py
│ ├── d.py
│ └── e.py
└── resources
├── ...
Некоторые сценарии import
вещи из других скриптов для выполнения своей работы. Но нет script - это просто библиотека, все они вызываемы. например Я мог бы вызывать ./my_tool.py
, ./a.by
, ./b.py
, ./c.py
и т.д., И они будут полезны для пользователя.
"my_tool.py" является основным script, который использует все другие скрипты.
Что я хочу сделать
Однако я хочу изменить способ организации проекта. Сам проект представляет собой целую программу, используемую пользователем, и будет распространяться как таковой, но я знаю, что части ее будут полезны в разных проектах позже, поэтому я хочу попробовать и инкапсулировать текущие файлы в пакет. В ближайшем будущем я также добавлю другие пакеты в этот же проект.
Чтобы облегчить это, я решил реорганизовать проект примерно так:
.
├── config.txt
├── docs/
│ ├── ...
├── my_tool
│ ├── __init__.py
│ ├── my_tool.py
│ ├── a.py
│ ├── b.py
│ ├── c.py
│ ├── d.py
│ ├── e.py
│ └── tests
│ ├── __init__.py
│ ├── a.py
│ ├── b.py
│ ├── c.py
│ ├── d.py
│ └── e.py
├── package2
│ ├── __init__.py
│ ├── my_second_package.py
| ├── ...
├── README.md
└── resources
├── ...
Однако я не могу понять организацию проекта, которая удовлетворяет следующим критериям:
- Все сценарии invokable в командной строке (либо как
my_tool\a.py
или cd my_tool && a.py
)
- Тесты действительно выполняются:)
- Файлы в пакете2 могут выполнять
import my_tool
Основная проблема заключается в операторах импорта, используемых пакетами и тестах.
В настоящее время все пакеты, включая тесты, просто выполняют import <module>
, и он правильно разрешен. Но когда смещение вокруг не работает.
Обратите внимание, что поддержка py2.7 является требованием, поэтому все файлы имеют from __future__ import absolute_import, ...
вверху.
То, что я пробовал, и катастрофические результаты
1
Если я перемещаю файлы, как показано выше, но оставляю все операторы импорта, как они есть в настоящее время:
-
$ ./my_tool/*.py
работает, и все они работают нормально
-
$ nosetests
запустить из верхнего каталога не работает. Тесты не позволяют импортировать сценарии пакетов.
- pycharm выделяет операторы импорта красным цветом при редактировании этих файлов: (
2
Если я затем изменил тестовые сценарии, выполните следующие действия:
from my_tool import x
-
$ ./my_tool/*.py
все еще работает, и все они работают нормально
-
$ nosetests
запустить из верхнего каталога не работает. Затем тесты могут импортировать правильные сценарии, но импорт самих скриптов терпит неудачу, когда тестовые скрипты импортируют их.
- pycharm выделяет операции импорта в красном в основных сценариях: (
3
Если я сохраняю одну и ту же структуру и меняю все на from my_tool import
, тогда:
-
$ ./my_tool/*.py
приводит к ImportError
-
$ nosetests
работает нормально.
- pycharm ни о чем не жалуется
например. от 1.:
Traceback (most recent call last):
File "./my_tool/a.py", line 34, in <module>
from my_tool import b
ImportError: cannot import name b
4
Я также пробовал from . import x
, но это просто заканчивается ValueError: Attempted relative import in non-package
для прямого запуска скриптов.
Глядя на некоторые другие ответы SO:
Я не могу просто использовать python -m pkg.tests.core_test
как
a) У меня нет main.py. Думаю, у меня может быть один?
б) Я хочу, чтобы иметь возможность запускать все скрипты, а не только основные?
Я пробовал:
if __name__ == '__main__' and __package__ is None:
from os import sys, path
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
но это не помогло.
Я также пробовал:
__package__ = "my_tool"
from . import b
Но получил:
SystemError: Parent module 'loading_tool' not loaded, cannot perform relative import
добавление import my_tool
до from . import b
просто заканчивается на ImportError: cannot import name b
Фикс?
Каков правильный набор магических заклинаний и макета каталога, чтобы все это работало?
Ответы
Ответ 1
Как только вы перейдете в нужную конфигурацию, абсолютный импорт, который вы используете для загрузки модулей, специфичных для my_tool
, больше не работает.
После создания подкаталога my_tool
вам потребуется три модификации и переместите в него файлы:
-
Создайте my_tool/__init__.py
. (Вы, кажется, уже это сделали, но я хотел упомянуть об этом для полноты.)
-
В файлах, находящихся непосредственно в разделе my_tool
: измените операторы import
для загрузки модулей из текущего пакета. Итак, в my_tool.py
измените:
import c
import d
import k
import s
to:
from . import c
from . import d
from . import k
from . import s
Вам нужно внести аналогичные изменения во все ваши другие файлы. (Вы говорите, что попробовали установку __package__
, а затем сделали относительный импорт, но установка __package__
не нужна.)
-
В файлах, расположенных в my_tool/tests
: измените операторы import
, которые импортируют код, который вы хотите протестировать, для относительного импорта, загружаемого из одного пакета в иерархии. Итак, в test_my_tool.py
измените:
import my_tool
в
from .. import my_tool
Аналогично для всех других тестовых файлов.
С приведенными выше изменениями я могу напрямую запускать модули:
$ python -m my_tool.my_tool
C!
D!
F!
V!
K!
T!
S!
my_tool!
my_tool main!
|main tool!||detected||tar edit!||installed||keys||LOL||ssl connect||parse ASN.1||config|
$ python -m my_tool.k
F!
V!
K!
K main!
|keys||LOL||ssl connect||parse ASN.1|
и я могу запускать тесты:
$ nosetests
........
----------------------------------------------------------------------
Ran 8 tests in 0.006s
OK
Обратите внимание, что я могу выполнить вышеуказанное как с Python 2.7, так и с Python 3.
Вместо того, чтобы различные модули под my_tool
выполнялись напрямую, я предлагаю использовать правильный файл setup.py
для объявления точек входа и setup.py
создавать эти точки входа при установке пакета. Поскольку вы собираетесь распространять этот код, вы должны использовать setup.py
, чтобы формально его упаковать.
-
Измените модули, которые можно вызвать из командной строки, чтобы вместо my_tool/my_tool.py
вместо этого:
if __name__ == "__main__":
print("my_tool main!")
print(do_something())
У вас есть:
def main():
print("my_tool main!")
print(do_something())
if __name__ == "__main__":
main()
-
Создайте файл setup.py
, который содержит правильный entry_points
. Например:
from setuptools import setup, find_packages
setup(
name="my_tool",
version="0.1.0",
packages=find_packages(),
entry_points={
'console_scripts': [
'my_tool = my_tool.my_tool:main'
],
},
author="",
author_email="",
description="Does stuff.",
license="MIT",
keywords=[],
url="",
classifiers=[
],
)
В приведенном выше файле setup.py
указано script с именем my_tool
, которое вызовет метод main
в модуле my_tool.my_tool
. В моей системе после установки пакета существует script, расположенный в /usr/local/bin/my_tool
, который вызывает метод main
в my_tool.my_tool
. Он производит тот же вывод, что и при запуске python -m my_tool.my_tool
, который я показал выше.
Ответ 2
Точка 1
Я считаю, что он работает, поэтому я не комментирую его.
Точка 2
Я всегда использовал тесты на том же уровне, что и my_tool, но не ниже, но они должны работать, если вы делаете это в верхней части каждого файла тестов (перед импортом my_tool или любого другого файла py в тот же каталог)
import os
import sys
sys.path.insert(0, os.path.abspath(__file__).rsplit(os.sep, 2)[0])
Точка 3
В my_second_package.py сделайте это вверху (перед импортом my_tool)
import os
import sys
sys.path.insert(0,
os.path.abspath(__file__).rsplit(os.sep, 2)[0] + os.sep
+ 'my_tool')
С уважением,
JM
Ответ 3
Чтобы запустить его из командной строки и действовать как библиотека, но позволяя nosetest работать стандартным образом, я считаю, вам придется делать двойной подход к импорту.
Например, для файлов Python потребуется:
try:
import f
except ImportError:
import tools.f as f
Я прошел и сделал PR из github, который вы связали со всеми работами, связанными с тестированием.
https://github.com/Poddster/package_problems/pull/1
Изменить: Забыл импорт в __init__.py
для правильного использования в других пакетах. Теперь вы сможете:
import tools
tools.c.do_something()