Подтвердить что ты не робот

Импорт пакетов 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, правильно?

4b9b3361

Ответ 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 проекта..). Самые основные шаги (запустить в корневой папке)

  • Создать виртуальную среду
    • python -m venv venv
  • Активировать виртуальную среду
    • ./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