reproducibly 0.0.15__py3-none-any.whl → 0.0.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reproducibly
3
- Version: 0.0.15
4
- Summary: Reproducibly build Python packages
3
+ Version: 0.0.17
4
+ Summary: Reproducibly build Python packages.
5
5
  Author-email: Keith Maxwell <keith.maxwell@gmail.com>
6
6
  Requires-Python: >=3.13
7
7
  Description-Content-Type: text/markdown
8
+ License-Expression: MPL-2.0
8
9
  Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
10
- Requires-Dist: build==1.2.2.post1
11
- Requires-Dist: cibuildwheel==2.23.2
12
- Requires-Dist: packaging==24.2
13
- Requires-Dist: pyproject_hooks==1.2.0
10
+ Requires-Dist: build==1.3.0
11
+ Requires-Dist: cibuildwheel==3.2.1
12
+ Requires-Dist: packaging==25.0
13
+ Requires-Dist: pyproject-hooks==1.2.0
14
14
  Project-URL: Homepage, https://github.com/maxwell-k/reproducibly/
15
15
  Project-URL: Issues, https://github.com/maxwell-k/reproducibly/issues
16
16
 
@@ -40,14 +40,19 @@ from subprocess import run
40
40
 
41
41
  import cog
42
42
 
43
- RESULT = run((".venv/bin/python", "./reproducibly.py", "--help"), text=True, check=True, capture_output=True)
44
- cog.out("\n```\n" + RESULT.stdout + "```\n\n")
43
+ CMD = ".venv/bin/python ./reproducibly.py --help"
44
+
45
+ cog.out("\n")
46
+ cog.out("```\n")
47
+ cog.out(run(CMD.split(), text=True, check=True, capture_output=True).stdout)
48
+ cog.out("```\n")
49
+ cog.out("\n")
45
50
  ]]]-->
46
51
 
47
52
  ```
48
53
  usage: reproducibly.py [-h] [--version] input [input ...] output
49
54
 
50
- Reproducibly build Python packages
55
+ Reproducibly build Python packages.
51
56
 
52
57
  features:
53
58
 
@@ -0,0 +1,5 @@
1
+ reproducibly.py,sha256=G9VtEkfsR0_CLkPWTYhvjXCkhvz42G6HpfLdu-CwhBg,13926
2
+ reproducibly-0.0.17.dist-info/METADATA,sha256=D2UWJPBnhLL0J6fVyj2L5g4-E7mjNiSLTsPgNojpLWg,2975
3
+ reproducibly-0.0.17.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
4
+ reproducibly-0.0.17.dist-info/entry_points.txt,sha256=J4fRzmY7XffHnoo9etzvgeph8np598MPpIy3jpnGlfk,50
5
+ reproducibly-0.0.17.dist-info/RECORD,,
reproducibly.py CHANGED
@@ -1,4 +1,4 @@
1
- """Reproducibly build Python packages
1
+ """Reproducibly build Python packages.
2
2
 
3
3
  features:
4
4
 
@@ -17,8 +17,8 @@ features:
17
17
  import gzip
18
18
  from argparse import ArgumentParser, RawDescriptionHelpFormatter
19
19
  from contextlib import chdir
20
- from datetime import datetime
21
- from enum import auto, Enum, nonmember
20
+ from datetime import datetime, UTC
21
+ from enum import auto, Enum
22
22
  from os import environ, utime
23
23
  from pathlib import Path
24
24
  from shutil import copyfileobj, move
@@ -27,6 +27,7 @@ from subprocess import CalledProcessError, run
27
27
  from sys import version_info
28
28
  from tarfile import TarFile, TarInfo
29
29
  from tempfile import TemporaryDirectory
30
+ from types import TracebackType
30
31
  from typing import cast, Literal, TypedDict
31
32
  from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
32
33
 
@@ -36,31 +37,15 @@ from cibuildwheel.__main__ import build_in_directory
36
37
  from cibuildwheel.options import CommandLineArguments
37
38
  from pyproject_hooks import default_subprocess_runner
38
39
 
39
- # [[[cog import cog ; from pathlib import Path ]]]
40
- # [[[end]]]
41
-
42
- # [[[cog
43
- # import tomllib
44
- # with open("pyproject.toml", "rb") as f:
45
- # pyproject = tomllib.load(f)
46
- # cog.outl("# /// script")
47
- # cog.outl(f'# requires-python = "{pyproject["project"]["requires-python"]}"')
48
- # cog.outl("# dependencies = [")
49
- # for dependency in pyproject["project"]["dependencies"]:
50
- # cog.outl(f"# \"{dependency}\",")
51
- # cog.outl("# ]")
52
- # cog.outl("# ///")
53
- # ]]]
54
40
  # /// script
55
41
  # requires-python = ">=3.13"
56
42
  # dependencies = [
57
- # "build==1.2.2.post1",
58
- # "cibuildwheel==2.23.2",
59
- # "packaging==24.2",
60
- # "pyproject_hooks==1.2.0",
43
+ # "build==1.3.0",
44
+ # "cibuildwheel==3.2.1",
45
+ # "packaging==25.0",
46
+ # "pyproject-hooks==1.2.0",
61
47
  # ]
62
48
  # ///
63
- # [[[end]]]
64
49
 
65
50
 
66
51
  # - Built distributions are created from source distributions
@@ -69,18 +54,21 @@ from pyproject_hooks import default_subprocess_runner
69
54
  # - The default date for this script is the earliest date supported by both
70
55
  # - The minimum date value supported by zip files, is documented in
71
56
  # <https://github.com/python/cpython/blob/3.13/Lib/zipfile.py>.
72
- EARLIEST = datetime(1980, 1, 1, 0, 0, 0).timestamp() # 315532800.0
57
+ EARLIEST = datetime(1980, 1, 1, 0, 0, 0, tzinfo=UTC).timestamp() # 315532800.0
73
58
 
74
59
 
75
- __version__ = "0.0.15"
60
+ __version__ = "0.0.17"
76
61
 
77
62
 
78
63
  def _build(
79
- srcdir: Path, output: Path, distribution: Literal["wheel"] | Literal["sdist"]
64
+ srcdir: Path,
65
+ output: Path,
66
+ distribution: Literal["wheel", "sdist"],
80
67
  ) -> Path:
81
- """Call the build API
68
+ """Call the build API.
82
69
 
83
- Returns the path to the built distribution"""
70
+ Returns the path to the built distribution
71
+ """
84
72
  with DefaultIsolatedEnv() as env:
85
73
  builder = ProjectBuilder.from_isolated_env(
86
74
  env,
@@ -100,14 +88,15 @@ def _extract_to_empty_directory(sdist: Path, directory: str) -> Path:
100
88
 
101
89
 
102
90
  def _cibuildwheel(sdist: Path, output: Path) -> Path:
103
- """Call the cibuildwheel API
91
+ """Call the cibuildwheel API.
104
92
 
105
- Returns the path to the built distribution"""
93
+ Returns the path to the built distribution
94
+ """
106
95
  with (
107
96
  ModifiedEnvironment(
108
97
  CIBW_BUILD_FRONTEND="build",
109
98
  CIBW_CONTAINER_ENGINE="podman",
110
- CIBW_ENVIRONMENT_PASS_LINUX="SOURCE_DATE_EPOCH",
99
+ CIBW_ENVIRONMENT_PASS_LINUX="SOURCE_DATE_EPOCH", # noqa: S106 …_PASS_… is not a password
111
100
  CIBW_ENVIRONMENT="PIP_TIMEOUT=150",
112
101
  ),
113
102
  TemporaryDirectory() as directory,
@@ -121,30 +110,39 @@ def _cibuildwheel(sdist: Path, output: Path) -> Path:
121
110
  build_in_directory(args)
122
111
  wheel = next(args.output_dir.glob("*.whl"))
123
112
  output.joinpath(wheel.name).unlink(missing_ok=True)
124
- path = Path(move(wheel, output))
125
- return path
113
+ return Path(move(wheel, output))
126
114
 
127
115
 
128
116
  class Arguments(TypedDict):
117
+ """Input arguments to reproducibly.py."""
118
+
129
119
  repositories: list[Path]
130
120
  sdists: list[Path]
131
121
  output: Path
132
122
 
133
123
 
134
124
  class ModifiedEnvironment:
135
- """A context manager to temporarily change environment variables"""
125
+ """A context manager to temporarily change environment variables."""
136
126
 
137
- def __init__(self, **kwargs: str | None):
127
+ def __init__(self, **kwargs: str | None) -> None:
128
+ """Initialise with all arguments as environment variables."""
138
129
  self.during: dict[str, str | None] = kwargs
139
130
 
140
- def __enter__(self):
141
- self.before = {key: environ.get(key) for key in self.during.keys()}
131
+ def __enter__(self) -> None:
132
+ """Set the environment variables."""
133
+ self.before = {key: environ.get(key) for key in self.during}
142
134
  self._update(self.during)
143
135
 
144
- def __exit__(self, exc_type, exc_value, exc_traceback):
136
+ def __exit__(
137
+ self,
138
+ exc_type: type[BaseException] | None,
139
+ exc_value: BaseException | None,
140
+ exc_traceback: TracebackType | None,
141
+ ) -> None:
142
+ """Reset environment."""
145
143
  self._update(self.before)
146
144
 
147
- def _update(self, other):
145
+ def _update(self, other: dict[str, str | None]) -> None:
148
146
  for key, value in other.items():
149
147
  if value is None:
150
148
  if key in environ:
@@ -154,19 +152,21 @@ class ModifiedEnvironment:
154
152
 
155
153
 
156
154
  class Builder(Enum):
155
+ """Enum to identifier build package."""
156
+
157
157
  cibuildwheel = auto()
158
158
  build = auto()
159
159
 
160
- @nonmember
161
160
  @staticmethod
162
161
  def which(archive: Path) -> "Builder":
162
+ """Return Builder.cibuildwheel if .c files are present otherwise Build.build."""
163
163
  with TarFile.open(archive, "r:gz") as tar:
164
164
  c = any(i.name.endswith(".c") for i in tar.getmembers())
165
165
  return Builder.cibuildwheel if c else Builder.build
166
166
 
167
167
 
168
168
  def cleanse_sdist(path_: Path, mtime: float) -> int:
169
- """Cleanse a single source distribution
169
+ """Cleanse a single source distribution.
170
170
 
171
171
  - Set all uids and gids to zero
172
172
  - Set all unames and gnames to root
@@ -181,11 +181,11 @@ def cleanse_sdist(path_: Path, mtime: float) -> int:
181
181
  mtime = max(mtime, EARLIEST)
182
182
 
183
183
  with TemporaryDirectory() as path:
184
- with TarFile.open(filename) as tar:
185
- tar.extractall(path=path, filter="data")
184
+ with TarFile.open(filename) as tarfile:
185
+ tarfile.extractall(path=path, filter="data")
186
186
 
187
187
  filename.unlink(missing_ok=True)
188
- (extracted,) = Path(path).iterdir()
188
+ (bare,) = Path(path).iterdir()
189
189
 
190
190
  prefix = path.removeprefix("/") + "/"
191
191
 
@@ -194,53 +194,66 @@ def cleanse_sdist(path_: Path, mtime: float) -> int:
194
194
  tarinfo.uid = tarinfo.gid = 0
195
195
  tarinfo.uname = tarinfo.gname = "root"
196
196
  tarinfo.mode = tarinfo.mode & ~S_IWGRP & ~S_IWOTH
197
- tarinfo.path = tarinfo.path.removeprefix(prefix)
197
+ tarinfo.name = tarinfo.name.removeprefix(prefix)
198
198
  return tarinfo
199
199
 
200
- tar = f"{extracted}.tar"
200
+ tar = f"{bare}.tar"
201
201
  with TarFile.open(tar, "w") as tarfile:
202
- tarfile.add(extracted, filter=filter_)
203
-
204
- with gzip.GzipFile(
205
- filename=filename,
206
- mode="wb",
207
- mtime=mtime,
208
- compresslevel=0,
209
- ) as file:
210
- with open(tar, "rb") as tar:
211
- copyfileobj(tar, file)
202
+ tarfile.add(bare, filter=filter_)
203
+
204
+ with (
205
+ Path(tar).open("rb") as fsrc,
206
+ gzip.GzipFile(
207
+ filename=filename,
208
+ mode="wb",
209
+ mtime=mtime,
210
+ compresslevel=0,
211
+ ) as fdst,
212
+ ):
213
+ copyfileobj(fsrc, fdst)
212
214
  utime(filename, (mtime, mtime))
213
215
  return 0
214
216
 
215
217
 
216
218
  def latest_modification_time(archive: Path) -> str:
217
- """Latest modification time for a gzipped tarfile as a string"""
219
+ """Latest modification time for a gzipped tarfile as a string."""
218
220
  with TarFile.open(archive, "r:gz") as tar:
219
221
  latest = max(member.mtime for member in tar.getmembers())
220
- return "{:.0f}".format(latest)
222
+ return f"{latest:.0f}"
221
223
 
222
224
 
223
225
  def latest_commit_time(repository: Path) -> float:
224
- """Return the time of the last commit to a repository
226
+ """Return the time of the last commit to a repository.
225
227
 
226
228
  As a UNIX timestamp, defined as the number of seconds, excluding leap
227
- seconds, since 01 Jan 1970 00:00:00 UTC."""
229
+ seconds, since 01 Jan 1970 00:00:00 UTC.
230
+ """
228
231
  cmd = ("git", "-C", repository, "log", "-1", "--pretty=%ct")
229
232
  output = run(cmd, check=True, capture_output=True, text=True).stdout
230
233
  return float(output.rstrip("\n"))
231
234
 
232
235
 
233
236
  def breadth_first_key(path: str) -> list[str | list]:
237
+ """Key for sorting breadth first.
238
+
239
+ An example of breadth first sorted strings:
240
+
241
+ 1. z
242
+ 2. a/y
243
+ 3. a/b/x
244
+
245
+ """
234
246
  start, sep, end = path.partition("/")
235
247
  return [sep, start, breadth_first_key(end)] if end else [sep, start]
236
248
 
237
249
 
238
250
  def key(input_: bytes | ZipInfo) -> tuple[int, list[str | list]]:
251
+ """Key for reproducibly sorting ZipFiles."""
239
252
  if hasattr(input_, "filename"):
240
- item = cast(ZipInfo, input_).filename
253
+ item = cast("ZipInfo", input_).filename
241
254
  path = item
242
255
  else:
243
- item = cast(bytes, input_).decode()
256
+ item = cast("bytes", input_).decode()
244
257
  path = item.split(",")[0]
245
258
  if "/RECORD" in path:
246
259
  group = 3
@@ -252,7 +265,7 @@ def key(input_: bytes | ZipInfo) -> tuple[int, list[str | list]]:
252
265
 
253
266
 
254
267
  def fix_zip_members(path: Path, umask: int = 0o022) -> Path:
255
- """Apply fixes to members in a zip file
268
+ """Apply fixes to members in a zip file.
256
269
 
257
270
  Processes the zip file in place. Path is both the source and destination, a
258
271
  temporary working copy is made.
@@ -292,7 +305,7 @@ def _is_git_repository(path: Path) -> bool:
292
305
 
293
306
  try:
294
307
  process = run(
295
- ["git", "rev-parse", "--show-toplevel"],
308
+ ("git", "rev-parse", "--show-toplevel"),
296
309
  cwd=path,
297
310
  check=True,
298
311
  capture_output=True,
@@ -307,6 +320,7 @@ def _is_git_repository(path: Path) -> bool:
307
320
 
308
321
 
309
322
  def parse_args(args: list[str] | None) -> Arguments:
323
+ """Parse command line arguments."""
310
324
  parser = ArgumentParser(
311
325
  prog="reproducibly.py",
312
326
  formatter_class=RawDescriptionHelpFormatter,
@@ -333,7 +347,7 @@ def parse_args(args: list[str] | None) -> Arguments:
333
347
 
334
348
 
335
349
  def _sortwheel(wheel: Path) -> Path:
336
- """Sort the lines in */RECORD and files in a wheel
350
+ """Sort the lines in */RECORD and files in a wheel.
337
351
 
338
352
  pypa/wheel has had reproducible builds since 0.27.0 (2016-02-05); this
339
353
  script post processes a wheel file to match the ordering that pypa/wheel
@@ -351,7 +365,8 @@ def _sortwheel(wheel: Path) -> Path:
351
365
  From observation of pypa/wheel output desired order is below. This can be
352
366
  called breadth first. It is easily created recursively. For a directory,
353
367
  list all the files in order then repeat for all of the subdirectories in
354
- order."""
368
+ order.
369
+ """
355
370
  with TemporaryDirectory() as directory:
356
371
  intermediate = Path(directory) / wheel.name
357
372
  with ZipFile(wheel, "r") as original, ZipFile(intermediate, "w") as destination:
@@ -369,6 +384,7 @@ def _sortwheel(wheel: Path) -> Path:
369
384
 
370
385
 
371
386
  def main(arguments: list[str] | None = None) -> int:
387
+ """Reproducibly build Python packages."""
372
388
  parsed = parse_args(arguments)
373
389
  for repository in parsed["repositories"]:
374
390
  sdist = _build(repository, parsed["output"], "sdist")
@@ -1,5 +0,0 @@
1
- reproducibly.py,sha256=AgMNoWREhLsRyyXuA9BwMJSI99_i9B_ft4RwwilTobQ,13518
2
- reproducibly-0.0.15.dist-info/METADATA,sha256=rOEfFbBwOe1F21wBsSSxht4xNYBMd2Hnf3AcO9wvBvI,2995
3
- reproducibly-0.0.15.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
4
- reproducibly-0.0.15.dist-info/entry_points.txt,sha256=J4fRzmY7XffHnoo9etzvgeph8np598MPpIy3jpnGlfk,50
5
- reproducibly-0.0.15.dist-info/RECORD,,