Импорт родственных пакетов
Я пытался прочитать вопросы об импорте родственных пакетов и даже документацию по пакету, но пока не нашел ответа.
Со следующей структурой:
├── 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
Как скрипты в директориях examples
и tests
могут импортироваться из модуля api
и запускаться из командной строки?
Кроме того, я бы хотел избежать уродливого sys.path.insert
взлома для каждого файла. Конечно, это можно сделать на Python, не так ли?
Переведено автоматически
Ответ 1
Устали от взломов sys.path?
Доступно множество sys.path.append
-хаков, но я нашел альтернативный способ решения данной проблемы.
Краткие сведения
- Перенесите код в одну папку (например,
packaged_stuff
) - Создайте
pyproject.toml
файл для описания вашего пакета (см. Минимумpyproject.toml
ниже) - Pip установит пакет в редактируемом состоянии с помощью
pip install -e <myproject_folder>
- Импорт с помощью
from packaged_stuff.modulename import function_name
Настройка
Отправной точкой является предоставленная вами файловая структура, заключенная в папку с именем 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
Я вызову .
корневую папку, и в моем примере она находится по адресу C:\tmp\test_imports\
.
api.py
В качестве тестового примера давайте воспользуемся следующим ./api/api.py
def function_from_api():
return 'I am the return value from api.api!'
test_one.py
from api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
Попробуйте запустить test_one:
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'
Также попытка относительного импорта не сработает:
Использование from ..api.api import function_from_api
приведет к
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
Шаги
1) Создайте файл pyproject.toml в каталог корневого уровня
(ранее люди использовали файл setup.py)
Содержимое для минимального pyproject.toml
будет*
[project]
name = "myproject"
version = "0.1.0"
description = "My small project"
[build-system]
build-backend = "flit_core.buildapi"
requires = ["flit_core >=3.2,<4"]
2) Используйте виртуальную среду
Если вы знакомы с виртуальными средами, активируйте одну из них и переходите к следующему шагу. Использование виртуальных сред не абсолютно обязательно, но они действительно помогут вам в долгосрочной перспективе (когда у вас выполняется более 1 проекта ..). Самые простые шаги (запускаются в корневой папке)
- Создание виртуального env
python -m venv venv
- Активировать виртуальную среду env
source ./venv/bin/activate
(Linux, macOS) или./venv/Scripts/activate
(Win)
Чтобы узнать больше об этом, просто найдите в Google "python virtual env tutorial" или аналогичный. Вероятно, вам никогда не понадобятся никакие другие команды, кроме создания, активации и деактивации.
После создания и активации виртуальной среды в вашей консоли должно быть указано имя виртуальной среды в круглых скобках
PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>
и ваше дерево папок должно выглядеть следующим образом**
.
├── 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
├── pyproject.toml
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
3) pip установит ваш проект в редактируемом состоянии
Установите свой пакет верхнего уровня myproject
с помощью pip
. Хитрость заключается в использовании -e
флага при выполнении установки. Таким образом, он устанавливается в состоянии, доступном для редактирования, и все изменения, внесенные в файлы .py, будут автоматически включены в установленный пакет. Для использования pyproject.toml и флага -e требуется значение pip >= 21.3
В корневом каталоге запустите
pip install -e .
(обратите внимание на точку, она обозначает "текущий каталог")
Вы также можете увидеть, что он установлен с помощью pip freeze
Obtaining file:///home/user/projects/myproject
Installing build dependencies ... done
Checking if build backend supports build_editable ... done
Getting requirements to build editable ... done
Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: myproj
Building editable for myproj (pyproject.toml) ... done
Created wheel for myproj: filename=myproj-0.1.0-py2.py3-none-any.whl size=903 sha256=f19858b080d4e770c2a172b9a73afcad5f33f4c43c86e8eb9bdacbe50a627064
Stored in directory: /tmp/pip-ephem-wheel-cache-qohzx1u0/wheels/55/5f/e4/507fdeb40cdef333e3e0a8c50c740a430b8ce84cbe17ae5875
Successfully built myproject
Installing collected packages: myproject
Successfully installed myproject-0.1.0
(venv) PS C:\tmp\test_imports> pip freeze
myproject==0.1.0
4) Добавьте myproject.
в свой импорт
Обратите внимание, что вам придется добавлять myproject.
только в импорт, который иначе не работал бы. Импорт, который работал без pyproject.toml
& pip install
, все равно будет работать нормально. Смотрите пример ниже.
Протестируйте решение
Теперь давайте протестируем решение, используя api.py
определенное выше и test_one.py
определенное ниже.
test_one.py
from myproject.api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
запуск теста
(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!
* здесь в качестве серверной части сборки используется flit. Существуют и другие альтернативы.
** На самом деле вы можете разместить свою виртуальную среду в любом месте вашего жесткого диска.
Ответ 2
Семь лет спустя
С тех пор, как я написал ответ ниже, модификация sys.path
по-прежнему остается быстрым и грязным приемом, который хорошо работает для частных скриптов, но было внесено несколько улучшений
- Установка пакета (в virtualenv или нет) даст вам то, что вы хотите, хотя я бы предложил использовать для этого pip, а не использовать setuptools напрямую (и использовать
setup.cfg
для хранения метаданных) - Использование
-m
флага и запуск как пакета тоже работает (но будет немного неудобно, если вы захотите преобразовать свой рабочий каталог в устанавливаемый пакет). - Что касается тестов, то, в частности, pytest может найти пакет api в этой ситуации и позаботится о
sys.path
взломах за вас
Так что это действительно зависит от того, что вы хотите сделать. Однако в вашем случае, поскольку кажется, что ваша цель - в какой-то момент создать правильный пакет, установка через pip -e
, вероятно, ваш лучший выбор, даже если он еще не идеален.
Старый ответ
Как уже говорилось в другом месте, ужасная правда заключается в том, что вам приходится выполнять уродливые взломы, чтобы разрешить импорт из родственных модулей или родительского пакета из __main__
модуля. Проблема подробно описана в PEP 366. PEP 3122 попытался обработать импорт более рациональным способом, но Guido отклонил его из-за
Похоже, единственным вариантом использования является запуск скриптов, которые находятся внутри каталога модуля, который я всегда рассматривал как антипаттерн.
(здесь)
Тем не менее, я регулярно использую этот шаблон с
# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__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
Здесь path[0]
находится родительская папка вашего запущенного скрипта и dir(path[0])
ваша папка верхнего уровня.
Я до сих пор не смог использовать относительный импорт с этим, но он допускает абсолютный импорт с верхнего уровня (в родительской папке вашего примера api
).
Ответ 3
Вот еще одна альтернатива, которую я вставляю поверх файлов Python в tests
папке:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Ответ 4
Вам не нужно и не следует взламыватьsys.path
, если только это не необходимо, а в данном случае это не так. Используйте:
import api.api_key # in tests, examples
Запуск из каталога проекта: python -m tests.test_one
.
Вероятно, вам следует переместить tests
(если это unittests api) внутрь api
и запустить python -m api.test
, чтобы выполнить все тесты (при условии, что они есть __main__.py
) или python -m api.test.test_one
запустить test_one
вместо этого.
Вы также можете удалить __init__.py
из examples
(это не пакет Python) и запустить примеры в virtualenv, где api
установлен, например, pip install -e .
в virtualenv будет установлен пакет inplace api
, если у вас есть соответствующий setup.py
.