common-python-tasks 0.3.0__py3-none-any.whl → 0.3.1__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.
@@ -0,0 +1,5 @@
1
+ [git]
2
+ conventional_commits = true
3
+
4
+ [bump]
5
+ breaking_always_bump_major = false
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import re
3
3
  import shutil
4
- from enum import IntEnum, StrEnum, auto
4
+ from enum import StrEnum, auto
5
5
  from pathlib import Path
6
6
  from typing import Any, NamedTuple
7
7
 
@@ -27,14 +27,6 @@ class ReleaseComponent(StrEnum):
27
27
  PATCH = auto()
28
28
 
29
29
 
30
- class ReleaseComponentBump(IntEnum):
31
- """Numeric bump positions expected by `Version.bump`."""
32
-
33
- MAJOR = 0
34
- MINOR = 1
35
- PATCH = 2
36
-
37
-
38
30
  class ReleaseStage(StrEnum):
39
31
  """Supported pre-release stages."""
40
32
 
@@ -105,18 +97,12 @@ def compute_next_release_version(
105
97
  A `NextReleaseVersion` with the serialized new version and normalized component.
106
98
  """
107
99
  current_version_text = project.get_project_version_from_poetry()
108
- release_component = component
109
- if release_component is None:
110
- release_component = infer_bump_component_from_git_cliff()
111
- if re.match(r"^0\.", str(Version.parse(current_version_text).base)):
112
- LOGGER.info(
113
- "Current version %s is pre-production; using patch bump.",
114
- current_version_text,
115
- )
116
- release_component = ReleaseComponent.PATCH
100
+ release_component = (
101
+ infer_bump_component_from_git_cliff() if component is None else component
102
+ )
117
103
 
118
104
  bumped = Version.parse(Version.parse(current_version_text).base).bump(
119
- ReleaseComponentBump[release_component.name]
105
+ list(ReleaseComponent).index(release_component)
120
106
  )
121
107
  if stage is not None:
122
108
  bumped.stage = stage.value
@@ -179,10 +165,8 @@ def infer_bump_component_from_git_cliff() -> ReleaseComponent:
179
165
  "Install git-cliff or pass one of: major, minor, patch."
180
166
  )
181
167
 
182
- result = utils.run_command(
183
- ["git-cliff", "--bumped-version", "--unreleased"],
184
- capture_output=True,
185
- acceptable_returncodes={0},
168
+ result = utils.run_git_cliff(
169
+ ["--bumped-version", "--unreleased"], capture_output=True
186
170
  )
187
171
  version_text = result.stdout.strip()
188
172
  if not version_text:
@@ -166,10 +166,20 @@ def _resolve_allow_dirty_release(dirty_files: list[Path]) -> bool:
166
166
 
167
167
 
168
168
  def _update_release_changelog(release_tag: str, dry_run: bool = False) -> None:
169
- from .utils import log_dry_run, prepend_changelog, run_command
169
+ from .utils import (
170
+ get_prepended_changelog_contents,
171
+ log_dry_run,
172
+ prepend_changelog,
173
+ run_command,
174
+ )
170
175
 
171
176
  changelog_path = Path("CHANGELOG.md")
172
177
  if dry_run:
178
+ LOGGER.debug(
179
+ "Dry-run rendered %s contents:\n%s",
180
+ changelog_path,
181
+ get_prepended_changelog_contents(release_tag, changelog_path),
182
+ )
173
183
  log_dry_run(
174
184
  "Would prepend generated git-cliff release notes for %s to %s",
175
185
  release_tag,
@@ -985,7 +995,7 @@ def changelog() -> None:
985
995
 
986
996
 
987
997
  def _run_release_flow(
988
- component: str = "patch",
998
+ component: str = "auto",
989
999
  *,
990
1000
  stage: str | None = None,
991
1001
  dry_run: bool = False,
@@ -1105,7 +1115,7 @@ def _run_release_flow(
1105
1115
 
1106
1116
  @tasks.script(task_name="release", tags=["packaging", "release", "containers"])
1107
1117
  def release(
1108
- component: str = "patch",
1118
+ component: str = "auto",
1109
1119
  *,
1110
1120
  stage: str | None = None,
1111
1121
  dry_run: bool = False,
@@ -1159,7 +1169,7 @@ def release(
1159
1169
 
1160
1170
  @tasks.script(task_name="release", tags=["packaging", "release", "common"])
1161
1171
  def release_without_containers(
1162
- component: str = "patch",
1172
+ component: str = "auto",
1163
1173
  *,
1164
1174
  stage: str | None = None,
1165
1175
  dry_run: bool = False,
@@ -10,7 +10,7 @@ from getpass import getuser
10
10
  from importlib.resources import files
11
11
  from pathlib import Path
12
12
  from shlex import quote
13
- from typing import Callable, Sequence
13
+ from typing import Callable, Collection, Sequence
14
14
 
15
15
  from jinja2 import Template
16
16
 
@@ -120,7 +120,7 @@ def remove_path(path: Path) -> None:
120
120
  def run_command(
121
121
  command: Sequence[str | Path | None],
122
122
  capture_output: bool = False,
123
- acceptable_returncodes: Sequence[int] | None = None,
123
+ acceptable_returncodes: Collection[int] | None = None,
124
124
  env: dict[str, str] | None = None,
125
125
  always_show_command: bool = False,
126
126
  dry_run: bool = False,
@@ -143,10 +143,9 @@ def run_command(
143
143
  The `CompletedProcess` instance from the subprocess run, or `None` for dry-run
144
144
  invocations.
145
145
  """
146
- import subprocess
147
-
148
146
  if acceptable_returncodes is None:
149
147
  acceptable_returncodes = {0}
148
+ acceptable_returncodes = set(acceptable_returncodes)
150
149
 
151
150
  command = [str(c) for c in command if c is not None]
152
151
 
@@ -208,7 +207,18 @@ def load_data_file(
208
207
  try:
209
208
  data_files = files("common_python_tasks") / "data" / type_identifier
210
209
  data_file = data_files / file_name
211
- return (Path(str(data_file)), data_file.read_text())
210
+ text = data_file.read_text()
211
+ # Try to provide a real filesystem Path when possible (packages in wheels
212
+ # may store resources inside archives). `as_file()` yields a real path
213
+ # via a context manager when supported by the Traversable.
214
+ try:
215
+ with data_file.as_file() as real_path:
216
+ return (Path(real_path), text)
217
+ except (AttributeError, OSError):
218
+ # Fall back to a Path constructed from the Traversable string
219
+ # representation so existing callers that index [0] still receive
220
+ # a Path-like object.
221
+ return (Path(str(data_file)), text)
212
222
  except FileNotFoundError as e:
213
223
  if fatal_on_missing:
214
224
  fatal(f"Data file not found: {file_name} ({e})")
@@ -239,7 +249,7 @@ def get_container_registry_url() -> str:
239
249
 
240
250
 
241
251
  @lru_cache
242
- def get_package_name(use_underscores: bool = False) -> str:
252
+ def get_package_name(use_underscores: bool = False) -> str | None:
243
253
  """Return the package name from environment or pyproject configuration.
244
254
 
245
255
  Args:
@@ -250,9 +260,17 @@ def get_package_name(use_underscores: bool = False) -> str:
250
260
  """
251
261
  import tomllib
252
262
 
253
- name = os.getenv("PACKAGE_NAME") or tomllib.loads(
254
- Path("pyproject.toml").read_text()
255
- ).get("project", {}).get("name")
263
+ name = os.getenv("PACKAGE_NAME")
264
+ if not name:
265
+ try:
266
+ pyproject = Path("pyproject.toml")
267
+ if pyproject.exists():
268
+ name = (
269
+ tomllib.loads(pyproject.read_text()).get("project", {}).get("name")
270
+ )
271
+ except Exception as e: # pragma: no cover - defensive logging
272
+ LOGGER.debug("Unable to read pyproject.toml: %s", e)
273
+
256
274
  if use_underscores and name:
257
275
  name = package_name_to_underscore(name)
258
276
  return name
@@ -340,13 +358,47 @@ def run_git_cliff(
340
358
  Returns:
341
359
  The `CompletedProcess` result, or `None` for dry-run invocations.
342
360
  """
361
+ config_path = get_config_path(
362
+ "GIT_CLIFF_CONFIG",
363
+ "cliff.toml",
364
+ "cliff.toml",
365
+ )
343
366
  return run_command(
344
- ["git-cliff", *args],
367
+ ["git-cliff", "--config", config_path if config_path else None, *args],
345
368
  capture_output=capture_output,
346
369
  acceptable_returncodes={0},
347
370
  )
348
371
 
349
372
 
373
+ def get_prepended_changelog_contents(
374
+ tag_name: str,
375
+ changelog_path: Path = Path("CHANGELOG.md"),
376
+ ) -> str:
377
+ """Return changelog content with release notes prepended for the given tag.
378
+
379
+ Args:
380
+ tag_name: The release tag to generate changelog entries for.
381
+ changelog_path: The changelog file to read as the base content.
382
+
383
+ Returns:
384
+ The full changelog text as it would look after prepending release notes.
385
+ """
386
+ existing_changelog = (
387
+ changelog_path.read_text(encoding="utf-8")
388
+ if changelog_path.exists()
389
+ else "# Changelog\n\n## [Unreleased]\n"
390
+ )
391
+ res = run_git_cliff(["--unreleased", "--tag", tag_name], capture_output=True)
392
+ release_notes = res.stdout if (res and res.stdout) else ""
393
+ if release_notes.strip():
394
+ # Ensure the prepended section is separated from existing content.
395
+ release_notes = release_notes.rstrip("\n") + "\n\n"
396
+ else:
397
+ release_notes = ""
398
+
399
+ return release_notes + existing_changelog
400
+
401
+
350
402
  def prepend_changelog(
351
403
  tag_name: str,
352
404
  changelog_path: Path = Path("CHANGELOG.md"),
@@ -360,25 +412,20 @@ def prepend_changelog(
360
412
  tag_name: The release tag to generate changelog entries for.
361
413
  changelog_path: The path to the changelog file to prepend to.
362
414
  """
363
- if not changelog_path.exists():
364
- changelog_path.write_text("# Changelog\n\n## [Unreleased]\n", encoding="utf-8")
365
- release_notes = run_git_cliff(
366
- ["--unreleased", "--tag", tag_name], capture_output=True
367
- ).stdout
368
- if release_notes and not release_notes.endswith("\n"):
369
- release_notes += "\n"
370
-
371
- existing_changelog = changelog_path.read_text(encoding="utf-8")
372
- changelog_path.write_text(release_notes + existing_changelog, encoding="utf-8")
415
+ changelog_path.write_text(
416
+ get_prepended_changelog_contents(tag_name, changelog_path),
417
+ encoding="utf-8",
418
+ )
373
419
 
374
420
 
375
421
  def changelog() -> str | None:
376
422
  """Generate release notes from git-cliff for unreleased commits."""
377
- result = run_git_cliff(["--unreleased"], capture_output=True).stdout
378
- if not result:
423
+ res = run_git_cliff(["--unreleased"], capture_output=True)
424
+ if not res or not res.stdout:
379
425
  LOGGER.warning("git-cliff produced no release notes for unreleased commits")
380
- return result
426
+ return None
381
427
 
428
+ result = res.stdout
382
429
  return re.sub(
383
430
  r"(?im)\A##\s*\[?unreleased\]?\s*\n?",
384
431
  "",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: common-python-tasks
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Opinionated Poe the Poet tasks for Python package development.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -50,7 +50,7 @@ curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents
50
50
  To install a specific release, set the environment variable `COMMON_PYTHON_TASKS_VERSION`.
51
51
 
52
52
  ```shell
53
- COMMON_PYTHON_TASKS_VERSION=0.3.0 \
53
+ COMMON_PYTHON_TASKS_VERSION=0.3.1 \
54
54
  sh -c "$(curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add-common-python-tasks.sh | jq -r '.content' | base64 -d)"
55
55
  ```
56
56
 
@@ -71,9 +71,9 @@ There's no real reason to run the automated script; I just like automating every
71
71
  ```toml
72
72
  [project]
73
73
  name = "my-awesome-project"
74
- version = "0.3.0"
74
+ version = "0.3.1"
75
75
  dependencies = [
76
- "common-python-tasks==0.3.0", # Always pin to a specific version
76
+ "common-python-tasks==0.3.1", # Always pin to a specific version
77
77
  ]
78
78
 
79
79
  [tool.poe]
@@ -287,8 +287,8 @@ By default, `tasks()` exposes the common task set. You can still include or excl
287
287
  ```toml
288
288
  [project]
289
289
  name = "simple-cli-tool"
290
- version = "0.3.0"
291
- dependencies = ["common-python-tasks==0.3.0"]
290
+ version = "0.3.1"
291
+ dependencies = ["common-python-tasks==0.3.1"]
292
292
 
293
293
  [tool.poe]
294
294
  include_script = "common_python_tasks:tasks()"
@@ -301,8 +301,8 @@ Available tasks: common defaults such as `format`, `lint`, `test`, and `build`.
301
301
  ```toml
302
302
  [project]
303
303
  name = "containerized-app"
304
- version = "0.3.0"
305
- dependencies = ["common-python-tasks==0.3.0"]
304
+ version = "0.3.1"
305
+ dependencies = ["common-python-tasks==0.3.1"]
306
306
 
307
307
  [tool.poe]
308
308
  include_script = "common_python_tasks:tasks(include_tags=['common', 'containers'])"
@@ -319,7 +319,7 @@ Available tasks: All tasks including `build-image` and `push-image`.
319
319
  ```toml
320
320
  [project]
321
321
  name = "custom-test-setup"
322
- dependencies = ["common-python-tasks==0.3.0"]
322
+ dependencies = ["common-python-tasks==0.3.1"]
323
323
  dynamic = ["version"]
324
324
 
325
325
  [tool.poe]
@@ -12,16 +12,17 @@ common_python_tasks/data/generic/.flake8,sha256=PzH98ak-AKcENaBCzOhI2eHvLxrneklm
12
12
  common_python_tasks/data/generic/.isort.cfg,sha256=P_hdco_n95VeXJjlUcXkChqNdct0SNDuk_n9ZaHzXPE,71
13
13
  common_python_tasks/data/generic/Dockerfile.deps.j2,sha256=rCGsBwLHQIRsGFzDbiRi_nWc-SZ3MAFZ_BeZJFV28Ck,432
14
14
  common_python_tasks/data/generic/Dockerfile.j2,sha256=QBWryTMqEPLpPehUyQHAiLCbKFlrC9VjkE4Sv56t1bk,5467
15
+ common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
15
16
  common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
16
17
  common_python_tasks/docker.py,sha256=lS22ecY1rVCkYGPc7hDgXNM-e-TjIW3ce9Um9FOiW7A,17515
17
18
  common_python_tasks/docker_compose.py,sha256=DN4cap0lGxBSCYwEXXSytbq4V5NPVv35e8KyCPTsMhM,20828
18
19
  common_python_tasks/env.py,sha256=G4CF2c3vpUe6qs_WNW41b4IqUnAMBwAuTnUyd8PwExk,13671
19
- common_python_tasks/git.py,sha256=smnZNAjmkmtoZAMCpC8y1IG1WiCA9_JANmIbqmcukgA,11151
20
+ common_python_tasks/git.py,sha256=PCkx6cvWRqarEhDV3aJCPUrjcVxxaZq03ZWY8JqD9Yo,10642
20
21
  common_python_tasks/github.py,sha256=32u4LshDOJpyah8I00c1IKeopNJCWAM8M8Ci2SOdxhQ,18983
21
22
  common_python_tasks/project.py,sha256=SIZRaj0wvPXQbfJOa9jCJJ6F6M_p2QeYjZUMVRsCPjI,10062
22
- common_python_tasks/tasks.py,sha256=UsIQrvhb_DVUKCPe05JN8x1Ti7jFDAziiffg9Eg_SZY,49884
23
- common_python_tasks/utils.py,sha256=6beDRT9Y_gtIRyYO_P6RpOCmGfeu0K34VCu-EZSLey8,12217
24
- common_python_tasks-0.3.0.dist-info/METADATA,sha256=sm81vRaK2M21Id2HB4bTJdRC8ayDqRGxHRXnWhR_tWo,21396
25
- common_python_tasks-0.3.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
26
- common_python_tasks-0.3.0.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
27
- common_python_tasks-0.3.0.dist-info/RECORD,,
23
+ common_python_tasks/tasks.py,sha256=nIIf4HaQoAUSy89ybg1zMNZwxKL1xIYwiCXHnY52uJo,50140
24
+ common_python_tasks/utils.py,sha256=RTVMeNyoIVvnJYsg92yXOVelEXlsLumO9Bgdqyx6BHw,14001
25
+ common_python_tasks-0.3.1.dist-info/METADATA,sha256=VvvfJdJJYUowwh8jm1t704ThsjIdk_Hm_6Z-9xCEUQc,21396
26
+ common_python_tasks-0.3.1.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
27
+ common_python_tasks-0.3.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
28
+ common_python_tasks-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.4.0
2
+ Generator: poetry-core 2.4.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any