Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changelog/030.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: generate
ts: 2026-04-12 16:53:43.865828+00:00
type: fix
author: Espen Albert
changelog_message: 'fix(docs): Filter changelog actions by group to prevent duplicate entries for same-named symbols'
group: generate
message: 'fix(docs): Filter changelog actions by group to prevent duplicate entries for same-named symbols'
short_sha: 03015c
---
name: action_group
ts: 2026-04-12 17:09:03.607681+00:00
type: keep_private
full_path: _internal.changelog.actions.action_group
1 change: 0 additions & 1 deletion .groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ groups:
- _internal.cli.gen_cmds
owned_refs:
- _internal.cli.gen_cmds.gen_docs
- _internal.cli.gen_cmds.gen_tests
- name: stability
owned_modules:
- _internal.cli.stability_cmds
Expand Down
2 changes: 2 additions & 0 deletions pkg_ext/_internal/changelog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
ReleaseAction,
RenameAction,
StabilityTarget,
action_group,
changelog_filepath,
default_changelog_path,
dump_changelog_actions,
Expand All @@ -51,6 +52,7 @@
"ReleaseAction",
"RenameAction",
"StabilityTarget",
"action_group",
"changelog_filepath",
"default_changelog_path",
"dump_changelog_actions",
Expand Down
32 changes: 31 additions & 1 deletion pkg_ext/_internal/changelog/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,19 +141,27 @@ def stable_sort_key(self) -> tuple[str, ...]:

class FixAction(ChangelogActionBase):
type: Literal["fix"] = "fix"
group: str = ""
short_sha: str
message: str
changelog_message: str = ""
rephrased: bool = False
ignored: bool = False

@model_validator(mode="after")
def _backfill_group(self) -> Self:
"""Legacy YAML files store the group in `name`; copy it to `group`."""
if not self.group and self.name:
self.group = self.name
return self

@property
def bump_type(self) -> BumpType:
return BumpType.PATCH

@property
def stable_sort_key(self) -> tuple[str, ...]:
return (self.type, self.short_sha, self.name)
return (self.type, self.group, self.short_sha, self.name)


class DeleteAction(ChangelogActionBase):
Expand Down Expand Up @@ -354,6 +362,28 @@ def stable_sort_key(self) -> tuple[str, ...]:
]


def action_group(action: ChangelogAction) -> str | None:
match action:
case (
MakePublicAction(group=g)
| DeleteAction(group=g)
| RenameAction(group=g)
| BreakingChangeAction(group=g)
| AdditionalChangeAction(group=g)
| FixAction(group=g)
):
return g or None
case (
ExperimentalAction(target=StabilityTarget.group, name=name)
| GAAction(target=StabilityTarget.group, name=name)
| DeprecatedAction(target=StabilityTarget.group, name=name)
):
return name
case ExperimentalAction(group=g) | GAAction(group=g) | DeprecatedAction(group=g):
return g
return None


_changelog_action_adapter: TypeAdapter[ChangelogAction] = TypeAdapter(ChangelogAction)


Expand Down
2 changes: 2 additions & 0 deletions pkg_ext/_internal/changelog/committer.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def fix_changelog_action(commit: GitCommit, ctx: pkg_ctx) -> FixAction | None:
if public_group == SKIPPED:
return FixAction(
name="",
group="",
short_sha=commit_sha,
message=commit_message,
ignored=True,
Expand All @@ -131,6 +132,7 @@ def fix_changelog_action(commit: GitCommit, ctx: pkg_ctx) -> FixAction | None:
prompt_text = f"commit({commit_sha}): {commit_message}"
fix = prompt_for_fix(commit_sha, commit_message, prompt_text)
fix.name = group
fix.group = group
fix.author = commit.author
return fix

Expand Down
1 change: 1 addition & 0 deletions pkg_ext/_internal/changelog/rebase_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def _fix(sha: str, message: str, ts: datetime | None = None, ignored: bool = Fal
short_sha=sha,
message=message,
name="group1",
group="group1",
ts=ts or datetime(2025, 1, 1, tzinfo=UTC),
author="test",
ignored=ignored,
Expand Down
2 changes: 1 addition & 1 deletion pkg_ext/_internal/changelog/write_changelog_md.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _group_changelog_entries(
for action in BumpType.sort_by_bump(actions):
line = as_changelog_line(action, remote_url, ctx)
try:
group: PublicGroup = ctx.action_group(action)
group: PublicGroup = ctx.get_action_group(action)
group_sections[group.name].append(line)
except NoPublicGroupMatch:
other_sections.append(line)
Expand Down
38 changes: 4 additions & 34 deletions pkg_ext/_internal/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,9 @@
from pathlib import Path

from pkg_ext._internal.changelog import (
AdditionalChangeAction,
BreakingChangeAction,
ChangelogAction,
ChangelogActionBase,
DeleteAction,
DeprecatedAction,
ExperimentalAction,
FixAction,
GAAction,
MakePublicAction,
RenameAction,
StabilityTarget,
action_group,
changelog_filepath,
default_changelog_path,
dump_changelog_actions,
Expand Down Expand Up @@ -82,30 +73,9 @@ def pr_changelog_actions(self) -> list[ChangelogAction]:
return parse_changelog_file_path(self.changelog_path)
return self._actions

def action_group(self, action: ChangelogAction) -> PublicGroup:
match action:
case (
MakePublicAction(group=group)
| DeleteAction(group=group)
| RenameAction(group=group)
| BreakingChangeAction(group=group)
| AdditionalChangeAction(group=group)
):
return self.tool_state.groups.get_or_create_group(group)
case (
ExperimentalAction(target=StabilityTarget.symbol, group=group)
| GAAction(target=StabilityTarget.symbol, group=group)
| DeprecatedAction(target=StabilityTarget.symbol, group=group)
):
return self.tool_state.groups.get_or_create_group(group) # type: ignore[arg-type]
case (
ExperimentalAction(target=StabilityTarget.group, name=name)
| GAAction(target=StabilityTarget.group, name=name)
| DeprecatedAction(target=StabilityTarget.group, name=name)
):
return self.tool_state.groups.get_or_create_group(name)
case FixAction(name=group_name):
return self.tool_state.groups.get_or_create_group(group_name)
def get_action_group(self, action: ChangelogAction) -> PublicGroup:
if group_name := action_group(action):
return self.tool_state.groups.get_or_create_group(group_name)
raise NoPublicGroupMatch()

def __enter__(self) -> pkg_ctx:
Expand Down
12 changes: 9 additions & 3 deletions pkg_ext/_internal/generation/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from model_lib import Entity
from zero_3rdparty.sections import slug, wrap_section

from pkg_ext._internal.changelog import actions as changelog_actions_mod
from pkg_ext._internal.changelog.actions import ChangelogAction
from pkg_ext._internal.config import (
PKG_EXT_TOOL_NAME,
Expand Down Expand Up @@ -39,6 +40,7 @@ class GeneratedDocsOutput(Entity):
@dataclass
class SymbolContext:
symbol: SymbolDump
group_name: str = ""
has_env_vars: bool = False
has_meaningful_changes: bool = False
is_primary: bool = False
Expand Down Expand Up @@ -72,11 +74,15 @@ def build_symbol_context(
changelog_actions: list[ChangelogAction],
) -> SymbolContext:
has_changes = any(
action.name == symbol.name and isinstance(action, MEANINGFUL_CHANGE_ACTIONS) for action in changelog_actions
action.name == symbol.name
and isinstance(action, MEANINGFUL_CHANGE_ACTIONS)
and ((ag := changelog_actions_mod.action_group(action)) is None or ag == group_name)
for action in changelog_actions
)
is_primary = symbol.name.lower() == group_name.lower()
return SymbolContext(
symbol=symbol,
group_name=group_name,
has_env_vars=has_env_vars(symbol),
has_meaningful_changes=has_changes,
is_primary=is_primary,
Expand Down Expand Up @@ -140,7 +146,7 @@ def render_group_index(
for ctx in sorted_contexts:
if not ctx.needs_own_page:
section_id = f"{slug(ctx.symbol.name)}_def"
symbol_changes = build_symbol_changes(ctx.symbol.name, changelog_actions_list)
symbol_changes = build_symbol_changes(ctx.symbol.name, changelog_actions_list, ctx.group_name)
example_link = _build_example_link(ctx.symbol.name, group.name, examples_set, examples_dir, "../examples/")
inline_content = render_inline_symbol(
ctx,
Expand Down Expand Up @@ -213,7 +219,7 @@ def generate_docs(
symbol_path = f"{dir_name}/{ctx.page_filename}"
if docs_dir and pkg_src_dir:
symbol_doc_path = docs_dir / symbol_path
symbol_changes = build_symbol_changes(ctx.symbol.name, changelog_actions)
symbol_changes = build_symbol_changes(ctx.symbol.name, changelog_actions, ctx.group_name)
example_link = _build_example_link(
ctx.symbol.name, group.name, examples_set, examples_dir, "../examples/"
)
Expand Down
17 changes: 10 additions & 7 deletions pkg_ext/_internal/generation/docs_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,14 @@ def _build_field_versions(
symbol_name: str,
fields: list[ClassFieldInfo] | None,
changelog_actions: Sequence[ChangelogAction],
group_name: str,
) -> dict[str, str]:
if not fields:
return {}
return {
f.name: v
for f in fields
if not f.is_computed and (v := get_field_since_version(symbol_name, f.name, changelog_actions))
if not f.is_computed and (v := get_field_since_version(symbol_name, f.name, changelog_actions, group_name))
}


Expand Down Expand Up @@ -296,12 +297,14 @@ def calculate_source_link(
return str(rel_path)


def _render_symbol_type_table(symbol: SymbolDump, changelog_actions: Sequence[ChangelogAction]) -> str | None:
def _render_symbol_type_table(
symbol: SymbolDump, changelog_actions: Sequence[ChangelogAction], group_name: str
) -> str | None:
if isinstance(symbol, CLICommandDump) and symbol.cli_params:
if table := render_cli_params_table(symbol.cli_params):
return "\n".join(["**CLI Options:**", "", table])
if isinstance(symbol, ClassDump) and symbol.fields:
field_versions = _build_field_versions(symbol.name, symbol.fields, changelog_actions)
field_versions = _build_field_versions(symbol.name, symbol.fields, changelog_actions, group_name)
if should_show_field_table(symbol.fields, field_versions):
return render_field_table(symbol.fields, field_versions)
return None
Expand All @@ -325,7 +328,7 @@ def render_inline_symbol(
symbol = ctx.symbol
changelog_actions = changelog_actions or []

since_badge = render_since_badge(get_symbol_since_version(symbol.name, changelog_actions))
since_badge = render_since_badge(get_symbol_since_version(symbol.name, changelog_actions, ctx.group_name))
anchor_id = f"{slug(symbol.name)}_def"
lines = [f'<a id="{anchor_id}"></a>\n\n### {symbol.type.value}: `{symbol.name}`']
if symbol_doc_path and pkg_src_dir and pkg_import_name:
Expand All @@ -346,7 +349,7 @@ def render_inline_symbol(
docstring = format_docstring(symbol.docstring)
if docstring:
lines.extend(["", docstring])
if type_table := _render_symbol_type_table(symbol, changelog_actions):
if type_table := _render_symbol_type_table(symbol, changelog_actions, ctx.group_name):
lines.extend(["", type_table])
if changes:
lines.extend(["", _render_changes_content(changes)])
Expand All @@ -363,7 +366,7 @@ def _render_symbol_main_section(
) -> str:
section_id = f"{slug(symbol.name)}_def"
stability = render_stability_badge(symbol.name, group.name, changelog_actions)
since_badge = render_since_badge(get_symbol_since_version(symbol.name, changelog_actions))
since_badge = render_since_badge(get_symbol_since_version(symbol.name, changelog_actions, group.name))
docstring = format_docstring(symbol.docstring)

lines = [f"## {symbol.type.value}: {symbol.name}", f"- [source]({source_link})"]
Expand Down Expand Up @@ -413,7 +416,7 @@ def render_symbol_page(
parts.extend(["", "### CLI Options", "", table])

if isinstance(symbol, ClassDump) and symbol.fields:
field_versions = _build_field_versions(symbol.name, symbol.fields, changelog_actions)
field_versions = _build_field_versions(symbol.name, symbol.fields, changelog_actions, group.name)
if should_show_field_table(symbol.fields, field_versions):
if table := render_field_table(symbol.fields, field_versions):
parts.extend(["", "### Fields", "", table])
Expand Down
2 changes: 1 addition & 1 deletion pkg_ext/_internal/generation/docs_render_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def test_render_inline_symbol_shows_since_badge():
from pkg_ext._internal.generation.docs import SymbolContext

func = _func_dump("my_func")
ctx = SymbolContext(symbol=func)
ctx = SymbolContext(symbol=func, group_name="config")
actions = [
MakePublicAction(name="my_func", group="config", full_path="mod.my_func", ts=datetime(2025, 1, 1, tzinfo=UTC)),
ReleaseAction(name="1.0.0", old_version="0.0.0", ts=datetime(2025, 1, 10, tzinfo=UTC)),
Expand Down
2 changes: 1 addition & 1 deletion pkg_ext/_internal/generation/docs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def test_build_symbol_context_only_make_public_not_complex():

def test_build_symbol_context_fix_action_is_complex():
func = _func_dump("my_func")
action = FixAction(name="my_func", short_sha="abc123", message="fix", ts=datetime.now(UTC))
action = FixAction(name="my_func", group="config", short_sha="abc123", message="fix", ts=datetime.now(UTC))
ctx = build_symbol_context(func, "config", [action])
assert ctx.has_meaningful_changes
assert ctx.needs_own_page
Expand Down
17 changes: 13 additions & 4 deletions pkg_ext/_internal/generation/docs_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from model_lib import Event, fields

from pkg_ext._internal.changelog import actions as changelog_actions_mod
from pkg_ext._internal.changelog.actions import (
AdditionalChangeAction,
BreakingChangeAction,
Expand Down Expand Up @@ -71,9 +72,11 @@ def _matches_symbol_or_group(
return False


def get_symbol_since_version(symbol_name: str, changelog_actions: Sequence[ChangelogAction]) -> str | None:
def get_symbol_since_version(
symbol_name: str, changelog_actions: Sequence[ChangelogAction], group_name: str
) -> str | None:
for action in sorted(changelog_actions):
if isinstance(action, MakePublicAction) and action.name == symbol_name:
if isinstance(action, MakePublicAction) and action.name == symbol_name and action.group == group_name:
if version := find_release_version(action.ts, changelog_actions):
return version
return UNRELEASED_VERSION
Expand All @@ -84,17 +87,19 @@ def get_field_since_version(
symbol_name: str,
field_name: str,
changelog_actions: Sequence[ChangelogAction],
group_name: str,
) -> str | None:
for action in sorted(changelog_actions):
if (
isinstance(action, AdditionalChangeAction)
and action.name == symbol_name
and action.field_name == field_name
and action.group == group_name
):
if version := find_release_version(action.ts, changelog_actions):
return version
return UNRELEASED_VERSION
return get_symbol_since_version(symbol_name, changelog_actions)
return get_symbol_since_version(symbol_name, changelog_actions, group_name)


def _action_description(action: ChangelogAction) -> str:
Expand All @@ -116,13 +121,17 @@ def _action_description(action: ChangelogAction) -> str:
return ""


def build_symbol_changes(symbol_name: str, changelog_actions: Sequence[ChangelogAction]) -> list[SymbolChange]:
def build_symbol_changes(
symbol_name: str, changelog_actions: Sequence[ChangelogAction], group_name: str
) -> list[SymbolChange]:
changes: list[SymbolChange] = []
for action in sorted(changelog_actions):
if isinstance(action, ReleaseAction):
continue
if action.name != symbol_name:
continue
if (ag := changelog_actions_mod.action_group(action)) and ag != group_name:
continue
version = find_release_version(action.ts, changelog_actions) or UNRELEASED_VERSION
if isinstance(action, MakePublicAction):
changes.append(SymbolChange(version=version, description="Made public", ts=action.ts))
Expand Down
Loading
Loading