Skip to content

Commit 7788503

Browse files
hooks: export docs/.requirements.txt too
1 parent ca1d880 commit 7788503

File tree

3 files changed

+119
-9
lines changed

3 files changed

+119
-9
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ repos:
2626
hooks:
2727
- id: generate_requirements.txt
2828
name: Generate requirements.txt
29-
entry: python -m scripts.export_requirements
30-
files: '(pyproject.toml|poetry.lock|requirements.txt|scripts\/export\_requirements\.py)$'
29+
entry: python -m scripts.export_requirements --docs
30+
files: '(pyproject.toml|poetry.lock|requirements.txt|scripts\/export\_requirements\.py|docs\/.requirements.txt)$'
3131
language: python
3232
pass_filenames: false
3333
require_serial: true

docs/.requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
# and readthedocs doesn't support building from poetry
33
# we have to declare our dependencies here
44

5+
mkdocs==1.2.3
6+
mkdocs-material==7.2.6
57
mkdocs-markdownextradata-plugin==0.1.9
6-
mkdocs-material==7.2.4
7-
mkdocs==1.2.2

scripts/export_requirements.py

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@
1313
import pathlib
1414
import re
1515
import textwrap
16-
import typing
1716

1817
import tomli
1918

2019

2120
GENERATED_FILE = pathlib.Path("requirements.txt")
21+
DOC_REQUIREMENTS = pathlib.Path("docs/.requirements.txt")
2222

2323
VERSION_RESTRICTER_REGEX = re.compile(r"(?P<sign>[<>=!]{1,2})(?P<version>\d+\.\d+?)(?P<patch>\.\d+?|\.\*)?")
2424
PLATFORM_MARKERS_REGEX = re.compile(r'sys_platform\s?==\s?"(?P<platform>\w+)"')
2525

26+
PACKAGE_REGEX = re.compile(r"^[^=<>~]+")
2627
# fmt: off
2728
MESSAGE = textwrap.indent(textwrap.dedent(
2829
f"""
@@ -55,7 +56,95 @@ def get_hash(content: dict) -> str:
5556
return hash == get_hash(content)
5657

5758

58-
def main(req_path: os.PathLike, should_validate_hash: bool = True) -> typing.Optional[int]:
59+
def _extract_packages_from_requirements(requirements: str) -> "tuple[set[str],list[str]]":
60+
"""Extract a list of packages from the provided requirements str."""
61+
req = requirements.split("\n")
62+
packages = set()
63+
for i, line in enumerate(req.copy()):
64+
if line.startswith("#"):
65+
continue
66+
if not len(line.strip()):
67+
continue
68+
69+
# requirement files we will be parsing can have `;` or =<>~
70+
match = PACKAGE_REGEX.match(line)
71+
if match is None:
72+
continue
73+
# replace the line with the match
74+
req[i] = match[0].strip()
75+
76+
# replacing `_` with `-` because pypi treats them as the same character
77+
# poetry is supposed to do this, but does not always
78+
package = req[i].lower().replace("_", "-")
79+
80+
packages.add(package)
81+
82+
return packages, req
83+
84+
85+
def _update_versions_in_requirements(requirements: "list[str]", packages: dict) -> str:
86+
"""Update the versions in requirements with the provided package to version mapping."""
87+
for i, package in enumerate(requirements.copy()):
88+
if package.startswith("#"):
89+
continue
90+
if not len(package.strip()):
91+
continue
92+
try:
93+
requirements[i] = package + "==" + packages[package.lower().replace("_", "-")]
94+
except KeyError:
95+
raise AttributeError(f"{package} could not be found in poetry.lock") from None
96+
return "\n".join(requirements)
97+
98+
99+
def _export_doc_requirements(toml: dict, file: pathlib.Path, *packages) -> int:
100+
"""
101+
Export the provided packages versions.
102+
103+
Return values:
104+
0 no changes
105+
1 exported new requirements
106+
2 file does not exist
107+
3 invalid packages
108+
"""
109+
file = pathlib.Path(file)
110+
if not file.exists():
111+
# file does not exist
112+
return 2
113+
114+
with open(file) as f:
115+
contents = f.read()
116+
117+
# parse the packages out of the requirements txt
118+
packages, req = _extract_packages_from_requirements(contents)
119+
120+
# get the version of each package
121+
packages_metadata: dict = toml["package"]
122+
new_versions = {
123+
package["name"]: package["version"]
124+
for package in packages_metadata
125+
if package["name"].lower().replace("_", "-") in packages
126+
}
127+
128+
try:
129+
new_contents = _update_versions_in_requirements(req, new_versions)
130+
except AttributeError as e:
131+
print(e)
132+
return 3
133+
if new_contents == contents:
134+
# don't write anything, just return 0
135+
return 0
136+
137+
with open(file, "w") as f:
138+
f.write(new_contents)
139+
140+
return 1
141+
142+
143+
def main(
144+
req_path: os.PathLike,
145+
should_validate_hash: bool = True,
146+
export_doc_requirements: bool = True,
147+
) -> int:
59148
"""Read and export all required packages to their pinned version in requirements.txt format."""
60149
req_path = pathlib.Path(req_path)
61150

@@ -149,16 +238,24 @@ def main(req_path: os.PathLike, should_validate_hash: bool = True) -> typing.Opt
149238
dependency_lines[k] = line
150239

151240
req_txt += "\n".join(sorted(k + v.rstrip() for k, v in dependency_lines.items())) + "\n"
241+
242+
if export_doc_requirements:
243+
exit_code = _export_doc_requirements(lockfile, DOC_REQUIREMENTS)
244+
else:
245+
exit_code = 0
246+
152247
if req_path.exists():
153248
with open(req_path, "r") as f:
154249
if req_txt == f.read():
155250
# nothing to edit
156-
return 0
251+
# if exit_code is ever removed from here, this should return zero
252+
return exit_code
157253

158254
with open(req_path, "w") as f:
159255
f.write(req_txt)
160256
print(f"Updated {req_path} with new requirements.")
161-
return 1
257+
258+
return 1
162259

163260

164261
if __name__ == "__main__":
@@ -181,6 +278,19 @@ def main(req_path: os.PathLike, should_validate_hash: bool = True) -> typing.Opt
181278
default=GENERATED_FILE,
182279
help="File to export to.",
183280
)
281+
parser.add_argument(
282+
"--docs",
283+
action="store_true",
284+
dest="export_doc_requirements",
285+
default=False,
286+
help="Also export the documentation requirements. Defaults to false.",
287+
)
184288

185289
args = parser.parse_args()
186-
sys.exit(main(args.output_file, should_validate_hash=not args.skip_hash_check))
290+
sys.exit(
291+
main(
292+
args.output_file,
293+
should_validate_hash=not args.skip_hash_check,
294+
export_doc_requirements=args.export_doc_requirements,
295+
)
296+
)

0 commit comments

Comments
 (0)