Вопрос-Ответ

Sibling package imports

Импорт родственных пакетов

Я пытался прочитать вопросы об импорте родственных пакетов и даже документацию по пакету, но пока не нашел ответа.

Со следующей структурой:

├── 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.

2024-01-03 01:23 python