<p>pip não suporta caminhos relativos em referências diretas de URL</p>

Criado em 28 jun. 2019  ·  7Comentários  ·  Fonte: pypa/pip

Meio Ambiente

  • versão pip: 19.1.1
  • Versão Python: 3.7
  • SO: MacOS Mojave
Sinta-se à vontade para adicionar mais informações sobre o seu ambiente aqui

Descrição
Não tenho certeza se este é um bug six ou um bug do Pip. Desculpe-me se ele pertence a six .

Pip parece permitir caminhos locais em install_requires via name @ ./some/path , mas a análise de URL está terrivelmente quebrada.

https://github.com/pypa/pip/blob/a38a0eacd4650a7d9af4e6831a2c0aed0b6a0329/src/pip/_internal/download.py#L670 -L690

Nesta função, ele usa urlsplit para obter os componentes individuais da URL de entrada.

Aqui está o que parece com algumas peças de entrada:

./foo/bar -> SplitResult(scheme='', netloc='', path='./foo/bar', query='', fragment='')
file:./foo/bar -> SplitResult(scheme='file', netloc='', path='./foo/bar', query='', fragment='')
file://./foo/bar -> SplitResult(scheme='file', netloc='.', path='/foo/bar', query='', fragment='')

Observe que o último resulta em netloc sendo . vez de vazio e path sendo absoluto, não local. Isso aciona o erro em relação a caminhos não locais. Está tudo bem - posso usar a segunda forma para satisfazer a lógica condicional (embora ela realmente deva oferecer suporte à primeira também).

No entanto, há uma lógica conflitante em outro lugar ...

https://github.com/pypa/pip/blob/169eebdb6e36a31e545804228cad94902a1ec8e9/src/pip/_vendor/packaging/requirements.py#L103 -L106

Essa é a lógica que falha, embora satisfaçamos a lógica anterior.

Aqui está uma função de teste que mostra o problema:

from six.moves.urllib import parse as urllib_parse

def tryparse(url):
    print(url)
    parsed = urllib_parse.urlparse(url)
    unparsed = urllib_parse.urlunparse(parsed)
    parsed_again = urllib_parse.urlparse(unparsed)
    print(parsed)
    print(unparsed)
    print(parsed_again)

Aqui está a saída para ./foo/bar :

>>> tryparse('./foo/bar')
./foo/bar
ParseResult(scheme='', netloc='', path='./foo/bar', params='', query='', fragment='')
./foo/bar
ParseResult(scheme='', netloc='', path='./foo/bar', params='', query='', fragment='')

Tudo bem, embora não satisfaça a lógica da primeira função de exigir um esquema de file: .

Aqui está o resultado para file:./foo/bar :

>>> tryparse('file:./foo/bar')
file:./foo/bar
ParseResult(scheme='file', netloc='', path='./foo/bar', params='', query='', fragment='')
file:///./foo/bar
ParseResult(scheme='file', netloc='', path='/./foo/bar', params='', query='', fragment='')

Ops! Observe como, quando "desparamos" o resultado da primeira chamada de parse, nosso path torna-se file:///... absoluto.

É por isso que a segunda verificação mencionada falha - o caminho não é local. Acredito que seja um bug em six mas pode ser mitigado no Pip permitindo scheme in ['file', ''] e instruindo os usuários a usar o formulário de URI ./foo/bar .

Dadas essas duas peças lógicas contraditórias, é impossível usar caminhos locais em chaves install_requires nas configurações distutils ou setuptools .

Comportamento esperado
Devo ser capaz de fazer name @ ./some/path (ou, honestamente, simplesmente ./some/path ) para especificar um pacote vendido localmente para minha base de código.

Como reproduzir

#!/usr/bin/env bash
mkdir /tmp/pip-uri-repro && cd /tmp/pip-uri-repro

mkdir -p foo/bar

cat > requirements.txt <<EOF
./foo
EOF

cat > foo/setup.py <<EOF
#!/usr/bin/env python
from setuptools import setup
setup(
    name="foo",
    version="0.1",
    install_requires=[
        "bar @ file:./bar"
    ]
)
EOF

cat > foo/bar/setup.py <<EOF
#!/usr/bin/env python
from setuptools import setup
setup(
    name="bar",
    version="0.1"
)
EOF

# (OUTPUT 1)
pip install -r requirements.txt

cat > foo/setup.py <<EOF
#!/usr/bin/env python
from setuptools import setup
setup(
    name="foo",
    version="0.1",
    install_requires=[
        # we're forced to use an absolute path
        # to make the "Invalid URL" error go
        # away, which isn't right anyway (the
        # error that is raised as a result
        # is justified)
        "bar @ file://./bar"
    ]
)
EOF

# (OUTPUT 2)
pip install -r requirements.txt

Resultado

Desde o primeiro pip install :

Processing ./foo
    ERROR: Complete output from command python setup.py egg_info:
    ERROR: error in foo setup command: 'install_requires' must be a string or list of strings containing valid project/version requirement specifiers; Invalid URL given

A partir do segundo pip install :

Processing ./foo
ERROR: Exception:
Traceback (most recent call last):
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/cli/base_command.py", line 178, in main
    status = self.run(options, args)
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/commands/install.py", line 352, in run
    resolver.resolve(requirement_set)
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/resolve.py", line 131, in resolve
    self._resolve_one(requirement_set, req)
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/resolve.py", line 294, in _resolve_one
    abstract_dist = self._get_abstract_dist_for(req_to_install)
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/resolve.py", line 242, in _get_abstract_dist_for
    self.require_hashes
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/operations/prepare.py", line 256, in prepare_linked_requirement
    path = url_to_path(req.link.url)
  File "/private/tmp/repro-pip-egg/env3/lib/python3.7/site-packages/pip/_internal/download.py", line 521, in url_to_path
    % url
ValueError: non-local file URIs are not supported on this platform: 'file://./bar'

EDITAR:

Acabei de descobrir que a RFC 3986 especifica que URIs de caminho relativo não são permitidos com o esquema file: , então, tecnicamente, six deveria estar errando em file:./foo/bar .

No entanto, isso significa que, tecnicamente, devo ser capaz de fazer o seguinte em meu setup.py:

PKG_DIR = os.path.dirname(os.path.abspath(__file__))
install_requires = [
    f"name @ file://{PKG_DIR}/foo/bar"
]

No entanto, pip parece estar criando uma cópia "limpa" do pacote em /tmp , então obtemos algo como file:///tmp/pip-req-build-9u3z545j/foo/bar .

Executando isso por meio de nossa função de teste, satisfazemos a condicional da segunda função:

>>> tryparse('file:///tmp/pip-req-build-9u3z545j/foo/bar')
file:///tmp/pip-req-build-9u3z545j/foo/bar
ParseResult(scheme='file', netloc='', path='/tmp/pip-req-build-9u3z545j/foo/bar', params='', query='', fragment='')
file:///tmp/pip-req-build-9u3z545j/foo/bar
ParseResult(scheme='file', netloc='', path='/tmp/pip-req-build-9u3z545j/foo/bar', params='', query='', fragment='')

Está tudo bem aí. O "unparse" produz o mesmo resultado e os requisitos de netloc são atendidos para a primeira função condicional.

No entanto, ainda encontramos um erro Invalid URL , embora a lógica da segunda função seja satisfeita.

Como pip (ou distutils ou setuptools ou qualquer outro) engole a saída, fui em frente e fiz o seguinte em meu setup.py

import os
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
assert False, os.system(f"find {PKG_DIR}")

O que verifica se todos os arquivos estão lá, conforme o esperado - então não pode ser um arquivo faltando ou algo assim. A linha acima que tem "Invalid URL given" é o único lugar na base de código que a string aparece.

Neste ponto, não tenho certeza de qual é o problema.

vendored dependency needs discussion bug

Comentários muito úteis

Ok, eu vejo o problema. setuptools , pkg-resources e pip usam versões ligeiramente diferentes da biblioteca packaging .

Em pip , é a versão que mostrei acima.

No entanto, em todo o resto, é o seguinte (não tenho certeza qual é o "mais novo", mas a lógica a seguir é muito limitante e não é totalmente compatível de acordo com RFC 3986, pois file:/// deve ser permitido, implicando um vazio netloc ):

        if req.url:
            parsed_url = urlparse.urlparse(req.url)
            if not (parsed_url.scheme and parsed_url.netloc) or (
                    not parsed_url.scheme and not parsed_url.netloc):
                raise InvalidRequirement("Invalid URL given")

🙄

Isso significa que, como meu caminho de arquivo tem file:///foo/bar e não file://localhost/foo/bar , ele falha.

Aqui está a solução completa :

import os
from setuptools import setup

PKG_DIR = os.path.dirname(os.path.abspath(__file__))

setup(
    install_requires=[
        f'foo @ file://localhost{PKG_DIR}/foo/bar'
    ]
)

Isso é uma UX muito ruim misturada com erros ambíguos e que causam perda de tempo.

Como podemos melhorar esta situação?

Todos 7 comentários

Ok, eu vejo o problema. setuptools , pkg-resources e pip usam versões ligeiramente diferentes da biblioteca packaging .

Em pip , é a versão que mostrei acima.

No entanto, em todo o resto, é o seguinte (não tenho certeza qual é o "mais novo", mas a lógica a seguir é muito limitante e não é totalmente compatível de acordo com RFC 3986, pois file:/// deve ser permitido, implicando um vazio netloc ):

        if req.url:
            parsed_url = urlparse.urlparse(req.url)
            if not (parsed_url.scheme and parsed_url.netloc) or (
                    not parsed_url.scheme and not parsed_url.netloc):
                raise InvalidRequirement("Invalid URL given")

🙄

Isso significa que, como meu caminho de arquivo tem file:///foo/bar e não file://localhost/foo/bar , ele falha.

Aqui está a solução completa :

import os
from setuptools import setup

PKG_DIR = os.path.dirname(os.path.abspath(__file__))

setup(
    install_requires=[
        f'foo @ file://localhost{PKG_DIR}/foo/bar'
    ]
)

Isso é uma UX muito ruim misturada com erros ambíguos e que causam perda de tempo.

Como podemos melhorar esta situação?

@ Qix- que bom que você encontrou isso! Eu estava batendo minha cabeça contra a parede tentando todos os mesmos formatos. Esta é minha opção alternativa para https://github.com/pypa/pip/issues/6162 e a depreciação de dependency_links.

Estamos tentando configurar um repo privado e não temos nosso próprio servidor interno. Nossa solução é publicar pacotes em s3 e, para consumi-los, nós os baixamos, colocamos em uma pasta local e depois os adicionamos a install_requires .

Tenho certeza de que há muitos outros casos de uso que se beneficiariam de uma maneira intuitiva de instalar pacotes locais.

@ryanaklein Eu realmente sugeriria ignorar toda a negatividade não pesquisada em relação aos submódulos git e experimentá-los (supondo que você esteja usando Git). Se você parar de pensar neles como branches e começar a pensar neles como tags (ou releases), eles começarão a funcionar muito bem. Eles são usados ​​com frequência no mundo C / C ++, e vendemos pacotes Python usando-os com muito sucesso (além do bug acima, é claro!).

Pode reduzir os custos de rede / $$ do S3 :)

Comportamento esperado
Devo ser capaz de fazer name @ ./some/path (ou, honestamente, simplesmente ./some/path ) para especificar um pacote vendido localmente para minha base de código.

Para a referência de URL direta ( name @ ./some/path ), há dois lugares onde o trabalho está acontecendo:

  1. pypa / packaging # 120 que rastreia a rejeição de referências de url diretas PEP 508
  2. no lado do pip, precisamos interpretar caminhos relativos em relação a algo, comecei a discussão aqui para obter feedback sobre o que faz mais sentido. Poderíamos rastrear a ação eventual aqui ou em uma nova edição dedicada.

Este último não seria aceitável pelo PEP 508, portanto, seria difícil justificar o suporte, muito menos fazê-lo funcionar em todas as ferramentas.

Isso é uma UX muito ruim misturada com erros ambíguos e que causam perda de tempo.
Como podemos melhorar esta situação?

5204 deve ajudar com esta questão geral do ponto de vista do pip.

Ops! Observe como, quando "desparamos" o resultado da primeira chamada de análise, nosso caminho se torna arquivo absoluto: /// ...

Acho que isso se deve ao bug CPython levantado na questão 22852 - "urllib.parse erroneamente remove #fragment,? Query, // netloc"

Esse bug parece causar também o problema nº 3783 - veja este comentário .

Qual é a situação deste problema? Uma solução para resolver dependências locais que não estão no PyPI é necessária com urgência, por exemplo, no contexto de repositórios monolíticos.

Observe que o npm implementou esse recurso de maneira semelhante e dependencies pode ser especificado em package.json usando um caminho local .

Por favor, veja este comentário acima para saber o que precisa acontecer antes que isso tenha a chance de ser implementado. pip não pode fazer nada antes disso.

Esta página foi útil?
0 / 5 - 0 avaliações