Skip to content

feat(init): discover third-party version providers via entry points#1971

Closed
bearomorphism wants to merge 2 commits intocommitizen-tools:masterfrom
bearomorphism:feat/1258-init-discover-providers
Closed

feat(init): discover third-party version providers via entry points#1971
bearomorphism wants to merge 2 commits intocommitizen-tools:masterfrom
bearomorphism:feat/1258-init-discover-providers

Conversation

@bearomorphism
Copy link
Copy Markdown
Collaborator

@bearomorphism bearomorphism commented May 9, 2026

Description

Closes #1258.

Why

cz init interactively asks the user to pick a version_provider. The picker's options were hard-coded to the eight built-in providers (commitizen, cargo, composer, npm, pep621, poetry, uv, scm), even though commitizen already supports third-party providers via the commitizen.provider entry-point group at runtime (commitizen/providers/__init__.py:get_provider). The mismatch meant a user who installed e.g. commitizen-deno-provider had to either know the provider name from memory and hand-edit the config, or skip cz init entirely. The original reporter offered the implementation sketch.

What changed

File Change
commitizen/commands/init.py _VERSION_PROVIDER_CHOICES (a tuple of questionary.Choice) is replaced by _BUILTIN_VERSION_PROVIDER_OPTIONS (data-only) and a new _construct_version_provider_choices() helper that builds the picker list at call time. _ask_version_provider() now invokes that helper.
tests/commands/test_init_command.py Three new tests: built-ins-only, third-party-discovered, duplicate-third-party-deduped.
docs/config/version_provider.md One-paragraph note in the "Custom Provider Plugins" section explaining that cz init now surfaces the plugin once installed.

How it works

  1. Built-ins still come first, with curated descriptions. The _BUILTIN_VERSION_PROVIDER_OPTIONS tuple keeps the human-readable labels (e.g. "pep621: Get and set version from pyproject.toml:project.version field") instead of falling back to a generic suffix.
  2. Third-party providers are discovered, never loaded. Only ep.name is read; ep.load() is not called, so installing a malicious cz_* plugin can't execute its module just because the user opened cz init.
  3. Two layers of dedup, both necessary:
    • The eight built-ins are themselves registered under [project.entry-points."commitizen.provider"] in pyproject.toml:73-81. A naive listing would yield each one twice. We seed seen_names with the built-in set and skip any entry-point name already in it.
    • Two distributions can register a third-party provider under the same name (e.g. two unrelated forks both publishing cz_jira). After the first occurrence is added we mark the name seen and skip the rest. We deliberately don't try to merge the duplicates: commitizen.providers.get_provider() raises VersionProviderUnknown when more than one entry point matches, so the user picking the duplicated name gets a clear error instead of the picker silently choosing whichever distribution loaded first.
  4. No registry caching. Each cz init invocation re-reads the entry-point group, so a provider installed mid-shell-session is picked up immediately.

Backward compatibility

  • Users without any third-party plugin see exactly the same picker as v4.15.1 (same eight built-ins, same order, same titles).
  • The function signature change is internal (_VERSION_PROVIDER_CHOICES was a leading-underscore module global, never exported).

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce (3 new tests, see the targeted commands below)
  • Run uv run poe all locally to ensure this change passes linter check and tests (poe lint clean; 33/33 init tests pass)
  • Manually test the changes (see "Steps to Test" below)
  • Update the documentation for the changes

Documentation Changes

  • Added a paragraph about auto-discovery to docs/config/version_provider.md
  • Run uv run poe doc locally to ensure the documentation pages render correctly
  • Check and fix any broken links (internal or external)

Expected Behavior

Scenario Outcome
cz init with no third-party providers installed Same eight built-in choices, same order, same descriptions as before.
cz init with a third-party plugin installed (e.g. commitizen-deno-provider exposing commitizen.provider -> deno) The eight built-ins appear first, then deno: third-party version provider. Selecting it writes version_provider = "deno" to pyproject.toml.
Two installed distributions both register commitizen.provider -> dup dup appears once in the picker. Selecting it surfaces the existing VersionProviderUnknown error from get_provider() so the conflict is loud, not silent.

Steps to Test This Pull Request

git fetch fork feat/1258-init-discover-providers
git checkout fork/feat/1258-init-discover-providers

# 1. Built-ins-only test (no plugins installed): all eight present, none labelled third-party.
uv run pytest tests/commands/test_init_command.py::test_construct_version_provider_choices_includes_builtins -v

# 2. Third-party-discovered test: a fake EntryPoint("my-third-party") is patched in.
uv run pytest tests/commands/test_init_command.py::test_construct_version_provider_choices_discovers_third_party -v

# 3. Duplicate-third-party-deduped test: two EntryPoint(name="duplicated") -> only one choice.
uv run pytest tests/commands/test_init_command.py::test_construct_version_provider_choices_dedupes_duplicate_third_party -v

# 4. Full init suite (32 pre-existing + 1 new dedup test).
uv run pytest tests/commands/test_init_command.py -q
# expected: 33 passed

Additional Context

Surfaced while triaging open issues in #1965. The implementation follows the original reporter's sketch but uses a single discover-on-call function and dedupes against the built-in list rather than building a separate registry. The duplicate-name handling was added in fixup commit 1bad9047 after a GitHub Copilot review pass flagged it as a potential UX gap.

Closes commitizen-tools#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>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.24%. Comparing base (4b93a50) to head (1bad904).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1971   +/-   ##
=======================================
  Coverage   98.23%   98.24%           
=======================================
  Files          61       61           
  Lines        2779     2792   +13     
=======================================
+ Hits         2730     2743   +13     
  Misses         49       49           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves cz init by dynamically discovering installed version providers registered under the commitizen.provider entry-point group, so third-party providers become selectable during interactive initialization (while keeping built-ins first with curated descriptions).

Changes:

  • Replace the hard-coded version provider questionary.Choice tuple with a built-in options tuple and a call-time choice constructor.
  • Discover and append third-party provider entry points (deduped against built-ins) to the cz init version provider picker.
  • Add tests and a short documentation note describing the auto-discovery behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
commitizen/commands/init.py Builds the version-provider selection list at runtime, appending third-party entry points after built-ins.
tests/commands/test_init_command.py Adds unit tests for built-in ordering/labels and third-party discovery via patched entry points.
docs/config/version_provider.md Documents that third-party providers will appear in cz init once installed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commitizen/commands/init.py Outdated
Comment on lines +94 to +100
third_party_choices = [
questionary.Choice(
title=f"{ep.name}: third-party version provider",
value=ep.name,
)
for ep in metadata.entry_points(group=PROVIDER_ENTRYPOINT)
if ep.name not in builtin_names
Comment on lines +517 to +523
def test_construct_version_provider_choices_discovers_third_party(
mocker: MockFixture,
):
"""Third-party providers registered under `commitizen.provider` are appended."""
from commitizen.commands.init import _construct_version_provider_choices
from commitizen.providers import PROVIDER_ENTRYPOINT

Address GitHub Copilot review feedback on PR commitizen-tools#1971: when two
distributions register a third-party provider under the same name in
the `commitizen.provider` entry-point group, the user previously saw
two ambiguous choices in `cz init`. Track names in a `seen_names`
set (seeded with the built-in names) and skip subsequent matches so
each name appears at most once.

The conflict is not silenced — `commitizen.providers.get_provider`
already raises `VersionProviderUnknown` if more than one entry point
shares the name, so the user gets a clear error if they pick the
duplicated provider, instead of a confusing UI showing two identical
choices that resolve the same way.

Add `test_construct_version_provider_choices_dedupes_duplicate_third_party`
which simulates two `EntryPoint(name="duplicated", ...)` values and
asserts only one appears in the choice list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bearomorphism
Copy link
Copy Markdown
Collaborator Author

Closing this PR per maintainer-triage policy: feature-request issues should sit with the maintainers for design / scope review before any implementation lands. The issue's type: feature (or implicit equivalent) label means it's not on a "ready-to-implement" track yet.

The implementation itself is preserved on the branch (feat/1258-init-discover-providers) — if a maintainer decides this is the direction they want, the PR can be re-opened in one click, or the work can serve as a starting point for a maintainer-led design.

This PR is being closed so that #1258 reverts to "awaiting maintainer triage / decision" rather than "PR pending review", which is the correct state for a feature request.

Closed via the round-2 triage cleanup in #1965.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scan for providers using metadata.entry_points in init

2 participants