diff --git a/pip_api/_parse_requirements.py b/pip_api/_parse_requirements.py index 5717da6..250ccae 100644 --- a/pip_api/_parse_requirements.py +++ b/pip_api/_parse_requirements.py @@ -1,22 +1,18 @@ import argparse import ast import os -import re -import traceback import posixpath +import re import string import sys - +import traceback from collections import defaultdict - from typing import Any, Dict, Optional, Union - -from urllib.parse import urljoin, unquote, urlsplit +from urllib.parse import unquote, urljoin, urlsplit from urllib.request import pathname2url, url2pathname from pip_api._vendor import tomli from pip_api._vendor.packaging import requirements, specifiers # type: ignore - from pip_api.exceptions import PipError parser = argparse.ArgumentParser() @@ -293,18 +289,21 @@ def _parse_editable(editable_req): # If a file path is specified with extras, strip off the extras. url_no_extras, extras = _strip_extras(url) + original_url = url_no_extras - if os.path.isdir(url_no_extras): - if not os.path.exists(os.path.join(url_no_extras, "setup.py")): + if os.path.isdir(original_url): + if not os.path.exists(os.path.join(original_url, "setup.py")): raise PipError( "Directory %r is not installable. File 'setup.py' not found." - % url_no_extras + % original_url ) # Treating it as code that has already been checked out url_no_extras = _path_to_url(url_no_extras) if url_no_extras.lower().startswith("file:"): - return _parse_local_package_name(url_no_extras[len("file://") :]), url_no_extras + # NOTE: url_no_extras may contain escaped characters here, meaning that + # it may no longer be a literal package path. So we pass original_url. + return _parse_local_package_name(original_url), url_no_extras if "+" not in url: raise PipError( diff --git a/tests/data/escapable@path/dummyproject_pyproject/pyproject.toml b/tests/data/escapable@path/dummyproject_pyproject/pyproject.toml new file mode 100644 index 0000000..edda1e6 --- /dev/null +++ b/tests/data/escapable@path/dummyproject_pyproject/pyproject.toml @@ -0,0 +1,2 @@ +[project] +name = "dummyproject_pyproject" diff --git a/tests/data/escapable@path/dummyproject_pyproject/setup.py b/tests/data/escapable@path/dummyproject_pyproject/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/tests/data/escapable@path/dummyproject_pyproject/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/test_parse_requirements.py b/tests/test_parse_requirements.py index ab76faa..f78e0c9 100644 --- a/tests/test_parse_requirements.py +++ b/tests/test_parse_requirements.py @@ -218,6 +218,22 @@ def test_parse_requirements_editable_pyprojecttoml(monkeypatch, data): ) +def test_parse_requirements_editable_escaped_path(monkeypatch, data): + files = { + "a.txt": [f"-e {data.join('escapable@path/dummyproject_pyproject')}\n"], + } + monkeypatch.setattr(pip_api._parse_requirements, "_read_file", files.get) + + result = pip_api.parse_requirements("a.txt") + + assert set(result) == {"dummyproject_pyproject"} + assert str(result["dummyproject_pyproject"]).startswith( + "dummyproject_pyproject@ file:///" + ) + # The @ in `escapable@path` should be URL-encoded + assert "escapable%40path" in str(result["dummyproject_pyproject"]) + + def test_parse_requirements_with_relative_references(monkeypatch): files = { "reqs/base.txt": ["django==1.11\n"],