Окружающая обстановка
Описание
Я не уверен, ошибка ли это six
или ошибка Пипа. Простите, если он принадлежит six
.
Кажется, что Pip разрешает локальные пути в install_requires
через name @ ./some/path
, но анализ 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='')
Обратите внимание, что последнее приводит к тому, что netloc
становится .
вместо пустого, а path
является абсолютным, а не локальным. Это вызывает ошибку в отношении нелокальных путей. Все в порядке - я могу использовать вторую форму для удовлетворения условной логики (хотя она действительно должна поддерживать и первую).
Однако в другом месте есть противоречивая логика ...
Это логика, которая не работает, даже если мы удовлетворяем предыдущей логике.
Вот тестовая функция, которая показывает проблему:
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:///...
.
Поэтому вторая проверка не проходит - путь не локальный. Я считаю, что это ошибка в six
но ее можно уменьшить в Pip, разрешив scheme in ['file', '']
и указав пользователям использовать форму URI ./foo/bar
.
С учетом этих двух противоречащих друг другу частей логики невозможно использовать локальные пути в ключах install_requires
в конфигурациях distutils
или setuptools
.
Ожидаемое поведение
Я должен иметь возможность сделать 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
Со второго 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 указывает, что относительные URL-адреса пути не разрешены в схеме 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
.
Выполняя это через нашу тестовую функцию, мы удовлетворяем условию второй функции:
>>> 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='')
Там все хорошо. «Непроанализированный» дает тот же результат, и требования netloc
выполняются для условного оператора первой функции.
Однако мы все еще встречаемся с ошибкой 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
) есть два места, где происходит работа:
Последнее было бы неприемлемо для PEP 508, поэтому было бы трудно оправдать поддержку, тем более, чтобы она работала во всех инструментах.
Это довольно плохой UX, смешанный с неоднозначными ошибками, которые тратят время.
Как мы можем исправить эту ситуацию?
Ой! Обратите внимание, как, когда мы «разбираем» результат первого вызова синтаксического анализа, наш путь становится абсолютным файлом: /// ...
Я думаю, это связано с ошибкой CPython, поднятой в проблеме 22852 - «urllib.parse неправильно удаляет пустой #fragment,? Query, // netloc»
Эта ошибка, кажется, также вызывает проблему № 3783 - см. Этот комментарий .
Каков статус этой проблемы? Срочно необходимо решение для разрешения локальных зависимостей, не связанных с PyPI, например, в контексте монолитных репозиториев.
Обратите внимание, что npm реализовал эту функцию аналогичным образом, и dependencies
можно указать в package.json
используя локальный путь .
См. Этот комментарий выше, чтобы узнать, что должно произойти, прежде чем это будет реализовано. pip ничего не может сделать до этого.
Самый полезный комментарий
Хорошо, я вижу проблему.
setuptools
,pkg-resources
иpip
все они используют несколько разные версии библиотекиpackaging
.В
pip
это та версия, которую я показал выше.Однако во всем остальном это следующее (я не уверен, какой из них «новее», но следующая логика очень ограничивает и не полностью соответствует RFC 3986, поскольку
file:///
должно быть разрешено, подразумевая пустойnetloc
):🙄
Это означает, что, поскольку мой путь к файлу имеет
file:///foo/bar
а неfile://localhost/foo/bar
он не работает.Вот полное решение :
Это довольно плохой UX, смешанный с неоднозначными ошибками, которые тратят время.
Как мы можем исправить эту ситуацию?