Environnement
La description
Je ne sais pas s'il s'agit d'un bogue six
ou d'un bogue Pip. Excusez-moi s'il appartient à six
.
Pip semble autoriser les chemins locaux dans install_requires
via name @ ./some/path
, mais l'analyse d'URL est terriblement cassée.
Dans cette fonction, il utilise urlsplit
pour obtenir les composants individuels de l'URL entrante.
Voici à quoi cela ressemble avec quelques éléments d'entrée:
./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='')
Notez que le dernier résultat fait que netloc
est .
au lieu de vide et que path
est absolu, pas local. Cela déclenche l'erreur concernant les chemins non locaux. C'est très bien - je peux utiliser la deuxième forme pour satisfaire la logique conditionnelle (même si elle devrait vraiment prendre en charge la première).
Cependant, il y a une logique contradictoire ailleurs ...
C'est la logique qui échoue même si nous satisfaisons la logique antérieure.
Voici une fonction de test qui montre le problème:
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)
Voici la sortie pour ./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='')
Tout va bien, même si cela ne satisfait pas la logique de la première fonction d'exiger un schéma de file:
.
Voici la sortie pour 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='')
Oops! Remarquez comment, lorsque nous "annulons" le résultat du premier appel d'analyse, notre path
devient absolu file:///...
.
C'est pourquoi la deuxième vérification mentionnée échoue - le chemin n'est pas local. Je crois que c'est un bogue dans six
mais peut être atténué dans Pip en autorisant scheme in ['file', '']
et en demandant aux utilisateurs d'utiliser le formulaire ./foo/bar
URI.
Compte tenu de ces deux éléments de logique contradictoires, il est impossible d'utiliser des chemins locaux dans les clés install_requires
dans les configurations distutils
ou setuptools
.
Comportement prévisible
Je devrais être en mesure de faire name @ ./some/path
(ou, honnêtement, simplement ./some/path
) pour spécifier un package vendu localement dans ma base de code.
Comment reproduire
#!/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
Production
À partir du premier 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
À partir du deuxième 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'
ÉDITER:
Je viens de découvrir que la RFC 3986 spécifie que les URI de chemin relatif ne sont pas autorisés avec le schéma file:
, donc techniquement six
devrait se tromper sur file:./foo/bar
.
Cependant, cela signifie que, techniquement, je devrais pouvoir faire ce qui suit dans mon setup.py:
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
install_requires = [
f"name @ file://{PKG_DIR}/foo/bar"
]
Cependant, pip semble créer une copie "propre" du paquet dans /tmp
, donc nous obtenons quelque chose comme file:///tmp/pip-req-build-9u3z545j/foo/bar
.
En exécutant cela via notre fonction de test, nous satisfaisons le conditionnel de la deuxième fonction:
>>> 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='')
Tout est bon là-bas. Le "unparse" donne le même résultat, et les conditions netloc
sont remplies pour le conditionnel de la première fonction.
Cependant, nous rencontrons toujours une erreur Invalid URL
, même si la logique de la deuxième fonction est satisfaite.
Puisque pip
(ou distutils ou setuptools ou autre) avale la sortie, je suis allé de l'avant et j'ai fait ce qui suit dans mon setup.py
import os
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
assert False, os.system(f"find {PKG_DIR}")
Ce qui vérifie que tous les fichiers sont là, comme prévu - il ne peut donc pas s'agir d'un fichier manquant ou quelque chose. La ligne ci-dessus qui a "Invalid URL given"
est le seul endroit dans la base de code où la chaîne apparaît.
À ce stade, je ne sais pas quel est le problème.
D'accord, je vois le problème. setuptools
, pkg-resources
et pip
utilisent tous des versions légèrement différentes de la bibliothèque packaging
.
Dans pip
, c'est la version que j'ai montrée ci-dessus.
Cependant, dans tout le reste, c'est le suivant (je ne suis pas sûr de savoir lequel est le "plus récent", mais la logique suivante est très limitative et n'est pas entièrement conforme à la RFC 3986 car file:///
devrait être autorisée, ce qui implique un vide 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")
🙄
Cela signifie que puisque mon chemin de fichier a file:///foo/bar
et non file://localhost/foo/bar
il échoue.
Voici la solution complète :
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'
]
)
C'est une très mauvaise expérience utilisateur mélangée à des erreurs ambiguës et fastidieuses.
Comment pouvons-nous améliorer cette situation?
@ Qix- heureux que vous ayez trouvé ça! Je me cognais la tête contre le mur en essayant tous les mêmes formats. C'est mon option alternative à https://github.com/pypa/pip/issues/6162 et la dépréciation de dependency_links.
Nous essayons de mettre en place un dépôt privé et n'avons pas notre propre serveur interne. Notre solution est de publier des packages sur s3 puis de les consommer nous les téléchargeons, les mettons dans un dossier local, puis les ajoutons à install_requires
.
Je suis sûr qu'il existe de nombreux autres cas d'utilisation qui bénéficieraient d'une manière intuitive d'installer des packages locaux.
@ryanaklein Je suggérerais en fait d'ignorer toute la négativité non recherchée envers les sous-modules git et de les essayer (en supposant que vous utilisez Git). Si vous arrêtez de les considérer comme des branches et que vous commencez à les considérer comme des balises (ou des versions), elles commencent à très bien fonctionner. Ils sont très fréquemment utilisés dans le monde C / C ++, et nous avons vendu des packages Python en les utilisant avec beaucoup de succès (mis à part le bogue ci-dessus, bien sûr!).
Pourrait réduire les coûts de réseau / $$ de S3 :)
Comportement prévisible
Je devrais être capable de fairename @ ./some/path
(ou, honnêtement, simplement./some/path
) pour spécifier un package vendu localement dans ma base de code.
Pour la référence URL directe ( name @ ./some/path
), il y a deux endroits où le travail se déroule:
Ce dernier ne serait pas acceptable selon la PEP 508, il serait donc difficile de justifier le soutien et encore moins de le faire fonctionner dans tous les outils.
C'est une très mauvaise expérience utilisateur mélangée à des erreurs ambiguës et fastidieuses.
Comment pouvons-nous améliorer cette situation?
Oops! Remarquez comment, lorsque nous "annulons" le résultat du premier appel d'analyse, notre chemin devient le fichier absolu: /// ...
Je pense que cela est dû au bogue CPython soulevé dans le numéro 22852 - "urllib.parse supprime à tort le #fragment vide,? Query, // netloc"
Ce bogue semble également causer le problème # 3783 - voir ce commentaire .
Quel est le statut de ce problème? Une solution pour résoudre les dépendances locales qui ne sont pas sur PyPI est urgente, par exemple dans le contexte de référentiels monolithiques.
Notez que npm a implémenté cette fonctionnalité de la même manière et que dependencies
peut être spécifié dans package.json
utilisant un chemin local .
Veuillez lire ce commentaire ci-dessus pour savoir ce qui doit se passer avant que cela ait une chance d'être implémenté. pip ne peut rien faire avant cela.
Commentaire le plus utile
D'accord, je vois le problème.
setuptools
,pkg-resources
etpip
utilisent tous des versions légèrement différentes de la bibliothèquepackaging
.Dans
pip
, c'est la version que j'ai montrée ci-dessus.Cependant, dans tout le reste, c'est le suivant (je ne suis pas sûr de savoir lequel est le "plus récent", mais la logique suivante est très limitative et n'est pas entièrement conforme à la RFC 3986 car
file:///
devrait être autorisée, ce qui implique un videnetloc
):🙄
Cela signifie que puisque mon chemin de fichier a
file:///foo/bar
et nonfile://localhost/foo/bar
il échoue.Voici la solution complète :
C'est une très mauvaise expérience utilisateur mélangée à des erreurs ambiguës et fastidieuses.
Comment pouvons-nous améliorer cette situation?