Импорт пакета родного брата

200

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

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

├── 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, верно?

zachwill
источник
7
Я рекомендую пропустить все sys.pathхаки и прочитать единственное актуальное решение, которое было опубликовано до сих пор (через 7 лет!).
Аран-Фей
1
Кстати, есть еще место для другого хорошего решения: отделение исполняемого кода от кода библиотеки; Большую часть времени сценарий внутри пакета не должен быть исполняемым для начала.
Аран-Фей
Это так полезно, как вопрос, так и ответы. Мне просто любопытно, почему «Принятый ответ» не совпадает с тем, который в этом случае получил награду?
Indominus
@ Aran-Fey Это недооцененное напоминание в этих относительных ошибках импорта. Все это время я искал взлом, но в глубине души я знал, что есть простой способ придумать выход из проблемы. Не сказать, что это решение для всех, кто здесь читает, но это хорошее напоминание, как это может быть для многих.
colorlace

Ответы:

70

Семь лет спустя

С тех пор как я написал ответ ниже, модификация 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родительской папке вашего примера ).

Evpok
источник
3
вам не нужно это делать, если вы запускаете из каталога проекта с помощью -mформы или устанавливаете пакет (pip и virtualenv облегчают это)
jfs
2
Как pytest находит пакет api для вас? Забавно, но я нашел эту ветку, потому что столкнулся с этой проблемой, особенно с импортом пакетов pytest и sibling.
JuniorIncanter
1
У меня есть два вопроса, пожалуйста. 1. Ваша модель, кажется, работает без __package__ = "examples"меня. Почему вы используете это? 2. В какой ситуации есть, __name__ == "__main__"но __package__нет None?
актуальная_панда
@actual_panda Настройка __packages__помогает, если вам нужен абсолютный путь, например, examples.apiдля работы iirc (но это было давно с тех пор, как я это делал в последний раз), и проверка того, что пакет не является None, была в основном отказоустойчивой для странных ситуаций и защиты будущего.
Евпок
Черт возьми, если бы только другие языки сделали такой же процесс простым, как в Python. Я понимаю, почему все любят этот язык. Кстати, документация тоже отлично. Я люблю извлекать типы возврата из неструктурированного текста, это отличное изменение от Javadoc и phpdoc. FFS ....
Мэтт
168

Устали от взлома sys.path?

Доступно множество sys.path.appendхаков, но я нашел альтернативный способ решения проблемы.

Резюме

  • Оберните код в одну папку (например packaged_stuff)
  • Используйте setup.pyсценарий создания, где вы используете setuptools.setup () .
  • 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. Создайте файл setup.py в корневой каталог

Содержание для setup.pyбудет *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Используйте виртуальную среду

Если вы знакомы с виртуальными средами, активируйте одну и перейдите к следующему шагу. Использование виртуальных сред не является абсолютно обязательным, но они действительно помогут вам в долгосрочной перспективе (когда у вас более 1 проекта ..). Самые основные шаги (запустить в корневой папке)

  • Создать виртуальную среду
    • python -m venv venv
  • Активировать виртуальную среду
    • 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
├── setup.py
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. 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
  1. Добавьте 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.

** На самом деле вы можете поместить свою виртуальную среду в любое место на жестком диске.

NP8
источник
13
Спасибо за подробный пост. Здесь моя проблема. Если я сделаю все, что вы сказали, и я сделаю стоп-кадр, я получу строку -e git+https://username@bitbucket.org/folder/myproject.git@f65466656XXXXX#egg=myprojectЛюбая идея о том, как решить?
Si Mon
2
Почему не работает решение относительного импорта? Я верю вам, но я пытаюсь понять запутанную систему Python.
Джаред Нильсен
8
У кого-нибудь есть проблемы в отношении ModuleNotFoundError? Я установил «myproject» в virtualenv, выполнив следующие действия, и когда я вхожу в интерпретируемый сеанс и запускаю, import myprojectя получаю ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectпоказывает , что она есть, каталог является правильным, и оба verison из pipи pythonпроверяется , чтобы быть правильными.
Те добр
2
Привет @ np8, это работает, я случайно установил его в venv и в os :) pip listпоказывает пакеты, в то время как pip freezeпоказывает странные имена, если установлен с флагом -e
Grzegorz Krug
3
Потратил около 2 часов, пытаясь выяснить, как заставить работать относительный импорт, и этот ответ, наконец, действительно сделал что-то разумное. Gra
Грэхэм Ли
43

Вот еще одна альтернатива, которую я вставляю поверх файлов Python в testsпапке:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Ченк Алти
источник
1
+1 действительно просто и отлично сработало. Вам нужно добавить родительский класс в импорт (например, api.api, examples.example_two), но я предпочитаю его таким образом.
Эван Плейс
10
Я думаю, что новичкам (как и мне) стоит упомянуть, что ..здесь речь идет о каталоге, из которого вы выполняете, а не о каталоге, содержащем этот файл test / example. Я выполняю из каталога проекта, и мне нужно было ./вместо этого. Надеюсь, это поможет кому-то еще.
Джошуа Детвилер
@JoshDetwiler, да, абсолютно. Я не знал об этом. Спасибо.
Доак
1
Это плохой ответ. Взломать путь не является хорошей практикой; это скандально, сколько это используется в мире питонов. Одним из основных пунктов этого вопроса было посмотреть, как можно сделать импорт, избегая такого рода взлома.
jtcotton63
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc
31

Вам не нужно и не следует взламывать, 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пакет inplace , если у вас все правильно setup.py.

JFS
источник
@ Ответ не предполагает, что тесты являются API-тестами, за исключением параграфа, в котором явно сказано, «если они являются тестами API» .
JFS
к сожалению, тогда вы застряли с запуском из корневого каталога, а PyCharm все еще не находит файл с его хорошими функциями
mhstnsc
@mhstnsc: это не правильно. Вы должны быть в состоянии бежать python -m api.test.test_oneиз любого места, когда virtualenv активирован. Если вы не можете настроить PyCharm для запуска своих тестов, попробуйте задать новый вопрос переполнения стека (если вы не можете найти существующий вопрос по этой теме).
JFS
@jfs Я пропустил виртуальный путь env, но я не хочу использовать ничего, кроме строки shebang, для запуска этого материала из любого каталога. Это не о работе с PyCharm. Разработчики с PyCharm знали бы также, что они имеют завершение и переходят через функции, которые я не мог заставить работать с любым решением.
Mhstnsc
@mhstnsc во многих случаях достаточно подходящего шебанга (укажите на двоичный файл Python virtualenv. Любая достойная среда разработки Python должна поддерживать virtualenv.
jfs
9

У меня пока нет понимания 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
user1330131
источник
Это все равно даст вам родительский каталог api, и вам не понадобится конкатенация "/ .." sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( file ))) )
Камило Санчес
4

Для импорта братья пакет, вы можете использовать либо вкладыш или на добавление метод [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
Паоло Ровелли
источник
Я использовал платформу Click, у которой нет __file__глобальной системы, поэтому мне пришлось использовать следующее: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Но сейчас она работает в любом каталоге
GammaGames,
3

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(после внесения соответствующих изменений в импорт), он генерирует исключение. Даже если он работает при вызове сценария в родительском каталоге, он не будет работать, если он считает, что находится на верхнем уровне пакета.

Тандервуд
источник
2

Я сделал пример проекта, чтобы продемонстрировать, как я справился с этим, что действительно является еще одним взломом sys.path, как указано выше. Пример импорта Python Sibling , который опирается на:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

Это кажется довольно эффективным, пока ваш рабочий каталог остается в корне проекта Python. Если кто-нибудь развернет это в реальной производственной среде, было бы здорово услышать, работает ли он там также.

ADataGMan
источник
Это работает, только если вы работаете из родительского каталога
скрипта
1

Вы должны посмотреть, как операторы импорта записаны в связанном коде. Если examples/example_one.pyиспользуется следующий оператор импорта:

import api.api

... тогда он ожидает, что корневой каталог проекта находится в системном пути.

Самый простой способ поддержать это без каких-либо взломов (как вы выразились) - запустить примеры из каталога верхнего уровня, например так:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
AJ.
источник
В Python 2.7.1 я получаю следующее: $ python examples/example.py Traceback (most recent call last): File "examples/example.py", line 3, in <module> from api.api import API ImportError: No module named api.api. Я также получаю то же самое с import api.api.
zachwill
Обновлен мой ответ ... Вы действительно должны добавить текущую директорию на пути импорта, никоим образом вокруг этого.
AJ.
1

На тот случай, если кто-то, использующий Pydev в Eclipse, окажется здесь: вы можете добавить родительский путь брата (и, следовательно, родителя вызывающего модуля) в качестве папки внешней библиотеки, используя Project-> Properties и установив External Libraries в левом меню Pydev-PYTHONPATH . Затем вы можете импортировать из вашего брата, например from sibling import some_class.

Лорд Генри Уоттон
источник
-3

Во-первых, вы должны избегать файлов с тем же именем, что и сам модуль. Это может сломать другой импорт.

Когда вы импортируете файл, сначала интерпретатор проверяет текущий каталог, а затем ищет глобальные каталоги.

Внутри examplesили testsвы можете позвонить:

from ..api import api

источник
Я получаю следующее с Python 2.7.1:Traceback (most recent call last): File "example_one.py", line 3, in <module> from ..api import api ValueError: Attempted relative import in non-package
zachwill
2
О, тогда вы должны добавить __init__.pyфайл в каталог верхнего уровня. В противном случае Python не может рассматривать его как модуль
8
Это не сработает. Проблема заключается не в том , что родительская папка не является пакет, то , что с модуля __name__является __main__вместо package.module, Python не может видеть его родительский пакет, так что .указывает на ничто.
Евпок