<p>pip ne prend pas en charge les chemins relatifs dans les références URL directes</p>

Créé le 28 juin 2019  ·  7Commentaires  ·  Source: pypa/pip

Environnement

  • version de pip: 19.1.1
  • Version Python: 3.7
  • Système d'exploitation: MacOS Mojave
N'hésitez pas à ajouter plus d'informations sur votre environnement ici

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.

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

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 ...

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

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.

vendored dependency needs discussion bug

Commentaire le plus utile

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?

Tous les 7 commentaires

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 faire name @ ./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:

  1. pypa / packaging # 120 qui suit le rejet des références URL directes PEP 508 valides
  2. Du côté du pip, nous devons interpréter les chemins relatifs par rapport à quelque chose, j'ai commencé la discussion ici pour obtenir des commentaires sur ce qui a le plus de sens. Nous pourrions suivre l'action éventuelle ici ou dans un nouveau numéro dédié.

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?

5204 devrait aider avec cette question générale d'un point de vue pip.

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.

Cette page vous a été utile?
0 / 5 - 0 notes