環境
説明
これがsix
バグなのか、Pipのバグなのかわかりません。 six
に属している場合は、すみません。
Pipはinstall_requires
を介してname @ ./some/path
install_requires
ローカルパスを許可しているようですが、URLの解析はひどく壊れています。
この関数では、 urlsplit
を使用して、受信URLの個々のコンポーネントを取得します。
いくつかの入力を行うと、次のようになります。
./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='')
最後の1つでは、 netloc
が空ではなく.
になり、 path
がローカルではなく絶対になります。 これにより、非ローカルパスに関するエラーが発生します。 それはすべて問題ありません。2番目の形式を使用して条件付きロジックを満たすことができます(ただし、実際には最初の形式もサポートする必要があります)。
ただし、他の場所で競合するロジックがあります...
これは、前のロジックを満たしていても失敗するロジックです。
問題を示すテスト関数は次のとおりです。
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)
./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='')
file:
スキームを要求するという最初の関数のロジックを満たしていませんが、すべて問題ありません。
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='')
おっと! 最初の解析呼び出しの結果を「解析解除」すると、 path
が絶対file:///...
なることに注意してください。
これが、2番目に述べたチェックが失敗する理由です-パスはローカルではありません。 これはsix
バグだと思いますが、 scheme in ['file', '']
を許可し、ユーザーに./foo/bar
URIフォームを使用するように指示することで、Pipで軽減できます。
これらの2つの相反するロジックを考えると、 distutils
またはsetuptools
構成のいずれかでinstall_requires
キーのローカルパスを使用することは不可能です。
予想される行動
name @ ./some/path
(または、正直なところ、単に./some/path
)を実行して、コードベースにローカルなベンダーパッケージを指定できるはずです。
再現する方法
#!/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
出力
最初の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
2番目の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'
編集:
RFC 3986が相対パスURIがfile:
スキームでは許可されないことを指定していることがわかったので、技術的にはsix
はfile:./foo/bar
でエラーになるはずです。
ただし、技術的には、setup.pyで次のことができるはずです。
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
install_requires = [
f"name @ file://{PKG_DIR}/foo/bar"
]
ただし、pipは/tmp
にパッケージの「クリーンな」コピーを作成しているように見えるため、 file:///tmp/pip-req-build-9u3z545j/foo/bar
ようなものが得られます。
これをテスト関数で実行すると、2番目の関数の条件が満たされます。
>>> 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='')
そこはすべて良いです。 「unparse」でも同じ結果が得られ、最初の関数の条件付きでnetloc
要件が満たされます。
ただし、2番目の関数のロジックが満たされていても、 Invalid URL
エラーが発生します。
pip
(またはdistutilsやsetuptoolsなど)が出力を飲み込むので、私は先に進み、setup.pyで次のことを行いました。
import os
PKG_DIR = os.path.dirname(os.path.abspath(__file__))
assert False, os.system(f"find {PKG_DIR}")
これは、期待どおりにすべてのファイルがそこにあることを確認します-したがって、ファイルが欠落しているなどの可能性はありません。 上記の"Invalid URL given"
は、コードベースで文字列が表示される唯一の場所です。
現時点では、何が問題なのかわかりません。
さて、問題がわかりました。 setuptools
、 pkg-resources
、およびpip
すべて、わずかに異なるバージョンのpackaging
ライブラリを使用します。
pip
では、上記のバージョンです。
ただし、他のすべてでは、次のようになります(どちらが「新しい」かはわかりませんが、次のロジックは非常に制限されており、RFC 3986に従って、 file:///
を許可する必要があるため、完全には準拠していません。空の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")
🙄
つまり、私のファイルパスにはfile:///foo/bar
ではなくfile://localhost/foo/bar
ため、失敗します。
これが完全な解決策です:
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'
]
)
これは、あいまいで時間の無駄なエラーと混ざり合ったかなり悪いUXです。
どうすればこの状況を改善できますか?
@ Qix-これを見つけてよかった! 私はすべて同じフォーマットを試し、壁に頭を打ちつけていました。 これは、 https://github.com/pypa/pip/issues/6162の代替オプションであり、dependency_linksの非推奨です。
プライベートリポジトリを設定しようとしていますが、独自の内部サーバーがありません。 私たちの解決策は、パッケージをs3に公開し、それらを消費するためにダウンロードし、ローカルフォルダーに配置してから、 install_requires
追加することです。
ローカルパッケージを直感的にインストールする方法から恩恵を受ける他の多くのユースケースがあると確信しています。
@ryanaklein実際には、gitサブモジュールに対する未研究の否定性をすべて無視して試してみることをお勧めします(Gitを使用していると仮定します)。 それらをブランチとして考えるのをやめて、タグ(またはリリース)として考え始めると、それらは本当にうまく機能し始めます。 これらはC / C ++の世界で非常に頻繁に使用されており、Pythonパッケージをかなりうまく使用してベンダー化しています(もちろん、上記のバグは別として)。
ネットワークを削減する可能性があります/ S3の$$コスト:)
予想される行動
name @ ./some/path
(または、正直なところ、単に./some/path
)を実行して、コードベースにローカルなベンダーパッケージを指定できるはずです。
直接URL参照( name @ ./some/path
)の場合、作業が行われている場所は2つあります。
後者はPEP508に従って受け入れられないため、すべてのツールで機能するようにサポートすることを正当化するのは難しいでしょう。
これは、あいまいで時間の無駄なエラーと混ざり合ったかなり悪いUXです。
どうすればこの状況を改善できますか?
この問題の状況はどうなっていますか? PyPIにないローカル依存関係を解決するためのソリューションは、たとえばモノリシックリポジトリのコンテキストで緊急に必要とされています。
npmはこの機能を同様の方法で実装しており、 dependencies
はローカルパスを使用してpackage.json
で指定できることに注意してください。
これが実施される前に何が起こる必要があるかを知るために、上記のこのコメントを参照し
最も参考になるコメント
さて、問題がわかりました。
setuptools
、pkg-resources
、およびpip
すべて、わずかに異なるバージョンのpackaging
ライブラリを使用します。pip
では、上記のバージョンです。ただし、他のすべてでは、次のようになります(どちらが「新しい」かはわかりませんが、次のロジックは非常に制限されており、RFC 3986に従って、
file:///
を許可する必要があるため、完全には準拠していません。空のnetloc
):🙄
つまり、私のファイルパスには
file:///foo/bar
ではなくfile://localhost/foo/bar
ため、失敗します。これが完全な解決策です:
これは、あいまいで時間の無駄なエラーと混ざり合ったかなり悪いUXです。
どうすればこの状況を改善できますか?