Импорт пакета родного брата - PullRequest
124 голосов
/ 12 июня 2011

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

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

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

Ответы [ 10 ]

76 голосов
/ 05 мая 2018

Устали от взлома 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

Steps

1) Создатьфайл setup.py в корневой каталог

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

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())

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

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

49 голосов
/ 24 июня 2011

Через семь лет после

С тех пор, как я написал ответ ниже, изменение 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).

38 голосов
/ 25 февраля 2012

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

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
25 голосов
/ 08 мая 2014

Вам не нужно и не следует взламывать 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.

8 голосов
/ 13 апреля 2012

У меня пока нет понимания 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
3 голосов
/ 10 января 2015

Для импорта пакета одного уровня вы можете использовать метод 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
2 голосов
/ 06 ноября 2018

TLDR

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

Просто сделайте скрипт в родительском каталоге того, что вы называете своим __main__, и запустите все оттуда.Для дальнейшего объяснения продолжите чтение.

Объяснение

Это может быть выполнено без совместного взлома нового пути, дополнительных аргументов командной строки или добавления кода для каждой из ваших программ враспознать его братьев и сестер.

Причина, по которой это не удается, как я уже упоминал, упоминалась ранее, заключается в том, что вызываемым программам присваивается значение __name__, равное __main__.Когда это происходит, вызываемый сценарий принимает себя на верхнем уровне пакета и отказывается распознавать сценарии в каталогах одного уровня.

Однако все, находящееся на верхнем уровне каталога, все равно распознает НИЧЕГОELSE под верхним уровнем.Это означает, что ТОЛЬКО , что вам нужно сделать, чтобы файлы в одноуровневых каталогах распознавали / использовали друг друга, - это вызывать их из сценария в родительском каталоге.

ПодтверждениеКонцепция В директории со следующей структурой:

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

1 голос
/ 07 мая 2014

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

1 голос
/ 12 июня 2011

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

import api.api

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

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

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
0 голосов
/ 12 июня 2011

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

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

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

from ..api import api
...