Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 36 additions & 29 deletions etc/scripts/utils_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"""
Utilities to manage requirements files and call pip.
NOTE: this should use ONLY the standard library and not import anything else
because this is used for boostrapping with no requirements installed.
because this is used for bootstrapping with no requirements installed.
"""


Expand All @@ -31,7 +31,7 @@ def load_requirements(requirements_file="requirements.txt", with_unpinned=False)

def get_required_name_versions(requirement_lines, with_unpinned=False):
"""
Yield required (name, version) tuples given a`requirement_lines` iterable of
Yield required (name, version) tuples given a `requirement_lines` iterable of
requirement text lines. Only accept requirements pinned to an exact version.
"""

Expand All @@ -47,21 +47,26 @@ def get_required_name_versions(requirement_lines, with_unpinned=False):

def get_required_name_version(requirement, with_unpinned=False):
"""
Return a (name, version) tuple given a`requirement` specifier string.
Return a (name, version) tuple given a `requirement` specifier string.
Requirement version must be pinned. If ``with_unpinned`` is True, unpinned
requirements are accepted and only the name portion is returned.

For example:
>>> assert get_required_name_version("foo==1.2.3") == ("foo", "1.2.3")
>>> assert get_required_name_version("fooA==1.2.3.DEV1") == ("fooa", "1.2.3.dev1")
>>> assert get_required_name_version("foo==1.2.3", with_unpinned=False) == ("foo", "1.2.3")
>>> assert get_required_name_version("foo", with_unpinned=True) == ("foo", "")
>>> expected = ("foo", ""), get_required_name_version("foo>=1.2")
>>> assert get_required_name_version("foo>=1.2", with_unpinned=True) == expected
Examples:
>>> get_required_name_version("foo==1.2.3")
('foo', '1.2.3')
>>> get_required_name_version("fooA==1.2.3.DEV1")
('fooa', '1.2.3.dev1')
>>> get_required_name_version("foo==1.2.3", with_unpinned=False)
('foo', '1.2.3')
>>> get_required_name_version("foo", with_unpinned=True)
('foo', '')
>>> get_required_name_version("foo>=1.2", with_unpinned=True)
('foo', '')
>>> try:
... assert not get_required_name_version("foo", with_unpinned=False)
... get_required_name_version("foo", with_unpinned=False)
... except Exception as e:
... assert "Requirement version must be pinned" in str(e)
... "Requirement version must be pinned" in str(e)
True
"""
requirement = requirement and "".join(requirement.lower().split())
if not requirement:
Expand Down Expand Up @@ -95,9 +100,7 @@ def lock_dev_requirements(
"""
Freeze and lock current installed development-only requirements and save
this to the `dev_requirements_file` requirements file. Development-only is
achieved by subtracting requirements from the `main_requirements_file`
requirements file from the current requirements using package names (and
ignoring versions).
achieved by subtracting requirements from the main requirements using names only.
"""
main_names = {n for n, _v in load_requirements(main_requirements_file)}
all_reqs = get_installed_reqs(site_packages_dir=site_packages_dir)
Expand All @@ -112,13 +115,11 @@ def lock_dev_requirements(

def get_installed_reqs(site_packages_dir):
"""
Return the installed pip requirements as text found in `site_packages_dir`
as a text.
Return the installed pip requirements as text found in `site_packages_dir`.
"""
if not os.path.exists(site_packages_dir):
raise Exception(f"site_packages directory: {site_packages_dir!r} does not exists")
# Also include these packages in the output with --all: wheel, distribute,
# setuptools, pip

args = ["pip", "freeze", "--exclude-editable", "--all", "--path", site_packages_dir]
return subprocess.check_output(args, encoding="utf-8") # noqa: S603

Expand All @@ -140,23 +141,29 @@ def get_installed_reqs(site_packages_dir):

def split_req(req):
"""
Return a three-tuple of (name, comparator, version) given a ``req``
requirement specifier string. Each segment may be empty. Spaces are removed.

For example:
>>> assert split_req("foo==1.2.3") == ("foo", "==", "1.2.3"), split_req("foo==1.2.3")
>>> assert split_req("foo") == ("foo", "", ""), split_req("foo")
>>> assert split_req("==1.2.3") == ("", "==", "1.2.3"), split_req("==1.2.3")
>>> assert split_req("foo >= 1.2.3 ") == ("foo", ">=", "1.2.3"), split_req("foo >= 1.2.3 ")
>>> assert split_req("foo>=1.2") == ("foo", ">=", "1.2"), split_req("foo>=1.2")
Return a three-tuple of (name, comparator, version) given a requirement
specifier string.

Examples:
>>> split_req("foo==1.2.3")
('foo', '==', '1.2.3')
>>> split_req("foo")
('foo', '', '')
>>> split_req("==1.2.3")
('', '==', '1.2.3')
>>> split_req("foo >= 1.2.3 ")
('foo', '>=', '1.2.3')
>>> split_req("foo>=1.2")
('foo', '>=', '1.2')
"""
if not req:
raise ValueError("req is required")
# do not allow multiple constraints and tags
if not any(c in req for c in ",;"):
raise Exception(f"complex requirements with : or ; not supported: {req}")

req = "".join(req.split())
if not any(c in req for c in comparators):
return req, "", ""

segments = version_splitter.split(req, maxsplit=1)
return tuple(segments)