Импорт пакетов Sibling
Я пробовал прочесть вопросы об импорте родных и даже
пакетная документация, но я еще не нашел ответа.
Со следующей структурой:
├── 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
по-прежнему является быстрым и грязным приемом, который хорошо работает для частных сценариев, но в нем было несколько улучшений.
- Установка пакета (в virtualenv или нет) даст вам то, что вы хотите, хотя я бы посоветовал использовать pip для этого, а не использовать setuptools напрямую (и использовать
setup.cfg
для хранения метаданных) - Использование флага
-m
и запуск в качестве пакета также работает (но будет немного неловко, если вы захотите преобразовать ваш рабочий каталог в устанавливаемый пакет). - В частности, для тестов pytest может найти пакет api в этой ситуации и позаботится о
sys.path
Так что это действительно зависит от того, что вы хотите сделать. В вашем случае, тем не менее, поскольку кажется, что в какой-то момент ваша цель - сделать правильный пакет, установка через pip -e
, вероятно, будет лучшим выбором, даже если он еще не идеален.
Старый ответ
Как уже говорилось в другом месте, ужасная правда заключается в том, что вы должны делать некрасивые хаки, чтобы разрешить импорт из модулей одного уровня или родительского пакета из модуля __main__
. Вопрос подробно описан в PEP 366. PEP 3122 попытался более рационально справиться с импортом, но Гвидо отверг это из-за
Кажется, что единственный вариант использования - запуск сценариев, которые живут внутри каталога модулей, который я всегда рассматривал как антипаттерн.
(здесь)
Хотя я использую этот шаблон на регулярной основе с
# 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
).
Ответ 2
Вот еще одна альтернатива, которую я вставляю поверх файлов Python в папку tests
:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Ответ 3
Устали от взлома sys.path?
Доступно множество sys.path.append
-hacks, но я нашел альтернативный способ решения проблемы: setuptools. Я не уверен, есть ли крайние случаи, которые не работают с этим. Следующее тестируется на Python 3.6.5, (Anaconda, conda 4.5.1), машина с Windows 10.
Настроить
Отправной точкой является предоставленная вами файловая структура, помещенная в папку с именем 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) Создайте файл setup.py в корневом каталоге
Содержимое для setup.py
будет *
from setuptools import setup, find_packages
setup(name='myproject', version='1.0', packages=find_packages())
2) Используйте виртуальную среду
Если вы знакомы с виртуальными средами, активируйте одну и перейдите к следующему шагу. Использование виртуальных сред не является абсолютно обязательным, но они действительно помогут вам в долгосрочной перспективе (когда у вас более 1 проекта..). Самые основные шаги (запустить в корневой папке)
- Создать виртуальную среду
- Активировать виртуальную среду
-
./venv/bin/activate
./venv/bin/activate
(Linux) или ./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
├── setup.py
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
3) pip установите ваш проект в редактируемое состояние
Установите пакет верхнего уровня myproject
с помощью pip
. Хитрость заключается в использовании флага -e
при установке. Таким образом, он устанавливается в редактируемом состоянии, и все изменения, внесенные в файлы .py, будут автоматически включены в установленный пакет.
В корневом каталоге запустите
pip install -e.
(обратите внимание на точку, это означает "текущий каталог")
Вы также можете увидеть, что он установлен с помощью 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
4) Добавить myproject.
в ваш импорт
Обратите внимание, что вам придется добавить myproject.
только в импорт, который не будет работать иначе. Импорт, который работал без установки setup.py
& 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!
* См. Документацию по setuptools для более подробных примеров setup.py.
** На самом деле вы можете поместить свою виртуальную среду в любое место на жестком диске.
Ответ 4
Вы не нуждаетесь и не должны взломать sys.path
, если это не необходимо, и в этом случае это не так. Использование:
import api.api_key # in tests, examples
Запуск из каталога проекта: python -m tests.test_one
.
Вероятно, вы должны переместить tests
(если они являются api unittests) внутри 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
.
Ответ 5
У меня пока нет понимания Pythonology, чтобы увидеть предполагаемый способ совместного использования кода между несвязанными проектами без взлома/относительного взлома. До этого дня это мое решение. Для examples
или tests
для импорта материала из ..\api
это будет выглядеть так:
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
Ответ 6
На всякий случай, если кто-то использует Pydev на Eclipse, вы можете добавить родительский путь sibling (и, следовательно, родительский модуль вызова) в качестве внешней библиотеки, используя Project- > Properties и установив внешние библиотеки в левом меню Pydev- PYTHONPATH. Затем вы можете импортировать из своего родного брата, например. г. from sibling import some_class
.
Ответ 7
Для импорта пакетов siblings вы можете использовать либо метод insert или append модуля [sys.path] [2]:
if __name__ == '__main__' and if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
Это будет работать, если вы запускаете свои скрипты следующим образом:
python examples/example_one.py
python tests/test_one.py
С другой стороны, вы также можете использовать относительный импорт:
if __name__ == '__main__' and if __package__ is not None:
import ..api.api
В этом случае вам придется запустить script с аргументом '- m' (обратите внимание, что в этом случае вы не должен давать расширение ".py" ):
python -m packageName.examples.example_one
python -m packageName.tests.test_one
Конечно, вы можете комбинировать два подхода, чтобы ваш script работал независимо от того, как он называется:
if __name__ == '__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
Ответ 8
TL;DR
Этот метод не требует установки инструментов, хаков путей, дополнительных аргументов командной строки или указания верхнего уровня пакета в каждом отдельном файле вашего проекта.
Просто создайте сценарий в родительском каталоге того, что вы называете своим __main__
и запустите все оттуда. Для дальнейшего объяснения продолжите чтение.
объяснение
Это может быть достигнуто без одновременного взлома нового пути, дополнительных аргументов командной строки или добавления кода в каждую из ваших программ для распознавания ее родных элементов.
Причина, по которой это не удается, как я уже упоминал, упоминалась ранее, заключается в том, что вызываемым программам __name__
как __main__
Когда это происходит, вызываемый скрипт принимает себя на верхнем уровне пакета и отказывается распознавать скрипты в каталогах одного уровня.
Однако все, что находится на верхнем уровне каталога, все равно распознает все остальное на верхнем уровне. Это означает, что ЕДИНСТВЕННАЯ вещь, которую вы должны сделать, чтобы файлы в одноуровневых каталогах распознавали/использовали друг друга, это вызывать их из скрипта в их родительском каталоге.
Доказательство концепции В директории со следующей структурой:
.
|__Main.py
|
|__Siblings
|
|___sib1
| |
| |__call.py
|
|___sib2
|
|__callsib.py
Main.py
содержит следующий код:
import sib1.call as call
def main():
call.Call()
if __name__ == '__main__':
main()
sib1/call.py содержит:
import sib2.callsib as callsib
def Call():
callsib.CallSib()
if __name__ == '__main__':
Call()
и sib2/Callsib.py содержит:
def CallSib():
print("Got Called")
if __name__ == '__main__':
CallSib()
Если вы воспроизведете этот пример, вы заметите, что вызов Main.py
приведет к Main.py
"Got Called", как это определено в sib2/callsib.py
даже если sib2/callsib.py
через sib1/call.py
Однако, если кто-то напрямую вызовет sib1/call.py
(после внесения соответствующих изменений в импорт), он sib1/call.py
исключение. Даже если он работает при вызове сценария в родительском каталоге, он не будет работать, если он считает, что находится на верхнем уровне пакета.
Ответ 9
Вам нужно посмотреть, как инструкции импорта записываются в соответствующем коде. Если examples/example_one.py
использует следующий оператор импорта:
import api.api
... тогда он ожидает, что корневая директория проекта будет в системном пути.
Самый простой способ поддержать это без хаков (как вы выразились) - запустить примеры из каталога верхнего уровня, например:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
Ответ 10
Во-первых, вы должны избегать наличия файлов с тем же именем, что и сам модуль. Он может нарушить другие импортные товары.
При импорте файла сначала интерпретатор проверяет текущий каталог и затем ищет глобальные каталоги.
Внутри examples
или tests
вы можете позвонить:
from ..api import api