Meio Ambiente
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.
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 ...
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.
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 fazername @ ./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:
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?
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.
Comentários muito úteis
Ok, eu vejo o problema.
setuptools
,pkg-resources
epip
usam versões ligeiramente diferentes da bibliotecapackaging
.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 vazionetloc
):🙄
Isso significa que, como meu caminho de arquivo tem
file:///foo/bar
e nãofile://localhost/foo/bar
, ele falha.Aqui está a solução completa :
Isso é uma UX muito ruim misturada com erros ambíguos e que causam perda de tempo.
Como podemos melhorar esta situação?