Импорт пакета родного брата
Я пытался прочитать вопросы об импорте родного брата и даже документацию к пакету, но пока не нашел ответа.
Со следующей структурой:
├── 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, верно?
19 ответов
Семь лет спустя
Поскольку я написал ответ ниже, изменив 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
родительская папка).
Устали от взлома sys.path?
Есть много sys.path.append
-Хаки доступны, но я нашел альтернативный способ решения проблемы: 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 проекта..). Самые основные шаги (запустить в корневой папке)
- Создать виртуальную среду
python -m venv venv
- Активировать виртуальную среду
. /venv/bin/activate
(Linux) или./venv/Scripts/activate
(Выиграть)
Чтобы узнать больше об этом, просто посмотрите в 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.
** На самом деле вы можете поместить свою виртуальную среду в любое место на жестком диске.
Вот еще одна альтернатива, которую я вставляю поверх файлов Python в tests
папка:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Вам не нужно и не должно взломать sys.path
если это необходимо, и в этом случае это не так. Использование:
import api.api_key # in tests, examples
Запустите из каталога проекта: python -m tests.test_one
,
Вы, вероятно, должны двигаться tests
(если они являются юнит-тестами 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 будет устанавливать на месте api
пакет, если у вас есть надлежащий setup.py
,
У меня пока нет понимания 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
Для читателей в 2021 году: если вы не уверены в
pip install -e
:
Рассмотрим эту иерархию, как рекомендовано в ответе на Относительный импорт в Python 3:
MyProject
├── src
│ ├── bot
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── sib1.py
│ └── mod
│ ├── __init__.py
│ └── module1.py
└── main.py
Содержание
main.py
, который является отправной точкой, и здесь мы используем абсолютный импорт (без начальных точек):
from src.bot import main
if __name__ == '__main__':
main.magic_tricks()
Содержание
bot/main.py
, который использует явный относительный импорт :
from .sib1 import my_drink # Both are explicit-relative-imports.
from ..mod.module1 import relative_magic
def magic_tricks():
# Using sub-magic
relative_magic(in=["newbie", "pain"], advice="cheer_up")
my_drink()
# Do your work
...
А теперь рассуждение:
- При выполнении
python MyProject/main.py
, добавляется в. - Абсолютный импорт
import src.bot
прочитаю это. - В
from ..mod
часть означает, что он поднимется на один уровень .- Можем ли мы это увидеть? ДА , поскольку
path/to/MyProject
добавляется вsys.path
.
- Можем ли мы это увидеть? ДА , поскольку
Итак, суть в следующем:
Мы должны поместить основной скрипт рядом с
MyProject/src
, поскольку при выполнении относительной ссылки мы не выйдем из
src
, а абсолютный импорт
import src.
предоставляет нам подходящие возможности:
src/
сфера.
Для импорта пакета 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
В этом случае вам нужно будет запустить скрипт с аргументом '-m' (обратите внимание, что в этом случае вы не должны давать расширение '.py'):
python -m packageName.examples.example_one
python -m packageName.tests.test_one
Конечно, вы можете смешать два подхода, чтобы ваш скрипт работал независимо от того, как он называется:
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
TLDR
Этот метод не требует установки инструментов, хаков путей, дополнительных аргументов командной строки или указания верхнего уровня пакета в каждом отдельном файле вашего проекта.
Просто сделайте скрипт в родительском каталоге того, что вы называете своим __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
приведет к печати "Got Called", как определено в sib2/callsib.py
даже если sib2/callsib.py
был вызван через sib1/call.py
, Однако если кто-то должен был позвонить напрямую sib1/call.py
(после внесения соответствующих изменений в импорт) он создает исключение. Даже если он работает при вызове сценария в родительском каталоге, он не будет работать, если он считает, что находится на верхнем уровне пакета.
Вы должны посмотреть, как операторы импорта записаны в связанном коде. Если examples/example_one.py
использует следующий оператор импорта:
import api.api
... тогда он ожидает, что корневой каталог проекта будет находиться в системном пути.
Самый простой способ поддержать это без каких-либо взломов (как вы выразились) - запустить примеры из каталога верхнего уровня, например так:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
Если вы используете pytest, то в документации pytest описывается метод ссылки на исходные пакеты из отдельного тестового пакета.
Предлагаемая структура каталогов проекта:
setup.py
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
Содержание
setup.py
файл:
from setuptools import setup, find_packages
setup(name="PACKAGENAME", packages=find_packages())
Установите пакеты в редактируемом режиме:
pip install -e .
Статья в pytest ссылается на это сообщение в блоге Ионела Кристиана Мэриэша .
Я сделал образец проекта, чтобы продемонстрировать, как я справился с этим, что действительно является еще одним взломом sys.path, как указано выше. Пример импорта Sibling Python, который основан на:
if __name__ == '__main__':
import os
import sys
sys.path.append(os.getcwd())
Это кажется довольно эффективным, если ваш рабочий каталог остается в корне проекта Python. Если кто-нибудь развернет это в реальной производственной среде, было бы здорово услышать, работает ли он и там.
Я хотел прокомментировать решение, предоставленное np8, но у меня недостаточно репутации, поэтому я просто упомяну, что вы можете создать файл setup.py точно так, как они предложили, а затем выполните
pipenv install --dev -e .
из корневого каталога проекта, чтобы превратить его в редактируемую зависимость. Тогда ваш абсолютный импорт будет работать, например
from api.api import foo
и вам не нужно возиться с общесистемными установками.
На тот случай, если кто-то, использующий Pydev в Eclipse, окажется здесь: вы можете добавить родительский путь брата (и, следовательно, родителя вызывающего модуля) в качестве папки внешней библиотеки, используя Project->Properties и установив External Libraries в левом меню Pydev-PYTHONPATH. Затем вы можете импортировать из вашего брата, например, from sibling import some_class
,
в свой основной файл добавьте это:
import sys
import os
sys.path.append(os.path.abspath(os.path.join(__file__,mainScriptDepth)))
mainScriptDepth = глубина основного файла от корня проекта.
Здесь в твоем случае
mainScriptDepth = "../../"
.
Не хотите редактировать sys.path вручную и вам не нравятся редактируемые установки?
pip install importmonkey
[github]
[пип]
Пример структуры:
Вы хотите импортировать что-то из родительского/родственного пути, но это не удается с одним из них:ModuleNotFoundError
илиImportError
илиSystemError
├─ src
│ └─ project
│ ├─ __init__.py
│ └─ module.py
└─ test
└─ test.py
Решение:
# In test.py
# You can add as many paths as needed, absolute or relative, in any file.
# Relative paths start from the current __file__ directory.
# Normal unix path conventions work so you can use '..' and '.' and so on.
# The paths you try to add are checked for validity etc. help(add_path) for details.
from importmonkey import add_path
add_path("../src")
import project
Раскрытие принадлежности: Я сделал importmonkey.
Проблема:
Вы просто не можете работать в . Вам понадобится либоeditable install
, изменить на или изменить на__name__
иpath
demo
├── dev
│ └── test.py
└── src
└── mypackage
├── __init__.py
└── module_of_mypackage.py
--------------------------------------------------------------
ValueError: attempted relative import beyond top-level package
Решение:
import sys; sys.path += [sys.path[0][:-3]+"src"]
Поместите вышеуказанное перед попыткой импорта в . Вот и все. Ты можешь сейчасimport mypackage
.
Это будет работать как в Windows, так и в Linux. Ему также все равно, по какому пути вы запускаете свой скрипт. Он достаточно короткий, чтобы шлепнуть его в любом месте, где он вам может понадобиться.
Почему это работает:
Содержит места по порядку, где искать пакеты при попытке импорта, если они не найдены в установленных пакетах сайта. Когда вы бежитеtest.py
первый пункт вsys.path
будет что-то вроде/mnt/c/Users/username/Desktop/demo/dev
то есть: где вы запустили свой файл. Oneliner просто добавит родственную папку в путь, и все заработает. Вам не придется беспокоиться о путях к файлам Windows и Linux, поскольку мы редактируем только последнее имя папки и ничего больше. Если структура вашего проекта уже определена для вашего репозитория, мы также можем разумно просто использовать магический номер3
отрезатьdev
и заменитьsrc
По главному вопросу:
вызовите родственную папку как модуль:
from.. import siblingfolder
вызвать a_file.py из родственной папки как модуль:
from..siblingfolder импортировать a_file
вызвать a_function внутри файла в одноуровневой папке как модуль:
from..siblingmodule.a_file import func_name_exists_in_a_file
Самый простой способ.
перейдите в папку lib/site-packages.
если существует файл easy_install.pth, просто отредактируйте его и добавьте свой каталог, в котором у вас есть скрипт, который вы хотите сделать как модуль.
если не существует, просто сделайте его одним... и поместите туда свою папку, которую хотите
после того, как вы добавите его..., python будет автоматически воспринимать эту папку как похожую на пакеты сайтов, и вы можете вызывать каждый скрипт из этой папки или подпапки как модуль.
Я написал это на свой телефон, и трудно настроить его так, чтобы всем было удобно читать.
- Проект
1.1 Пользователь
1.1.1 about.py
1.1.2 init.py
1.2 Технология
1.2.1 info.py
1.1.2 init.py
Теперь, если вы хотите получить доступ к модулю about.py в пакете User из модуля info.py в пакете Tech, вам нужно перенести путь cmd (в Windows) к Project, т.е. **C:\Users\Personal\Desktop\Project>** согласно приведенному выше примеру пакета. И из этого пути вы должны ввести, python -m имя_пакета.имя_модуля. Например, для указанного выше пакета мы должны сделать,
C:\Users\Personal\Desktop\Project> python -m Tech.info
Очки бесов
- Не используйте расширение.py после информационного модуля, например python -m Tech.info.py
- Введите это, где одноуровневые пакеты находятся на одном уровне.
- -m - это флаг, чтобы проверить это, вы можете ввести cmd python --help
Во-первых, вы должны избегать файлов с тем же именем, что и сам модуль. Это может сломать другой импорт.
Когда вы импортируете файл, сначала интерпретатор проверяет текущий каталог, а затем ищет глобальные каталоги.
внутри examples
или же tests
ты можешь позвонить:
from ..api import api