Importazioni di pacchetti di pari livello


200

Ho provato a leggere le domande sulle importazioni dei fratelli e persino sul documentazione pacchetto , ma non ho ancora trovato una risposta.

Con la seguente struttura:

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

Come possono importare gli script nelle directory examplese testsdal file api modulo ed essere eseguiti dalla riga di comando?

Inoltre, vorrei evitare il brutto sys.path.inserthack per ogni file. Sicuramente questo può essere fatto in Python, giusto?


7
Consiglio di saltare tutti gli sys.pathhack e di leggere l'unica vera soluzione che è stata pubblicata finora (dopo 7 anni!).
Aran-Fey,

1
A proposito, c'è ancora spazio per un'altra buona soluzione: separare il codice eseguibile dal codice della libreria; il più delle volte uno script all'interno di un pacchetto non dovrebbe essere eseguibile per cominciare.
Aran-Fey,

Questo è così utile, sia la domanda che le risposte. Sono solo curioso, come mai "risposta accettata" non è la stessa di quella che ha ricevuto la generosità in questo caso?
Indominus,

@ Aran-Fey Questo è un promemoria sottovalutato in queste relative domande e risposte sull'errore di importazione. Ho cercato un hack per tutto il tempo, ma in fondo sapevo che c'era un modo semplice per uscire dal problema. Per non dire che è la soluzione per tutti coloro che leggono qui, ma è un buon promemoria come potrebbe essere per molti.
colorlace

Risposte:


70

Sette anni dopo

Da quando ho scritto la risposta di seguito, la modifica sys.pathè ancora un trucco semplice e veloce che funziona bene per gli script privati, ma ci sono stati diversi miglioramenti

  • L'installazione del pacchetto (in virtualenv o no) ti darà quello che vuoi, anche se suggerirei di usare pip per farlo piuttosto che usare direttamente setuptools (e usaresetup.cfg per archiviare i metadati)
  • Usando la -mbandiera e l'esecuzione come pacchetto funzionano (ma risulteranno un po 'scomodi se si desidera convertire la directory di lavoro in un pacchetto installabile).
  • Per i test, in particolare, pytest è in grado di trovare il pacchetto api in questa situazione e si occupa degli sys.pathhack per te

Quindi dipende davvero da cosa vuoi fare. Nel tuo caso, però, poiché sembra che il tuo obiettivo sia quello di creare un pacchetto adeguato ad un certo punto, installandolopip -e è probabilmente la soluzione migliore, anche se non è ancora perfetta.

Vecchia risposta

Come già detto altrove, la terribile verità è che devi fare brutti hack per consentire l'importazione da moduli di fratelli o pacchetto genitori da un __main__modulo. Il problema è dettagliato in PEP 366 . PEP 3122 ha tentato di gestire le importazioni in un modo più razionale, ma Guido l'ha respinto per un conto

L'unico caso d'uso sembra essere l'esecuzione di script che si trovano all'interno della directory di un modulo, che ho sempre visto come un antipattern.

( qui )

Tuttavia, uso questo modello regolarmente

# 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

Ecco path[0]la cartella principale del tuo script in esecuzione edir(path[0]) la cartella di livello superiore.

Tuttavia, non sono stato ancora in grado di utilizzare le importazioni relative con questo, ma consente importazioni assolute dal livello superiore (nella apicartella principale del tuo esempio ).


3
non è necessario se si esegue da una directory di progetto utilizzando -mform o se si installa il pacchetto (pip e virtualenv lo rendono facile)
jfs

2
Come fa pytest a trovare il pacchetto api per te? In modo divertente, ho trovato questo thread perché sto incontrando questo problema in particolare con l'importazione di pacchetti pytest e fratello.
JuniorIncanter,

1
Ho due domande, per favore. 1. Il tuo modello sembra funzionare senza __package__ = "examples"di me. Perche 'lo usi? 2. In quale situazione si trova __name__ == "__main__"ma __package__non lo è None?
actual_panda,

@actual_panda L'impostazione __packages__aiuta se si desidera un percorso assoluto come examples.apilavorare con iirc (ma è passato molto tempo dall'ultima volta che l'ho fatto) e controllare che il pacchetto non sia None è stato principalmente un fail-safe per situazioni strane e futureproofing.
Evpok,

Accidenti, se solo altre lingue renderebbero lo stesso processo semplice come in Python. Vedo perché tutti amano questa lingua. A proposito, anche la documentazione è eccellente. Adoro estrarre i tipi di ritorno da testo non strutturato, è un bel cambiamento da Javadoc e phpdoc. ffs ....
matt

168

Stanco degli hack di sys.path?

Ce ne sono molti sys.path.append -hack disponibili, ma ho trovato un modo alternativo per risolvere il problema in mano.

Sommario

  • Avvolgere il codice in una cartella (ad es. packaged_stuff)
  • Usa lo setup.pyscript di creazione in cui usi setuptools.setup () .
  • Pip installa il pacchetto in stato modificabile con pip install -e <myproject_folder>
  • Importa utilizzando from packaged_stuff.modulename import function_name

Impostare

Il punto di partenza è la struttura del file che hai fornito, racchiusa in una cartella chiamata 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

Chiamerò la .cartella principale e nel mio caso di esempio si trova in C:\tmp\test_imports\.

api.py

Come test case, usiamo il seguente ./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()

Prova a eseguire 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'

Anche provare le importazioni relative non funzionerà:

L'uso from ..api.api import function_from_apisarebbe risultato in

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

passi

  1. Crea un file setup.py nella directory di livello principale

I contenuti per il setup.pysarebbero *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Usa un ambiente virtuale

Se hai familiarità con gli ambienti virtuali, attivane uno e vai al passaggio successivo. L'uso di ambienti virtuali non è assolutamente necessario, ma ti aiuteranno davvero a lungo termine (quando hai in corso più di 1 progetto ...). I passaggi più basilari sono (esegui nella cartella principale)

  • Crea un ambiente virtuale
    • python -m venv venv
  • Attiva env virtuale
    • source ./venv/bin/activate(Linux, macOS) o ./venv/Scripts/activate(Win)

Per saperne di più su questo, basta uscire da Google "tutorial virtuale su Python" o simili. Probabilmente non avrai mai bisogno di altri comandi oltre a creare, attivare e disattivare.

Dopo aver creato e attivato un ambiente virtuale, la console dovrebbe fornire il nome dell'ambiente virtuale tra parentesi

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

e l'albero delle cartelle dovrebbe apparire così **

.
├── 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 installa il tuo progetto in stato modificabile

Installa il tuo pacchetto di livello superiore myprojectutilizzando pip. Il trucco è usare la -ebandiera durante l'installazione. In questo modo viene installato in uno stato modificabile e tutte le modifiche apportate ai file .py verranno automaticamente incluse nel pacchetto installato.

Nella directory principale, eseguire

pip install -e . (notare il punto, sta per "directory corrente")

Puoi anche vedere che è installato usando 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. Aggiungi myproject.alle tue importazioni

Tieni presente che dovrai aggiungere myproject.solo le importazioni che altrimenti non funzionerebbero. Le importazioni che hanno funzionato senza setup.pye pip installfunzioneranno ancora bene. Vedi un esempio di seguito.


Prova la soluzione

Ora, testiamo la soluzione usando api.pydefinito sopra e test_one.pydefinito di seguito.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

eseguendo il test

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

* Vedi i documenti setuptools per esempi più dettagliati di setup.py.

** In realtà, potresti mettere il tuo ambiente virtuale ovunque sul tuo disco rigido.


13
Grazie per il post dettagliato. Ecco il mio problema Se faccio tutto quello che hai detto e faccio un congelamento del pip, ottengo una linea -e git+https://username@bitbucket.org/folder/myproject.git@f65466656XXXXX#egg=myprojectQualche idea su come risolvere?
Si lun

2
Perché la relativa soluzione di importazione non funziona? Ti credo, ma sto cercando di capire il sistema contorto di Python.
Jared Nielsen,

8
Qualcuno ha problemi per quanto riguarda un ModuleNotFoundError? Ho installato 'myproject' in un virtualenv seguendo questi passaggi, e quando entro in una sessione interpretata ed eseguo import myprojectottengo ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectmostra che è lì, la directory è corretta, e sia la versione di pipe pythonsono verificati per essere corretti.
Quel tipo il

2
Ehi @ np8, funziona, l'ho installato accidentalmente in venv e in os :) pip listmostra i pacchetti, mentre pip freezemostra nomi strani se installato con flag -e
Grzegorz Krug

3
Ho trascorso circa 2 ore a cercare di capire come far funzionare le importazioni relative e questa risposta è stata quella che alla fine ha fatto qualcosa di sensato. 👍👍
Graham Lea,

43

Ecco un'altra alternativa che inserisco nella parte superiore dei file Python nella testscartella:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))

1
+1 davvero semplice e ha funzionato perfettamente. Devi aggiungere la classe genitore all'importazione (ex api.api, esempi.esempio_due) ma preferisco così.
Evan Plaice,

10
Penso che valga la pena ricordare ai neofiti (come me) che ..qui è relativo alla directory da cui si sta eseguendo --- non alla directory che contiene quel file di test / esempio. Sto eseguendo dalla directory del progetto, e ./invece avevo bisogno . Spero che questo aiuti qualcun altro.
Joshua Detwiler,

@JoshDetwiler, sì, assolutamente. Non ero conscio di ciò. Grazie.
Doak

1
Questa è una risposta scadente. Hacking the path non è una buona pratica; è scandaloso quanto sia usato nel mondo dei pitoni. Uno dei punti principali di questa domanda era vedere come si potevano fare le importazioni evitando questo tipo di hack.
jtcotton63,

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc

31

Non hai bisogno e non dovresti hackerare a sys.pathmeno che non sia necessario e in questo caso non lo è. Uso:

import api.api_key # in tests, examples

Esegui dalla directory del progetto: python -m tests.test_one .

Probabilmente dovresti spostarti tests(se sono unittest di api) all'interno apied eseguire python -m api.testper eseguire tutti i test (supponendo che ci sia __main__.py) o python -m api.test.test_oneper eseguiretest_one invece.

Potresti anche rimuovere __init__.pyda examples(non è un pacchetto Python) ed eseguire gli esempi in un virtualenv in cui apiè installato, ad esempio, pip install -e .in un virtualenv installerebbe un apipacchetto inplace se hai ragione setup.py.


@Alex la risposta non presuppone che i test siano test API, ad eccezione del paragrafo in cui si dice esplicitamente "se sono unittest di api" .
jfs

sfortunatamente allora sei bloccato con l'esecuzione dalla directory principale e PyCharm non trova ancora il file per le sue belle funzioni
mhstnsc

@mhstnsc: non è corretto. Dovresti essere in grado di correre python -m api.test.test_oneda qualsiasi luogo quando virtualenv è attivato. Se non riesci a configurare PyCharm per eseguire i test, prova a porre una nuova domanda Stack Overflow (se non riesci a trovare una domanda esistente su questo argomento).
jfs,

@jfs Ho perso il percorso env virtuale ma non voglio usare altro che la linea shebang per eseguire questa roba da qualsiasi directory di sempre. Non si tratta di correre con PyCharm. Gli sviluppatori con PyCharm saprebbero anche che hanno il completamento e saltano attraverso le funzioni che non sono riuscito a far funzionare con qualsiasi soluzione.
mhstnsc,

@mhstnsc è sufficiente uno shebang appropriato in molti casi (puntalo al binario virtualenv python. Qualsiasi IDE Python decente dovrebbe supportare un virtualenv.
jfs

9

Non ho ancora la comprensione di Pythonology necessaria per vedere il modo previsto di condividere il codice tra progetti non correlati senza un hack di importazione fratello / fratello. Fino a quel giorno, questa è la mia soluzione. Per exampleso testsper importare cose da ..\api, sarebbe simile a:

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

Questo ti darebbe comunque la directory padre API e non avresti bisogno della concatenazione "/ .." sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( file ))) )
Camilo Sanchez,

4

Per le importazioni di pacchetti fratelli, è possibile utilizzare il metodo insert o append del modulo [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

Questo funzionerà se avvii i tuoi script come segue:

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

D'altra parte, puoi anche usare l'importazione relativa:

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

In questo caso dovrai avviare il tuo script con l' argomento '-m' (nota che, in questo caso, non devi dare l' estensione '.py' ):

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

Naturalmente, puoi mescolare i due approcci, in modo che il tuo script funzioni indipendentemente da come viene chiamato:

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

Stavo usando il framework Click che non ha il __file__globale, quindi ho dovuto usare quanto segue: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Ma ora funziona in qualsiasi directory
GammaGames

3

TLDR

Questo metodo non richiede setuptools, path hack, argomenti aggiuntivi da riga di comando o specificare il livello superiore del pacchetto in ogni singolo file del progetto.

Basta creare uno script nella directory principale di qualunque cosa tu stia chiamando per essere tuo __main__ed eseguire tutto da lì. Per ulteriori spiegazioni continua a leggere.

Spiegazione

Ciò può essere realizzato senza hackerare insieme un nuovo percorso, argomenti aggiuntivi da riga di comando o aggiungere codice a ciascuno dei tuoi programmi per riconoscere i suoi fratelli.

La ragione per cui questo fallisce come credo sia stato menzionato prima è che i programmi chiamati hanno il loro __name__set come__main__ . Quando ciò si verifica, lo script chiamato accetta se stesso di essere al livello superiore del pacchetto e rifiuta di riconoscere gli script nelle directory dei fratelli.

Tuttavia, tutto sotto il livello superiore della directory riconoscerà ANYTHING ELSE sotto il livello superiore. Ciò significa che l' unica cosa che devi fare per ottenere i file nelle directory dei fratelli per riconoscersi / utilizzarli è chiamarli da uno script nella loro directory principale.

Proof of Concept In un dir con la seguente struttura:

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

Main.py contiene il seguente codice:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py contiene:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

e sib2 / Callsib.py contiene:

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

if __name__ == '__main__':
    CallSib()

Se riproduci questo esempio, noterai che la chiamata Main.pycomporterà la stampa di "Chiamato" come definito, sib2/callsib.py anche se sib2/callsib.pyviene richiamato sib1/call.py. Tuttavia, se si dovesse chiamare direttamente sib1/call.py(dopo aver apportato le opportune modifiche alle importazioni) si genera un'eccezione. Anche se ha funzionato quando chiamato dallo script nella sua directory principale, non funzionerà se si ritiene che si trovi al livello più alto del pacchetto.


2

Ho realizzato un progetto di esempio per dimostrare come ho gestito questo, che in effetti è un altro hack sys.path come indicato sopra. Esempio di importazione di Python Sibling , che si basa su:

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

Questo sembra essere abbastanza efficace fintanto che la directory di lavoro rimane alla radice del progetto Python. Se qualcuno lo distribuisce in un vero ambiente di produzione, sarebbe bello sapere se funziona anche lì.


Funziona solo se stai eseguendo dalla directory principale dello script
Evpok il

1

Devi guardare per vedere come sono scritte le dichiarazioni di importazione nel relativo codice. Se examples/example_one.pyutilizza la seguente dichiarazione di importazione:

import api.api

... quindi si aspetta che la directory principale del progetto si trovi nel percorso di sistema.

Il modo più semplice per supportare questo senza alcun hack (come lo hai messo) sarebbe quello di eseguire gli esempi dalla directory di livello superiore, in questo modo:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 

Con Python 2.7.1 ottengo il seguente: $ 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. Anch'io ottengo lo stesso con import api.api.
Zachwill,

Aggiornato la mia risposta ... tu non devi aggiungere la directory corrente al percorso di importazione, alcun modo per aggirare questo.
AJ.

1

Nel caso in cui qualcuno che utilizza Pydev su Eclipse finisca qui: è possibile aggiungere il percorso padre del fratello (e quindi il padre del modulo chiamante) come cartella di libreria esterna usando Progetto-> Proprietà e impostando Librerie esterne nel menu a sinistra Pydev-PYTHONPATH . Quindi puoi importare dal tuo fratello, ad esfrom sibling import some_class .


-3

Innanzitutto, dovresti evitare di avere file con lo stesso nome del modulo stesso. Potrebbe interrompere altre importazioni.

Quando si importa un file, prima l'interprete controlla la directory corrente e quindi cerca le directory globali.

All'interno exampleso testspuoi chiamare:

from ..api import api

Ottengo quanto segue con 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
Oh, allora dovresti aggiungere un __init__.pyfile alla directory di livello superiore. Altrimenti Python non può trattarlo come un modulo

8
Non funzionerà Il problema non è che la cartella padre non è un pacchetto, è che poiché il modulo __name__è __main__invece di package.module, Python non può vedere il suo pacchetto padre, quindi .punta a nulla.
Evpok,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.