-
-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathutils_requirements.py
More file actions
162 lines (139 loc) · 6.23 KB
/
utils_requirements.py
File metadata and controls
162 lines (139 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# ScanCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/aboutcode-org/skeleton for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
import os
import re
import subprocess
"""
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.
"""
def load_requirements(requirements_file="requirements.txt", with_unpinned=False):
"""
Yield package (name, version) tuples for each requirement in a `requirement`
file. Only accept requirements pinned to an exact version.
"""
with open(requirements_file) as reqs:
req_lines = reqs.read().splitlines(False)
return get_required_name_versions(req_lines, with_unpinned=with_unpinned)
def get_required_name_versions(requirement_lines, with_unpinned=False):
"""
Yield required (name, version) tuples given a`requirement_lines` iterable of
requirement text lines. Only accept requirements pinned to an exact version.
"""
for req_line in requirement_lines:
req_line = req_line.strip()
if not req_line or req_line.startswith("#"):
continue
if req_line.startswith("-") or (not with_unpinned and "==" not in req_line):
print(f"Requirement line is not supported: ignored: {req_line}")
continue
yield get_required_name_version(requirement=req_line, with_unpinned=with_unpinned)
def get_required_name_version(requirement, with_unpinned=False):
"""
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
>>> try:
... assert not get_required_name_version("foo", with_unpinned=False)
... except Exception as e:
... assert "Requirement version must be pinned" in str(e)
"""
requirement = requirement and "".join(requirement.lower().split())
if not requirement:
raise ValueError(f"specifier is required is empty:{requirement!r}")
name, operator, version = split_req(requirement)
if not name:
raise ValueError(f"Name is required: {requirement}")
is_pinned = operator == "=="
if with_unpinned:
version = ""
else:
if not is_pinned and version:
raise ValueError(f"Requirement version must be pinned: {requirement}")
return name, version
def lock_requirements(requirements_file="requirements.txt", site_packages_dir=None):
"""
Freeze and lock current installed requirements and save this to the
`requirements_file` requirements file.
"""
with open(requirements_file, "w") as fo:
fo.write(get_installed_reqs(site_packages_dir=site_packages_dir))
def lock_dev_requirements(
dev_requirements_file="requirements-dev.txt",
main_requirements_file="requirements.txt",
site_packages_dir=None,
):
"""
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).
"""
main_names = {n for n, _v in load_requirements(main_requirements_file)}
all_reqs = get_installed_reqs(site_packages_dir=site_packages_dir)
all_req_lines = all_reqs.splitlines(False)
all_req_nvs = get_required_name_versions(all_req_lines)
dev_only_req_nvs = {n: v for n, v in all_req_nvs if n not in main_names}
new_reqs = "\n".join(f"{n}=={v}" for n, v in sorted(dev_only_req_nvs.items()))
with open(dev_requirements_file, "w") as fo:
fo.write(new_reqs)
def get_installed_reqs(site_packages_dir):
"""
Return the installed pip requirements as text found in `site_packages_dir`
as a text.
"""
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
comparators = (
"===",
"~=",
"!=",
"==",
"<=",
">=",
">",
"<",
)
_comparators_re = r"|".join(comparators)
version_splitter = re.compile(rf"({_comparators_re})")
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")
"""
if not req:
raise ValueError("req is required")
# do not allow multiple constraints and tags
if 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)