Skip to content

Commit df340d2

Browse files
committed
feat: add PEP 751 version provider with pylock.toml support
Introduce Pep751Provider that extends Pep621Provider to update version strings in pylock*.toml lock files alongside pyproject.toml.
1 parent 818fa1a commit df340d2

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

commitizen/providers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from commitizen.providers.composer_provider import ComposerProvider
1111
from commitizen.providers.npm_provider import NpmProvider
1212
from commitizen.providers.pep621_provider import Pep621Provider
13+
from commitizen.providers.pep751_provider import Pep751Provider
1314
from commitizen.providers.poetry_provider import PoetryProvider
1415
from commitizen.providers.scm_provider import ScmProvider
1516
from commitizen.providers.uv_provider import UvProvider
@@ -24,6 +25,7 @@
2425
"ComposerProvider",
2526
"NpmProvider",
2627
"Pep621Provider",
28+
"Pep751Provider",
2729
"PoetryProvider",
2830
"ScmProvider",
2931
"UvProvider",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import tomlkit
6+
from packaging.utils import canonicalize_name
7+
8+
from commitizen.providers.pep621_provider import Pep621Provider
9+
10+
11+
class Pep751Provider(Pep621Provider):
12+
"""
13+
PEP 621 + PEP 751 lockfile awareness
14+
15+
Updates pyproject.toml (via Pep621Provider) and any pylock*.toml
16+
lock files that contain a matching local directory package entry.
17+
"""
18+
19+
lock_patterns: tuple[str, ...] = ("pylock.toml", "pylock.*.toml")
20+
21+
def set_version(self, version: str) -> None:
22+
doc = tomlkit.parse(self.file.read_text())
23+
project_name = canonicalize_name(doc["project"]["name"]) # type: ignore[index,arg-type]
24+
25+
super().set_version(version)
26+
27+
for pattern in self.lock_patterns:
28+
for lock_file in Path().glob(pattern):
29+
lock_doc = tomlkit.parse(lock_file.read_text())
30+
updated = False
31+
for pkg in lock_doc.get("packages", []):
32+
if (
33+
canonicalize_name(pkg.get("name", "")) == project_name
34+
and "directory" in pkg
35+
):
36+
pkg["version"] = version
37+
updated = True
38+
if updated:
39+
lock_file.write_text(tomlkit.dumps(lock_doc))

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ commitizen = "commitizen.providers:CommitizenProvider"
7676
composer = "commitizen.providers:ComposerProvider"
7777
npm = "commitizen.providers:NpmProvider"
7878
pep621 = "commitizen.providers:Pep621Provider"
79+
pep751 = "commitizen.providers:Pep751Provider"
7980
poetry = "commitizen.providers:PoetryProvider"
8081
scm = "commitizen.providers:ScmProvider"
8182
uv = "commitizen.providers:UvProvider"
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
from __future__ import annotations
2+
3+
from textwrap import dedent
4+
from typing import TYPE_CHECKING
5+
6+
import pytest
7+
8+
from commitizen.providers import get_provider
9+
from commitizen.providers.pep751_provider import Pep751Provider
10+
11+
if TYPE_CHECKING:
12+
from pathlib import Path
13+
14+
from commitizen.config.base_config import BaseConfig
15+
16+
PYPROJECT_TOML = """\
17+
[project]
18+
name = "my-package"
19+
version = "0.1.0"
20+
"""
21+
22+
PYPROJECT_TOML_EXPECTED = """\
23+
[project]
24+
name = "my-package"
25+
version = "42.1"
26+
"""
27+
28+
PYLOCK_TOML_WITH_DIRECTORY = """\
29+
lock-version = "1.0"
30+
created-by = "test"
31+
32+
[[packages]]
33+
name = "my-package"
34+
version = "0.1.0"
35+
36+
[packages.directory]
37+
path = "."
38+
editable = true
39+
40+
[[packages]]
41+
name = "some-dep"
42+
version = "1.2.3"
43+
"""
44+
45+
PYLOCK_TOML_WITH_DIRECTORY_EXPECTED = """\
46+
lock-version = "1.0"
47+
created-by = "test"
48+
49+
[[packages]]
50+
name = "my-package"
51+
version = "42.1"
52+
53+
[packages.directory]
54+
path = "."
55+
editable = true
56+
57+
[[packages]]
58+
name = "some-dep"
59+
version = "1.2.3"
60+
"""
61+
62+
PYLOCK_TOML_NON_MATCHING_NAME = """\
63+
lock-version = "1.0"
64+
created-by = "test"
65+
66+
[[packages]]
67+
name = "other-package"
68+
version = "0.1.0"
69+
70+
[packages.directory]
71+
path = "."
72+
editable = true
73+
"""
74+
75+
PYLOCK_TOML_NON_DIRECTORY = """\
76+
lock-version = "1.0"
77+
created-by = "test"
78+
79+
[[packages]]
80+
name = "my-package"
81+
version = "0.1.0"
82+
"""
83+
84+
85+
@pytest.fixture
86+
def pyproject(chdir: Path) -> Path:
87+
file = chdir / "pyproject.toml"
88+
file.write_text(dedent(PYPROJECT_TOML))
89+
return file
90+
91+
92+
def test_get_version(config: BaseConfig, pyproject: Path):
93+
config.settings["version_provider"] = "pep751"
94+
provider = get_provider(config)
95+
assert isinstance(provider, Pep751Provider)
96+
assert provider.get_version() == "0.1.0"
97+
98+
99+
def test_set_version_without_lock_files(
100+
config: BaseConfig, pyproject: Path, chdir: Path
101+
):
102+
config.settings["version_provider"] = "pep751"
103+
provider = get_provider(config)
104+
105+
provider.set_version("42.1")
106+
107+
assert pyproject.read_text() == dedent(PYPROJECT_TOML_EXPECTED)
108+
# No pylock*.toml files should exist
109+
assert list(chdir.glob("pylock*.toml")) == []
110+
111+
112+
def test_set_version_with_pylock_toml(config: BaseConfig, pyproject: Path, chdir: Path):
113+
lock_file = chdir / "pylock.toml"
114+
lock_file.write_text(dedent(PYLOCK_TOML_WITH_DIRECTORY))
115+
config.settings["version_provider"] = "pep751"
116+
provider = get_provider(config)
117+
118+
provider.set_version("42.1")
119+
120+
assert pyproject.read_text() == dedent(PYPROJECT_TOML_EXPECTED)
121+
assert lock_file.read_text() == dedent(PYLOCK_TOML_WITH_DIRECTORY_EXPECTED)
122+
123+
124+
def test_set_version_with_named_lock_file(
125+
config: BaseConfig, pyproject: Path, chdir: Path
126+
):
127+
lock_file = chdir / "pylock.dev.toml"
128+
lock_file.write_text(dedent(PYLOCK_TOML_WITH_DIRECTORY))
129+
config.settings["version_provider"] = "pep751"
130+
provider = get_provider(config)
131+
132+
provider.set_version("42.1")
133+
134+
assert pyproject.read_text() == dedent(PYPROJECT_TOML_EXPECTED)
135+
assert lock_file.read_text() == dedent(PYLOCK_TOML_WITH_DIRECTORY_EXPECTED)
136+
137+
138+
def test_set_version_non_matching_package_not_updated(
139+
config: BaseConfig, pyproject: Path, chdir: Path
140+
):
141+
lock_file = chdir / "pylock.toml"
142+
lock_file.write_text(dedent(PYLOCK_TOML_NON_MATCHING_NAME))
143+
original_lock_content = lock_file.read_text()
144+
config.settings["version_provider"] = "pep751"
145+
provider = get_provider(config)
146+
147+
provider.set_version("42.1")
148+
149+
assert pyproject.read_text() == dedent(PYPROJECT_TOML_EXPECTED)
150+
# Lock file should be unchanged since no matching package name
151+
assert lock_file.read_text() == original_lock_content
152+
153+
154+
def test_set_version_non_directory_source_not_updated(
155+
config: BaseConfig, pyproject: Path, chdir: Path
156+
):
157+
lock_file = chdir / "pylock.toml"
158+
lock_file.write_text(dedent(PYLOCK_TOML_NON_DIRECTORY))
159+
original_lock_content = lock_file.read_text()
160+
config.settings["version_provider"] = "pep751"
161+
provider = get_provider(config)
162+
163+
provider.set_version("42.1")
164+
165+
assert pyproject.read_text() == dedent(PYPROJECT_TOML_EXPECTED)
166+
# Lock file should be unchanged since package has no directory source
167+
assert lock_file.read_text() == original_lock_content
168+
169+
170+
def test_set_version_multiple_lock_files(
171+
config: BaseConfig, pyproject: Path, chdir: Path
172+
):
173+
lock_file1 = chdir / "pylock.toml"
174+
lock_file1.write_text(dedent(PYLOCK_TOML_WITH_DIRECTORY))
175+
lock_file2 = chdir / "pylock.dev.toml"
176+
lock_file2.write_text(dedent(PYLOCK_TOML_WITH_DIRECTORY))
177+
config.settings["version_provider"] = "pep751"
178+
provider = get_provider(config)
179+
180+
provider.set_version("42.1")
181+
182+
assert pyproject.read_text() == dedent(PYPROJECT_TOML_EXPECTED)
183+
assert lock_file1.read_text() == dedent(PYLOCK_TOML_WITH_DIRECTORY_EXPECTED)
184+
assert lock_file2.read_text() == dedent(PYLOCK_TOML_WITH_DIRECTORY_EXPECTED)
185+
186+
187+
def test_provider_registration(config: BaseConfig, pyproject: Path):
188+
config.settings["version_provider"] = "pep751"
189+
provider = get_provider(config)
190+
assert isinstance(provider, Pep751Provider)

0 commit comments

Comments
 (0)