Подтвердить что ты не робот

Как организовать проект 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

Фикс?

Каков правильный набор магических заклинаний и макета каталога, чтобы все это работало?

4b9b3361

Ответ 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()