Importação de pacotes irmãos

199

Tentei ler as perguntas sobre importações de irmãos e até a documentação do pacote , mas ainda não encontrei uma resposta.

Com a seguinte estrutura:

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

Como podem os scripts no examplese testsdiretórios importar do apimódulo e ser executado a partir da linha de comando?

Além disso, eu gostaria de evitar o feio sys.path.inserthack para cada arquivo. Certamente isso pode ser feito em Python, certo?

zachwill
fonte
7
Eu recomendo ignorar todos os sys.pathhacks e ler a única solução real publicada até agora (após 7 anos!).
22418 Aran-Fey #
1
A propósito, ainda há espaço para outra boa solução: separar o código executável do código da biblioteca; na maioria das vezes, um script dentro de um pacote não deve ser executável para começar.
Aran-Fey
Isso é muito útil, tanto para a pergunta quanto para as respostas. Estou curioso, como é que "Resposta Aceita" não é a mesma que recebeu a recompensa neste caso?
Indominus
@ Aran-Fey Esse é um lembrete subestimado nessas perguntas e respostas relativas a erros de importação. Eu estive procurando um hack o tempo todo, mas no fundo eu sabia que havia uma maneira simples de projetar minha saída do problema. Para não dizer que é a solução para todos que estão lendo, mas é um bom lembrete, pois pode ser para muitos.
precisa saber é o seguinte

Respostas:

69

Sete anos depois

Desde que escrevi a resposta abaixo, modificar sys.pathainda é um truque rápido e sujo que funciona bem para scripts privados, mas houve várias melhorias

  • Instalar o pacote (em um virtualenv ou não) fornecerá o que você deseja, embora eu sugira usar o pip para fazê-lo, em vez de usar diretamente o setuptools (e usar setup.cfgpara armazenar os metadados)
  • Usar o -msinalizador e executar como um pacote também funciona (mas ficará um pouco estranho se você quiser converter seu diretório de trabalho em um pacote instalável).
  • Para os testes, especificamente, o pytest é capaz de encontrar o pacote api nessa situação e cuida dos sys.pathhacks para você

Então, realmente depende do que você quer fazer. Porém, no seu caso, já que parece que seu objetivo é criar um pacote adequado em algum momento, a instalação através do pip -eprovavelmente é sua melhor aposta, mesmo que ainda não seja perfeita.

Resposta antiga

Como já foi dito em outro lugar, a terrível verdade é que você precisa fazer hacks feios para permitir a importação de módulos irmãos ou de pacotes pais de um __main__módulo. A questão está detalhada no PEP 366 . A PEP 3122 tentou lidar com as importações de uma maneira mais racional, mas Guido a rejeitou na conta de

O único caso de uso parece estar executando scripts que vivem dentro do diretório de um módulo, que eu sempre vi como um antipadrão.

( aqui )

Porém, eu uso esse padrão regularmente com

# 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

Aqui path[0]está a pasta pai do script em execução e dir(path[0])a pasta de nível superior.

Porém, ainda não consegui usar importações relativas com isso, mas ele permite importações absolutas do nível superior (na apipasta pai do seu exemplo ).

Evpok
fonte
3
você não tem que se você executar a partir de um diretório de projeto usando -mforma ou se você instalar o pacote (pip e virtualenv tornar mais fácil)
JFS
2
Como o pytest encontra o pacote api para você? Divertidamente, encontrei esse segmento porque estou enfrentando esse problema especificamente com a importação de pacotes pytest e irmãos.
JuniorIncanter 18/01/19
1
Eu tenho duas perguntas, por favor. 1. Seu padrão parece funcionar sem __package__ = "examples"mim. Por quê você usa isso? 2. Em que situação é, __name__ == "__main__"mas __package__não é None?
Atual_panda #
@actual_panda A configuração __packages__ajuda se você quiser um caminho absoluto, como o examples.apiiirc (mas já faz muito tempo desde a última vez que fiz isso) e verificar se o pacote não é Nenhum.
Evpok 30/01
165

Cansado de hacks sys.path?

Existem muitos sys.path.appendtruques disponíveis, mas eu encontrei uma maneira alternativa de resolver o problema em questão.

Resumo

  • Coloque o código em uma pasta (por exemplo packaged_stuff)
  • Use o setup.pyscript create onde você usa setuptools.setup () .
  • O Pip instala o pacote no estado editável com pip install -e <myproject_folder>
  • Importar usando from packaged_stuff.modulename import function_name

Configuração

O ponto de partida é a estrutura de arquivos que você forneceu, agrupada em uma pasta chamada 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

Vou chamar a .pasta raiz e, no meu exemplo, está localizada em C:\tmp\test_imports\.

api.py

Como caso de teste, vamos usar o seguinte ./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()

Tente executar 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'

Também tentar importações relativas não funcionará:

O uso from ..api.api import function_from_apiresultaria em

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

Passos

  1. Crie um arquivo setup.py no diretório no nível raiz

O conteúdo para o setup.pyseria *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Use um ambiente virtual

Se você estiver familiarizado com ambientes virtuais, ative um e pule para a próxima etapa. O uso de ambientes virtuais não é absolutamente necessário, mas eles realmente o ajudarão a longo prazo (quando você tiver mais de um projeto em andamento ..). As etapas mais básicas são (executadas na pasta raiz)

  • Criar ambiente virtual
    • python -m venv venv
  • Ativar env virtual
    • source ./venv/bin/activate(Linux, macOS) ou ./venv/Scripts/activate(Win)

Para saber mais sobre isso, basta pesquisar no "python virtual env tutorial" ou similar. Você provavelmente nunca precisará de outros comandos além de criar, ativar e desativar.

Depois de criar e ativar um ambiente virtual, seu console deve dar o nome do ambiente virtual entre parênteses

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

e sua árvore de pastas deve ficar assim **

.
├── 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 instale seu projeto em estado editável

Instale seu pacote de nível superior myprojectusando pip. O truque é usar a -ebandeira ao fazer a instalação. Dessa forma, ele é instalado em um estado editável e todas as edições feitas nos arquivos .py serão automaticamente incluídas no pacote instalado.

No diretório raiz, execute

pip install -e . (observe o ponto, significa "diretório atual")

Você também pode ver que ele está instalado 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. Adicione myproject.em suas importações

Observe que você precisará adicionar myproject.apenas importações que não funcionariam de outra maneira. As importações que funcionaram sem o setup.py& pip installainda funcionarão bem. Veja um exemplo abaixo.


Teste a solução

Agora, vamos testar a solução usando api.pydefinido acima e test_one.pydefinido abaixo.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

executando o teste

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

* Consulte os documentos setuptools para obter mais exemplos detalhados de setup.py.

** Na realidade, você pode colocar seu ambiente virtual em qualquer lugar do seu disco rígido.

np8
fonte
13
Obrigado pelo post detalhado. Aqui está o meu problema. Se eu fizer tudo o que você disse e congelar pip, recebo uma linha -e git+https://[email protected]/folder/myproject.git@f65466656XXXXX#egg=myprojectAlguma idéia de como resolver?
111318 Si Si
2
Por que a solução de importação relativa não funciona? Eu acredito em você, mas estou tentando entender o sistema complicado do Python.
Jared Nielsen
8
Alguém tem problemas com relação a um ModuleNotFoundError? Instalei 'myproject' em um virtualenv seguindo estas etapas e, quando entro em uma sessão interpretada e executo import myproject, recebo ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectmostra que ele está lá, o diretório está correto e a verificação pipe a pythonverificação estão corretas.
precisa saber é o seguinte
2
Hey @ NP8, ele funciona, eu acidentalmente instalou em venv e no OS :) pip listpacotes de shows, enquanto pip freezemostra nomes estranhos se instalado com a bandeira -e
Grzegorz Krug
3
Passei cerca de duas horas tentando descobrir como fazer as importações relativas funcionarem, e essa resposta foi a que finalmente fez algo sensato. Gra
Graham Lea
43

Aqui está outra alternativa que eu insiro na parte superior dos arquivos Python na testspasta:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Cenk Alti
fonte
1
+1 realmente simples e funcionou perfeitamente. Você precisa adicionar a classe pai à importação (ex api.api, examples.example_two), mas eu prefiro assim.
Evan Plaice
10
Eu acho que vale a pena mencionar para iniciantes (como eu) que ..aqui é relativo ao diretório do qual você está executando - não ao diretório que contém esse arquivo de teste / exemplo. Estou executando a partir do diretório do projeto e, em ./vez disso, precisava . Espero que isso ajude outra pessoa.
Joshua Detwiler
@ JoshDetwiler, sim, absolutamente. Eu não estava ciente disso. Obrigado.
Dok
1
Esta é uma resposta ruim. Abrir o caminho não é uma boa prática; é escandaloso o quanto é usado no mundo python. Um dos principais pontos dessa pergunta foi ver como as importações poderiam ser feitas, evitando esse tipo de invasão.
jtcotton63 20/02
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc
31

Você não precisa e não deve invadir, a sys.pathmenos que seja necessário e, nesse caso, não é. Usar:

import api.api_key # in tests, examples

Executado a partir do diretório do projeto: python -m tests.test_one.

Você provavelmente deve se mover tests(se forem unittests da API) para dentro apie executar python -m api.testpara executar todos os testes (supondo que exista __main__.py) ou python -m api.test.test_oneexecutar test_one.

Você também pode remover __init__.pyde examples(não é um pacote Python) e executar os exemplos em um virtualenv onde apiestá instalado, por exemplo, pip install -e .em um virtualenv instalaria o apipacote local se você tiver o apropriado setup.py.

jfs
fonte
@Alex, a resposta não pressupõe que os testes sejam API, exceto o parágrafo em que diz explicitamente "se são os unittests da API" .
JFS
infelizmente, em seguida, você está preso com o funcionamento do dir raiz e PyCharm ainda não encontrar o arquivo para suas funções agradáveis
mhstnsc
@mhstnsc: não está correto. Você deve poder executar python -m api.test.test_onede qualquer lugar quando o virtualenv estiver ativado. Se você não pode configurar o PyCharm para executar seus testes, tente fazer uma nova pergunta de Estouro de Pilha (se você não encontrar uma pergunta existente sobre este tópico).
JFS
@ jfs Eu perdi o caminho do ambiente virtual, mas não quero usar nada além da linha shebang para executar essas coisas em qualquer diretório de todos os tempos. Não se trata de rodar com PyCharm. Devs com PyCharm saberiam também que eles têm conclusão e passam por funções que eu não poderia fazer funcionar com nenhuma solução.
mhstnsc
@mhstnsc um shebang apropriada é suficiente em muitos casos (aponte-o para o virtualenv python binário Qualquer decente Python IDE devem apoiar uma virtualenv..
jfs
9

Ainda não tenho a compreensão de Pythonology necessária para ver a maneira pretendida de compartilhar código entre projetos não relacionados sem um hack de importação relativo / irmão. Até aquele dia, esta é a minha solução. Para examplesou testsimportar coisas de ..\api, seria semelhante 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
user1330131
fonte
Isso ainda forneceria o diretório pai da API e você não precisaria da concatenação "/ .." sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( arquivo ))) )
Camilo Sanchez
4

Para importações de pacotes irmãos, você pode usar o método insert ou append do módulo [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

Isso funcionará se você estiver iniciando seus scripts da seguinte maneira:

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

Por outro lado, você também pode usar a importação relativa:

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

Nesse caso, você terá que iniciar seu script com o argumento '-m' (observe que, nesse caso, você não deve dar o '.py' extensão ):

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

Obviamente, você pode combinar as duas abordagens, para que seu script funcione, independentemente de como é chamado:

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
fonte
Eu estava usando a estrutura Clique que não tem o __file__mundial, então eu tive que usar o seguinte: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Mas ele funciona em qualquer diretório agora
GammaGames
3

TLDR

Esse método não requer ferramentas de instalação, invasões de caminho, argumentos adicionais da linha de comando ou especificação do nível superior do pacote em todos os arquivos do seu projeto.

Basta criar um script no diretório pai do que você está chamando para ser o seu __main__e executar tudo a partir daí. Para mais explicações, continue lendo.

Explicação

Isso pode ser feito sem hackear um novo caminho juntos, argumentos extras na linha de comando ou adicionar código a cada um dos seus programas para reconhecer seus irmãos.

A razão pela qual isso falha, como acredito que foi mencionado antes, é que os programas que estão sendo chamados têm o seu __name__conjunto como__main__ . Quando isso ocorre, o script que está sendo chamado se aceita no nível superior do pacote e se recusa a reconhecer scripts nos diretórios irmãos.

No entanto, tudo no nível superior do diretório ainda reconhecerá QUALQUER OUTRA COISA no nível superior. Isso significa que a única coisa que você deve fazer para obter arquivos nos diretórios irmãos para se reconhecer / utilizar um ao outro é chamá-los a partir de um script no diretório pai.

Prova de conceito Em um diretório com a seguinte estrutura:

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

Main.py contém o seguinte código:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py contém:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

e sib2 / callsib.py contém:

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

if __name__ == '__main__':
    CallSib()

Se você reproduzir este exemplo, notará que a chamada Main.pyresultará na impressão de "Recebido", conforme definido em, sib2/callsib.py mesmo que sib2/callsib.pyrecebido sib1/call.py. No entanto, se alguém ligar diretamente sib1/call.py(após fazer as alterações apropriadas nas importações), isso gera uma exceção. Embora tenha funcionado quando chamado pelo script em seu diretório pai, ele não funcionará se acreditar que está no nível superior do pacote.

Thunderwood
fonte
2

Eu fiz um projeto de amostra para demonstrar como eu lidei com isso, que é de fato outro hack do sys.path, conforme indicado acima. Exemplo de importação de irmão do Python , que se baseia em:

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

Isso parece ser bastante eficaz desde que seu diretório de trabalho permaneça na raiz do projeto Python. Se alguém implantar isso em um ambiente de produção real, seria ótimo saber se funciona lá também.

ADataGMan
fonte
Isso só funciona se você estiver executando no diretório pai do script
Evpok
1

Você precisa ver como as instruções de importação são gravadas no código relacionado. Se examples/example_one.pyusa a seguinte instrução de importação:

import api.api

... espera que o diretório raiz do projeto esteja no caminho do sistema.

A maneira mais fácil de dar suporte a isso sem hacks (como você disse) seria executar os exemplos no diretório de nível superior, como este:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
AJ.
fonte
Com Python 2.7.1 recebo a seguinte: $ 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. Eu também recebo o mesmo import api.api.
zachwill
Atualizei a minha resposta ... você não tem que adicionar o diretório atual para o caminho de importação, há maneira de contornar isso.
AJ.
1

Caso alguém usando Pydev no Eclipse acabe aqui: você pode adicionar o caminho pai do irmão (e, portanto, o pai do módulo de chamada) como uma pasta de biblioteca externa usando Projeto-> Propriedades e configurando Bibliotecas Externas no menu esquerdo Pydev-PYTHONPATH . Então você pode importar do seu irmão, por exemplo from sibling import some_class.

Lord Henry Wotton
fonte
-3

Primeiro, você deve evitar ter arquivos com o mesmo nome que o próprio módulo. Pode quebrar outras importações.

Quando você importa um arquivo, primeiro o intérprete verifica o diretório atual e depois pesquisa os diretórios globais.

Dentro examplesou testsvocê pode ligar para:

from ..api import api

fonte
Eu obtenho o seguinte com o 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
Ah, você deve adicionar um __init__.pyarquivo ao diretório de nível superior. Caso contrário Python não pode tratá-lo como um módulo
8
Isso não vai funcionar. O problema não é que a pasta pai não seja um pacote, é que, como o módulo __name__é em __main__vez de package.module, o Python não pode ver seu pacote pai, portanto, .nada indica.
Evpok