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

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

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

├── 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/ сфера.

См. Также: ModuleNotFoundError: нет модуля с именем 'sib1'

Для импорта пакета 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.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

Очки бесов

  1. Не используйте расширение.py после информационного модуля, например python -m Tech.info.py
  2. Введите это, где одноуровневые пакеты находятся на одном уровне.
  3. -m - это флаг, чтобы проверить это, вы можете ввести cmd python --help

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

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

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

from ..api import api
Другие вопросы по тегам