Import pakietów rodzeństwa

199

Próbowałem przeczytać pytania dotyczące importu rodzeństwa, a nawet dokumentacji paczki , ale jeszcze nie znalazłem odpowiedzi.

O następującej strukturze:

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

Jak można skrypty w examplesi testskatalogi importować z apimodułem i być uruchamiane z linii poleceń?

Ponadto chciałbym uniknąć brzydkiego sys.path.insertwłamania do każdego pliku. Z pewnością można to zrobić w Pythonie, prawda?

zachwill
źródło
7
Polecam pominąć wszystkie sys.pathhacki i przeczytać jedyne aktualne rozwiązanie, które zostało opublikowane (po 7 latach!).
Aran-Fey,
1
Nawiasem mówiąc, jest jeszcze miejsce na inne dobre rozwiązanie: oddzielenie kodu wykonywalnego od kodu biblioteki; przez większość czasu skrypt wewnątrz pakietu nie powinien być wykonywalny na początek.
Aran-Fey,
Jest to bardzo pomocne, zarówno pytanie, jak i odpowiedzi. Jestem tylko ciekawy, dlaczego „Odpowiedź zaakceptowana” nie jest tym samym, co ten, który przyznał nagrodę w tym przypadku?
Indominus
@ Aran-Fey To niedoceniane przypomnienie w tych względnych błędach importu Pytania i odpowiedzi. Cały czas szukałem hacka, ale głęboko wiedziałem, że istnieje prosty sposób na rozwiązanie problemu. Nie wspominając o tym, że jest to rozwiązanie dla wszystkich, którzy tu czytają, ale jest to dobre przypomnienie dla wielu osób.
colorlace

Odpowiedzi:

70

Siedem lat później

Ponieważ napisałem odpowiedź poniżej, modyfikowanie sys.pathnadal jest szybką i brudną sztuczką, która działa dobrze w przypadku prywatnych skryptów, ale wprowadzono kilka ulepszeń

  • Zainstalowanie pakietu (w wersji wirtualnej lub nie) da ci to, czego chcesz, ale sugerowałbym użycie pip do tego, a nie bezpośrednie użycie setuptools (i setup.cfgprzechowywanie metadanych)
  • Używanie -mflagi i działania jako pakietu również działa (ale okaże się nieco niewygodne, jeśli chcesz przekonwertować katalog roboczy na pakiet instalowalny).
  • W przypadku badań, w szczególności, pytest jest w stanie znaleźć pakiet API w tej sytuacji i dba o sys.pathhacki dla Ciebie

To naprawdę zależy od tego, co chcesz zrobić. Jednak w twoim przypadku, ponieważ wydaje się, że Twoim celem jest przygotowanie odpowiedniego pakietu w pewnym momencie, instalacja za pomocą pip -ejest prawdopodobnie najlepszym rozwiązaniem, nawet jeśli nie jest jeszcze idealna.

Stara odpowiedź

Jak już powiedziano gdzie indziej, okropną prawdą jest to, że musisz robić brzydkie hacki, aby umożliwić import z modułów rodzeństwa lub pakietu rodziców z __main__modułu. Problem jest szczegółowo opisany w PEP 366 . PEP 3122 próbował zająć się importem w bardziej racjonalny sposób, ale Guido odrzucił go na konto

Wydaje się, że jedynym przypadkiem użycia są uruchomione skrypty, które znajdują się w katalogu modułu, co zawsze uważałem za antipattern.

( tutaj )

Chociaż używam tego wzoru regularnie

# 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

Oto path[0]folder nadrzędny działającego skryptu i folder dir(path[0])najwyższego poziomu.

Nadal nie byłem jednak w stanie używać do tego importu względnego, ale pozwala on na import absolutny z najwyższego poziomu (w apifolderze nadrzędnym twojego przykładu ).

Evpok
źródło
3
nie musisz tego robić , jeśli uruchamiasz z katalogu projektu przy użyciu-m formularza lub instalujesz pakiet (pip i virtualenv ułatwiają)
jfs
2
Jak Pytest znajduje dla ciebie pakiet API? Zabawnie znalazłem ten wątek, ponieważ mam problem z importowaniem pakietów pytest i sibling.
JuniorIncanter,
1
Proszę o dwa pytania. 1. Twój wzór wydaje mi się bezskuteczny __package__ = "examples". Dlaczego go używasz? 2. W jakiej sytuacji jest, __name__ == "__main__"ale __package__nie jest None?
actual_panda
@actual_panda Ustawienie __packages__pomaga, jeśli chcesz bezwzględną ścieżkę, na przykład examples.apido pracy w trybie iirc (ale minęło już dużo czasu, odkąd to ostatnio zrobiłem), a sprawdzenie, czy pakiet nie jest żaden, było w większości przypadków niezawodne w przypadku dziwnych sytuacji i zabezpieczenia na przyszłość.
Evpok
Boże, gdyby tylko inne języki uczyniłyby ten sam proces tak łatwym, jak w Pythonie. Rozumiem, dlaczego wszyscy kochają ten język. Btw, dokumentacja jest również doskonała. Uwielbiam wyodrębniać typy zwrotów z tekstu nieustrukturyzowanego, jest to miła odmiana od Javadoc i phpdoc. ffs ....
mat
167

Masz dość hacków sys.path?

Dostępnych jest wiele sys.path.appendhacków, ale znalazłem alternatywny sposób rozwiązania problemu.

Podsumowanie

  • Zawiń kod w jeden folder (np. packaged_stuff)
  • Użyj opcji Utwórz setup.py skryptu którym używasz setuptools.setup () .
  • Pip zainstaluj pakiet w stanie edytowalnym za pomocą pip install -e <myproject_folder>
  • Importuj za pomocą from packaged_stuff.modulename import function_name

Ustawiać

Punktem wyjścia jest podana przez Ciebie struktura plików, zawinięta w folder o nazwie 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

Zadzwonię do . folderu korzenia, aw moim przykładowym przypadku to znajduje się C:\tmp\test_imports\.

api.py

Jako przypadek testowy użyjmy następującego pliku ./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()

Spróbuj uruchomić 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'

Próbowanie importu względnego również nie zadziała:

Używanie from ..api.api import function_from_apispowoduje

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

Kroki

  1. Utwórz plik setup.py w katalogu głównym

Zawartość setup.pybędzie *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Użyj środowiska wirtualnego

Jeśli znasz środowiska wirtualne, aktywuj jedno i przejdź do następnego kroku. Korzystanie ze środowisk wirtualnych nie jest absolutnie wymagane, ale naprawdę pomoże ci w dłuższej perspektywie (jeśli masz więcej niż jeden projekt w toku ..). Najbardziej podstawowe kroki to (uruchom w folderze głównym)

  • Utwórz wirtualne środowisko
    • python -m venv venv
  • Aktywuj środowisko wirtualne
    • source ./venv/bin/activate(Linux, macOS) lub ./venv/Scripts/activate(Win)

Aby dowiedzieć się więcej na ten temat, po prostu wypróbuj Google „samouczek wirtualnego środowiska python” lub podobny. Prawdopodobnie nigdy nie potrzebujesz żadnych innych poleceń niż tworzenie, aktywowanie i dezaktywowanie.

Po utworzeniu i aktywacji środowiska wirtualnego konsola powinna podać nazwę środowiska wirtualnego w nawiasie

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

a twoje drzewo folderów powinno wyglądać tak **

.
├── 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 zainstaluj swój projekt w stanie edytowalnym

Zainstaluj pakiet najwyższego poziomu myprojectza pomocą pip. Sztuką jest użycie -eflagi podczas instalacji. W ten sposób jest on instalowany w stanie edytowalnym, a wszystkie zmiany dokonane w plikach .py zostaną automatycznie uwzględnione w zainstalowanym pakiecie.

W katalogu głównym uruchom

pip install -e . (zwróć uwagę na kropkę, oznacza „bieżący katalog”)

Możesz również zobaczyć, że jest on instalowany przy użyciu 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. Dodaj myproject.do importu

Pamiętaj, że będziesz musiał dodać myproject.tylko do importu, który inaczej nie działałby. Importowanie, które działało bez setup.py& pip installnadal będzie działać. Zobacz przykład poniżej.


Przetestuj rozwiązanie

Teraz przetestujmy rozwiązanie przy użyciu api.pyzdefiniowanych powyżej i test_one.pyzdefiniowanych poniżej.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

uruchomienie testu

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

* Zobacz pełne dokumenty setuptools, aby uzyskać bardziej szczegółowe przykłady setup.py.

** W rzeczywistości środowisko wirtualne można umieścić w dowolnym miejscu na dysku twardym.

np8
źródło
13
Dzięki za szczegółowy post. Oto mój problem. Jeśli zrobię wszystko, co powiedziałeś, i zamrożę pip, dostanę komunikat -e git+https://[email protected]/folder/myproject.git@f65466656XXXXX#egg=myprojectJakiś pomysł na rozwiązanie?
Si Pon
2
Dlaczego rozwiązanie importu względnego nie działa? Wierzę ci, ale staram się zrozumieć skomplikowany system Pythona.
Jared Nielsen
8
Czy ktoś ma problemy w odniesieniu do ModuleNotFoundError? Zainstalowałem „myproject” w virtualenv, wykonując następujące kroki, a kiedy wchodzę w interpretowaną sesję i uruchamiam import myproject, otrzymuję ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectpokazuje, że tam jest, katalog jest poprawny, a zarówno weryfikacja , jak pipi pythonweryfikacja poprawności.
ThoseKind
2
Hej @ np8, działa, przypadkowo zainstalowałem go w venv iw os :) pip listpokazuje pakiety, a pip freezepokazuje dziwne nazwy, jeśli jest zainstalowany z flagą -e
Grzegorz Krug
3
Spędziłem około 2 godzin, próbując dowiedzieć się, jak sprawić, by import względny zadziałał, a ta odpowiedź była tą, która w końcu zrobiła coś sensownego. 👍👍
Graham Lea
43

Oto kolejna alternatywa, którą wstawiam u góry plików Pythona w testsfolderze:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Cenk Alti
źródło
1
+1 naprawdę proste i działało idealnie. Musisz dodać klasę nadrzędną do importu (np. Api.api, Examples.example_two), ale wolę to w ten sposób.
Evan Plaice
10
Myślę, że warto wspomnieć początkującym (jak ja), że ..tutaj jest względna do katalogu, z którego wykonujesz --- nie do katalogu zawierającego ten test / przykładowy plik. Wykonuję z katalogu projektu i ./zamiast tego potrzebowałem . Mam nadzieję, że to pomaga komuś innemu.
Joshua Detwiler
@JoshDetwiler, tak absolutnie. Nie byłam tego świadoma. Dzięki.
doak
1
To kiepska odpowiedź. Hakowanie ścieżki nie jest dobrą praktyką; skandaliczne jest to, ile jest używane w świecie python. Jednym z głównych punktów tego pytania było sprawdzenie, jak można dokonać importu, unikając tego rodzaju włamań.
jtcotton63
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc
31

Nie potrzebujesz i nie powinieneś hakować, sys.pathchyba że jest to konieczne, aw tym przypadku nie jest to konieczne. Posługiwać się:

import api.api_key # in tests, examples

Uruchom z katalogu projektu: python -m tests.test_one .

Prawdopodobnie powinieneś przenieść się tests(jeśli są to najbardziej nieprzystosowane interfejsy API) do wewnątrz apii uruchomić, python -m api.testaby uruchomić wszystkie testy (zakładając, że są __main__.py) lub python -m api.test.test_oneuruchomićtest_one zamiast nich.

Możesz także usunąć __init__.pyz examples(nie jest to pakiet Python) i uruchomić przykłady w virtualenv, gdzie apijest zainstalowany, np. pip install -e .W virtualenv zainstalowałby apipakiet inplace , jeśli masz odpowiednie setup.py.

jfs
źródło
@Alex odpowiedź nie zakłada, że ​​testy są testami API, z wyjątkiem akapitu, w którym wyraźnie powiedziano „jeśli są to niepoprawne interfejsy API” .
jfs
niestety wtedy utknąłeś przy uruchamianiu z katalogu głównego, a PyCharm nadal nie znajduje pliku dla swoich fajnych funkcji
mhstnsc
@mhstnsc: to nie jest poprawne. Powinieneś być w stanie uruchomić python -m api.test.test_onez dowolnego miejsca, gdy virtualenv jest aktywowany. Jeśli nie możesz skonfigurować PyCharm do uruchamiania testów, spróbuj zadać nowe pytanie o przepełnienie stosu (jeśli nie możesz znaleźć istniejącego pytania na ten temat).
jfs
@ jfs Brakowało mi wirtualnej ścieżki env, ale nie chcę używać niczego więcej niż linii shebang do uruchamiania tych rzeczy z dowolnego katalogu. Nie chodzi o współpracę z PyCharm. Deweloperzy z PyCharm wiedzieliby również, że mają kompletność i przeskakują przez funkcje, których nie mogłem zmusić do działania z żadnym rozwiązaniem.
mhstnsc
@mhstnsc w wielu przypadkach wystarczy odpowiedni shebang (wskaż go na plik binarny Python virtualenv. Każde przyzwoite IDE w Pythonie powinno obsługiwać virtualenv.
jfs
9

Nie rozumiem jeszcze Pythonologii niezbędnej do zobaczenia zamierzonego sposobu udostępniania kodu między niepowiązanymi projektami bez rodzeństwa / względnego importu. Do tego dnia jest to moje rozwiązanie. W przypadku exampleslub testsdo importowania rzeczy ..\apiwyglądałoby to tak:

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
użytkownik1330131
źródło
To wciąż dawałoby katalog nadrzędny interfejsu API i nie potrzebowałbyś konkatenacji „/ ..” sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( plik ))) )
Camilo Sanchez
4

W przypadku importu pakietu rodzeństwa możesz użyć metody insert lub append modułu [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

Działa to, jeśli uruchamiasz skrypty w następujący sposób:

python examples/example_one.py
python tests/test_one.py

Z drugiej strony możesz także użyć importu względnego:

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api

W takim przypadku będziesz musiał uruchomić skrypt z argumentem „-m” (zwróć uwagę, że w tym przypadku nie możesz podać rozszerzenia „.py” ):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

Oczywiście możesz łączyć oba podejścia, aby skrypt działał bez względu na to, jak się nazywa:

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
Paolo Rovelli
źródło
Korzystałem z frameworka Click, który nie ma __file__globalnego, więc musiałem zastosować następujące: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Ale teraz działa w dowolnym katalogu
GammaGames
3

TLDR

Ta metoda nie wymaga narzędzi konfiguracyjnych, hacków ścieżek, dodatkowych argumentów wiersza poleceń ani określania najwyższego poziomu pakietu w każdym pliku projektu.

Po prostu stwórz skrypt w katalogu nadrzędnym tego, do czego wołasz __main__ i uruchom wszystko od tego miejsca. Aby uzyskać dalsze wyjaśnienia, czytaj dalej.

Wyjaśnienie

Można to osiągnąć bez zhakowania razem nowej ścieżki, dodatkowych argumentów wiersza poleceń lub dodania kodu do każdego z programów w celu rozpoznania jego rodzeństwa.

Przyczyną niepowodzenia tego, jak sądzę, wspomniano wcześniej, jest to, że wywoływane programy mają __name__ustawiony na__main__ . Gdy tak się dzieje, wywoływany skrypt akceptuje się na najwyższym poziomie pakietu i odmawia rozpoznania skryptów w katalogach rodzeństwa.

Jednak wszystko poniżej najwyższego poziomu katalogu nadal rozpoznaje WSZYSTKIE POZOSTAŁE pod najwyższym poziomem. Oznacza to, że jedyną rzeczą, którą musisz zrobić, aby pliki w katalogach rodzeństwa były rozpoznawane / wykorzystywane, jest wywoływanie ich ze skryptu w katalogu nadrzędnym.

Dowód koncepcji W reż. O następującej strukturze:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py zawiera następujący kod:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py zawiera:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

i sib2 / Callsib.py zawiera:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

Jeśli odtworzysz ten przykład, zauważysz, że wywołanie Main.pyspowoduje wydrukowanie napisu „Got Called” zgodnie z definicją, sib2/callsib.py nawet jeśli zostaniesz sib2/callsib.pywywołany sib1/call.py. Jeśli jednak ktoś zadzwoni bezpośrednio sib1/call.py(po wprowadzeniu odpowiednich zmian w imporcie), zgłasza wyjątek. Mimo że zadziałał, gdy został wywołany przez skrypt w katalogu nadrzędnym, nie będzie działał, jeśli uważa, że ​​jest na najwyższym poziomie pakietu.

Thunderwood
źródło
2

Zrobiłem przykładowy projekt, aby zademonstrować, jak sobie z tym poradziłem, co jest rzeczywiście kolejnym hackem sys.path, jak wskazano powyżej. Przykład importu Python Sibling , który opiera się na:

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

Wydaje się to dość skuteczne, o ile katalog roboczy pozostaje w katalogu głównym projektu Python. Jeśli ktoś wdroży to w prawdziwym środowisku produkcyjnym, dobrze byłoby usłyszeć, czy tam również działa.

ADataGMan
źródło
Działa to tylko, jeśli korzystasz z katalogu nadrzędnego skryptu
Evpok
1

Musisz sprawdzić, jak zapisywane są instrukcje importu w powiązanym kodzie. Jeśli examples/example_one.pyużywa następującej instrukcji importu:

import api.api

... oczekuje, że katalog główny projektu znajdzie się w ścieżce systemowej.

Najłatwiejszym sposobem obsługi tego bez żadnych włamań (jak to ująłeś) byłoby uruchomienie przykładów z katalogu najwyższego poziomu, jak poniżej:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
AJ
źródło
Z Python 2.7.1 otrzymuję następujący: $ 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. Ja też to samo rozumiem import api.api.
zachwill
Zaktualizowany moją odpowiedź ... ty nie masz, aby dodać bieżący katalog na ścieżkę importu, który nie odwrotnie.
AJ.
1

Na wypadek, gdyby ktoś korzystający z Pydev na Eclipse skończył tutaj: możesz dodać ścieżkę rodzica rodzeństwa (a tym samym rodzica modułu wywołującego) jako folder biblioteki zewnętrznej, korzystając z Project-> Właściwości i ustawiając Biblioteki zewnętrzne w lewym menu Pydev-PYTHONPATH . Następnie możesz zaimportować z rodzeństwa, np from sibling import some_class.

Lord Henry Wotton
źródło
-3

Po pierwsze, należy unikać plików o tej samej nazwie co sam moduł. Może zniszczyć inny import.

Podczas importowania pliku interpreter najpierw sprawdza bieżący katalog, a następnie przeszukuje katalogi globalne.

Wewnątrz exampleslub testsmożesz zadzwonić:

from ..api import api

źródło
W Pythonie 2.7.1 otrzymuję następujące informacje: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
Och, powinieneś dodać __init__.pyplik do katalogu najwyższego poziomu. W przeciwnym razie Python nie będzie mógł potraktować tego jako modułu
8
To nie zadziała. Problemem nie jest to, że folder nadrzędny nie jest pakiet, to jest to, że od modułu __name__jest __main__zamiast package.module, Python nie widzi swoją paczkę rodzica, więc .punkty do niczego.
Evpok