Skip to content

Commit e4dcc5b

Browse files
feat(init): discover third-party version providers via entry points
Closes #1258. `cz init` previously presented a hard-coded tuple of eight built-in version providers (`commitizen/commands/init.py:_VERSION_PROVIDER_CHOICES`). Third-party providers registered under the `commitizen.provider` entry-point group — already loaded at runtime by `get_provider()` in `commitizen/providers/__init__.py` — were therefore invisible during `cz init`, forcing users to edit the config by hand. This change: - Renames the tuple to `_BUILTIN_VERSION_PROVIDER_OPTIONS` (data only, no `questionary.Choice` objects). - Adds `_construct_version_provider_choices()`, which builds the picker list at call time: built-in providers first (curated descriptions), followed by any third-party providers discovered via `metadata.entry_points(group=PROVIDER_ENTRYPOINT)` that are not already listed as built-ins. - Wires `_ask_version_provider()` to call the new constructor. - Documents the auto-discovery in `docs/config/version_provider.md` so custom-provider authors know `cz init` will surface their plugin once installed. Adds two unit tests: - `test_construct_version_provider_choices_includes_builtins` — asserts all eight built-ins appear with curated titles and no `third-party` suffix. - `test_construct_version_provider_choices_discovers_third_party` — patches `metadata.entry_points` to add a fake plugin and asserts it is appended (and that built-ins registered in the entry-point group are not duplicated under the generic suffix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4d99415 commit e4dcc5b

3 files changed

Lines changed: 134 additions & 37 deletions

File tree

commitizen/commands/init.py

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from importlib import metadata
34
from pathlib import Path
45
from typing import TYPE_CHECKING, Any, NamedTuple
56

@@ -17,6 +18,7 @@
1718
NoAnswersError,
1819
)
1920
from commitizen.git import get_latest_tag_name, get_tag_names, smart_open
21+
from commitizen.providers import PROVIDER_ENTRYPOINT
2022
from commitizen.version_schemes import (
2123
KNOWN_SCHEMES,
2224
VersionProtocol,
@@ -38,45 +40,68 @@ def title(self) -> str:
3840
return f"{self.provider_name}: {self.description}"
3941

4042

41-
_VERSION_PROVIDER_CHOICES = tuple(
42-
questionary.Choice(title=option.title, value=option.provider_name)
43-
for option in (
44-
_VersionProviderOption(
45-
provider_name="commitizen",
46-
description="Fetch and set version in commitizen config (default)",
47-
),
48-
_VersionProviderOption(
49-
provider_name="cargo",
50-
description="Get and set version from Cargo.toml:project.version field",
51-
),
52-
_VersionProviderOption(
53-
provider_name="composer",
54-
description="Get and set version from composer.json:project.version field",
55-
),
56-
_VersionProviderOption(
57-
provider_name="npm",
58-
description="Get and set version from package.json:project.version field",
59-
),
60-
_VersionProviderOption(
61-
provider_name="pep621",
62-
description="Get and set version from pyproject.toml:project.version field",
63-
),
64-
_VersionProviderOption(
65-
provider_name="poetry",
66-
description="Get and set version from pyproject.toml:tool.poetry.version field",
67-
),
68-
_VersionProviderOption(
69-
provider_name="uv",
70-
description="Get and set version from pyproject.toml and uv.lock",
71-
),
72-
_VersionProviderOption(
73-
provider_name="scm",
74-
description="Fetch the version from git and does not need to set it back",
75-
),
76-
)
43+
_BUILTIN_VERSION_PROVIDER_OPTIONS: tuple[_VersionProviderOption, ...] = (
44+
_VersionProviderOption(
45+
provider_name="commitizen",
46+
description="Fetch and set version in commitizen config (default)",
47+
),
48+
_VersionProviderOption(
49+
provider_name="cargo",
50+
description="Get and set version from Cargo.toml:project.version field",
51+
),
52+
_VersionProviderOption(
53+
provider_name="composer",
54+
description="Get and set version from composer.json:project.version field",
55+
),
56+
_VersionProviderOption(
57+
provider_name="npm",
58+
description="Get and set version from package.json:project.version field",
59+
),
60+
_VersionProviderOption(
61+
provider_name="pep621",
62+
description="Get and set version from pyproject.toml:project.version field",
63+
),
64+
_VersionProviderOption(
65+
provider_name="poetry",
66+
description="Get and set version from pyproject.toml:tool.poetry.version field",
67+
),
68+
_VersionProviderOption(
69+
provider_name="uv",
70+
description="Get and set version from pyproject.toml and uv.lock",
71+
),
72+
_VersionProviderOption(
73+
provider_name="scm",
74+
description="Fetch the version from git and does not need to set it back",
75+
),
7776
)
7877

7978

79+
def _construct_version_provider_choices() -> list[questionary.Choice]:
80+
"""Build the version-provider picker for `cz init`.
81+
82+
Built-in providers come first (with curated descriptions), then any
83+
third-party providers that register themselves under the
84+
`commitizen.provider` entry-point group. Third-party providers are
85+
not loaded — only their entry-point name is used for the choice.
86+
"""
87+
builtin_names = {
88+
option.provider_name for option in _BUILTIN_VERSION_PROVIDER_OPTIONS
89+
}
90+
builtin_choices = [
91+
questionary.Choice(title=option.title, value=option.provider_name)
92+
for option in _BUILTIN_VERSION_PROVIDER_OPTIONS
93+
]
94+
third_party_choices = [
95+
questionary.Choice(
96+
title=f"{ep.name}: third-party version provider",
97+
value=ep.name,
98+
)
99+
for ep in metadata.entry_points(group=PROVIDER_ENTRYPOINT)
100+
if ep.name not in builtin_names
101+
]
102+
return [*builtin_choices, *third_party_choices]
103+
104+
80105
class Init:
81106
_PRE_COMMIT_CONFIG_PATH = ".pre-commit-config.yaml"
82107

@@ -254,7 +279,7 @@ def _ask_version_provider(self) -> str:
254279

255280
version_provider: str = questionary.select(
256281
"Choose the source of the version:",
257-
choices=_VERSION_PROVIDER_CHOICES,
282+
choices=_construct_version_provider_choices(),
258283
style=self.cz.style,
259284
default=project_info.get_default_version_provider(),
260285
).unsafe_ask()

docs/config/version_provider.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ setup(
297297
version_provider = "my-provider"
298298
```
299299

300+
Once installed, the provider is also offered as a choice during `cz init`,
301+
labelled `my-provider: third-party version provider`. Built-in providers
302+
keep their curated descriptions and appear first in the picker.
303+
300304
### Provider Implementation Guidelines
301305

302306
When creating a custom provider, keep these guidelines in mind:

tests/commands/test_init_command.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
from importlib import metadata
45
from pathlib import Path
56
from typing import TYPE_CHECKING, Any
67

@@ -483,3 +484,70 @@ def test_construct_name_choice_from_registry(config: BaseConfig):
483484
choices[2].description
484485
== "<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS>"
485486
)
487+
488+
489+
def test_construct_version_provider_choices_includes_builtins():
490+
"""Built-in providers appear first with curated descriptions."""
491+
from commitizen.commands.init import _construct_version_provider_choices
492+
493+
choices = _construct_version_provider_choices()
494+
values = [choice.value for choice in choices]
495+
titles = [choice.title for choice in choices]
496+
497+
# All eight built-ins must be present.
498+
for builtin in (
499+
"commitizen",
500+
"cargo",
501+
"composer",
502+
"npm",
503+
"pep621",
504+
"poetry",
505+
"uv",
506+
"scm",
507+
):
508+
assert builtin in values
509+
510+
# And they must come with their curated descriptions, not the
511+
# generic "third-party version provider" suffix.
512+
assert "commitizen: Fetch and set version in commitizen config (default)" in titles
513+
for title in titles[:8]:
514+
assert "third-party" not in title
515+
516+
517+
def test_construct_version_provider_choices_discovers_third_party(
518+
mocker: MockFixture,
519+
):
520+
"""Third-party providers registered under `commitizen.provider` are appended."""
521+
from commitizen.commands.init import _construct_version_provider_choices
522+
from commitizen.providers import PROVIDER_ENTRYPOINT
523+
524+
real_entry_points = metadata.entry_points
525+
526+
fake_third_party = metadata.EntryPoint(
527+
name="my-third-party",
528+
value="some.module:Provider",
529+
group=PROVIDER_ENTRYPOINT,
530+
)
531+
532+
def fake_entry_points(*args: Any, **kwargs: Any):
533+
eps = real_entry_points(*args, **kwargs)
534+
if kwargs.get("group") == PROVIDER_ENTRYPOINT:
535+
return list(eps) + [fake_third_party]
536+
return eps
537+
538+
mocker.patch(
539+
"commitizen.commands.init.metadata.entry_points",
540+
side_effect=fake_entry_points,
541+
)
542+
543+
choices = _construct_version_provider_choices()
544+
values = [choice.value for choice in choices]
545+
titles = [choice.title for choice in choices]
546+
547+
assert "my-third-party" in values
548+
assert "my-third-party: third-party version provider" in titles
549+
# Built-in `pep621` already lives in the entry-point group; ensure we
550+
# didn't duplicate it once with the curated description and again
551+
# with the generic third-party suffix.
552+
assert values.count("pep621") == 1
553+
assert "pep621: third-party version provider" not in titles

0 commit comments

Comments
 (0)