Skip to main content

Python package public API management, versioning, and changelog generation

Project description

Pkg Ext

PyPI GitHub codecov Docs

A CLI tool for managing Python package public API, versioning, and changelog generation.

Overview

pkg-ext tracks which symbols (functions, classes, exceptions) in your package are "exposed" (public) vs "hidden" (internal). It:

  • Generates __init__.py with imports and __all__ based on decisions stored in changelog entries
  • Creates group modules (e.g., my_group.py) that re-export related symbols
  • Maintains a structured changelog directory (.changelog/) per PR
  • Bumps version based on changelog action types (make_public=minor, fix=patch, delete/rename=major)
  • Writes a human-readable CHANGELOG.md
  • Provides stability decorators (@experimental, @deprecated) with suppressible warnings
  • Generates _warnings.py in target packages to avoid runtime pkg-ext dependency

Installation

uv pip install pkg-ext
# or
pip install pkg-ext

Core Concepts

Symbol Reference IDs

Symbols are identified by {module_path}.{symbol_name}, e.g., my_pkg.utils.parse_config.

Changelog Actions

Stored in .changelog/{pr_number}.yaml files using Pydantic discriminated unions:

Action Type Description Version Bump Key Fields
make_public Make symbol public Minor group, details
keep_private Keep symbol internal None full_path
fix Bug fix from git commit Patch short_sha, message, changelog_message, ignored
delete Remove from public API Major group
rename Rename with old alias Major group, old_name
breaking_change Breaking API change Major group, details
additional_change Non-breaking change Patch group, details
group_module Assign module to a group None module_path
release Version release marker None old_version
experimental Mark as experimental Patch target, group/parent
ga Graduate to GA Patch target, group/parent
deprecated Mark as deprecated Patch target, group/parent, replacement
max_bump_type Cap version bump None max_bump, reason
chore Internal changes Patch description

All actions inherit common fields: name, ts, author, pr.

The breaking_change and additional_change actions support optional fields for API diff:

  • change_kind: str | None - machine-readable change type (e.g., param_removed, default_changed)
  • auto_generated: bool - true when created by API diff, false for interactive actions
  • field_name: str | None - field name for field-level changes

Stability Targets

Stability actions (experimental, ga, deprecated) support three target levels:

Target Description Required Field
group Entire group name = group name
symbol Single symbol group + name = symbol name
arg Function argument parent = {group}.{symbol}, name = arg name

Public Groups

Groups organize related symbols. Configured in .groups.yaml:

groups:
  - name: __ROOT__  # Top-level exports in __init__.py
    owned_refs: []
    owned_modules: []
  - name: my_group
    owned_refs:
      - my_pkg.utils.parse_config
    owned_modules:
      - my_pkg.utils

When a new symbol is exposed, the tool prompts you to select which group it belongs to. All symbols from the same module go to the same group.

CLI Commands

pkg-ext [OPTIONS] COMMAND

Global Options

Option Description
-p, --path, --pkg-path Package directory path (auto-detected if not provided)
--repo-root Repository root (auto-detected from .git)
--is-bot CI mode: no prompts, fail on missing decisions
--skip-open Skip opening files in editor
--tag-prefix Git tag prefix (e.g., v for v1.0.0)

Command Reference

Category Commands Description Docs
Workflow pre-change, pre-commit, post-merge Development lifecycle commands docs/workflows
Changelog chore, promote, release-notes Changelog management docs/changelog
Stability exp, ga, dep Stability level management docs/stability
API diff-api, dump-api API comparison and export docs/api_commands
Generation gen-docs, gen-examples, gen-tests Generate documentation and scaffolds docs/generate

When to Use Workflow Commands

Scenario Command
Added or removed symbols pre-change
Final validation before commit pre-commit
Single command for everything pre-change --full
CI/CD pipeline pre-commit (bot mode)
  • pre-change handles interactive decisions (expose/hide symbols, delete/rename). Fast because it only generates example and test scaffolds.
  • pre-commit validates all decisions are made (fails in bot mode if prompts needed), syncs generated files, regenerates docs, and runs the dirty check.
  • pre-change --full combines both: runs interactive prompts, generates examples/tests, then syncs files and regenerates docs. The dirty check is skipped since you're still developing.

Configuration

User Config (~/.config/pkg-ext/config.toml)

[user]
editor = "cursor"  # or "code", "vim", etc.
skip_open_in_editor = false

Project Config (pyproject.toml)

[tool.pkg-ext]
tag_prefix = "v"
file_header = "# Generated by pkg-ext"
commit_fix_prefixes = ["fix:", "fix(", "bugfix:", "hotfix:"]
commit_diff_suffixes = [".py", ".pyi"]
changelog_cleanup_count = 30  # Archive when count exceeds this
changelog_keep_count = 10     # Keep this many after cleanup
format_command = ["ruff", "format"]  # ruff check --fix always runs first
max_bump_type = "minor"  # Cap version bumps (patch, minor, major)
# after_file_write_hooks = ["extra-cmd {pkg_path}"]  # Custom post-write hooks

Group Configuration

Define groups with explicit settings in pyproject.toml:

[tool.pkg-ext.groups.my_group]
dependencies = ["__ROOT__"]  # Groups this depends on
docs_exclude = ["internal_helper"]
docstring = "Utilities for common operations"

Note: Stability is not configured here. Use pkg-ext exp/ga/dep CLI commands to manage stability via changelog actions.

Version Bump Limits

For pre-1.0.0 packages where breaking changes are expected, cap the version bump:

Project-level (applies to all PRs):

# pyproject.toml
[tool.pkg-ext]
max_bump_type = "minor"  # All PRs capped to minor

Per-PR override (MaxBumpTypeAction in changelog overrides config):

# .changelog/{pr}.yaml
name: version_cap
type: max_bump_type
max_bump: patch
reason: Documentation-only release
ts: '2026-01-17T14:35:00+00:00'

Dev Mode

The pre-commit command enables dev mode, which writes to -dev suffixed files:

  • .groups-dev.yaml instead of .groups.yaml
  • CHANGELOG-dev.md instead of CHANGELOG.md

This allows iterating on changelog entries during development without modifying the production files. The real files are only updated by post-merge after PR is merged.

Generated Files

Files Updated During PR

File Purpose Editable
.changelog/{pr}.yaml Changelog actions for this PR Yes
.groups-dev.yaml Group assignments (dev copy) No
CHANGELOG-dev.md Human-readable changelog (dev copy) No
{pkg}.api-dev.yaml API dump for dev comparison (gitignored) No
{pkg}/__init__.py Package exports (VERSION unchanged) No
{pkg}/{group}.py Group re-export modules No
{pkg}/_warnings.py Stability warning decorators No
docs/**/*.md API documentation Yes (outside markers)
{group}_examples.py Example scaffolds Yes (outside markers)
{group}_test.py Test scaffolds Yes (outside markers)
  • __init__.py exports are updated but VERSION remains unchanged until release
  • Symbol doc pages include a "Changes" table showing unreleased modifications
  • Content outside === OK_EDIT: pkg-ext ... === markers can be customized and is preserved

Files Updated During Release

These files are updated by post-merge after PR is merged (main branch only):

File What Changes
.groups.yaml Copied from .groups-dev.yaml
CHANGELOG.md Copied from CHANGELOG-dev.md
{pkg}/__init__.py VERSION updated to new version
pyproject.toml Version field updated (if used)
{pkg}.api.yaml Regenerated with new version
docs/**/*.md Unreleased changes become versioned

File Contents Examples

__init__.py:

# Generated by pkg-ext
# flake8: noqa
from my_pkg import my_group
from my_pkg.utils import parse_config

VERSION = "0.1.0"
__all__ = [
    "my_group",
    "parse_config",
]

Group module (my_group.py):

# Generated by pkg-ext
from my_pkg.helpers import helper_func as _helper_func
from my_pkg._warnings import _experimental

helper_func = _experimental(_helper_func)  # With experimental stability

The underscore alias pattern prevents re-export issues with __all__.

_warnings.py:

When any group has non-GA stability, pkg-ext generates a _warnings.py module in the target package (removes runtime dependency on pkg-ext):

class MyPkgWarning(UserWarning): ...
class MyPkgExperimentalWarning(MyPkgWarning): ...
class MyPkgDeprecationWarning(MyPkgWarning, DeprecationWarning): ...

Symbol Detection

The tool parses Python files using AST to find:

  • Functions - Public functions (not starting with _)
  • Classes - Public classes
  • Exceptions - Classes inheriting from Exception or BaseException
  • Type Aliases - Names ending with T
  • Global Variables - UPPERCASE names with 2+ characters

Files skipped:

  • __init__.py, __main__.py (dunder files)
  • *_test.py, test_*.py, conftest.py (test files)
  • Files starting with the configured file_header (already generated)

Automatic Behaviors

Function Argument Exposure

When exposing a function, its type hint arguments are auto-exposed if they reference local package types.

Git Integration

  • Uses GitPython for commit analysis
  • Uses gh CLI to detect PR info
  • Extracts PR number from merge commit message (Merge pull request #123)

API Diff and Breaking Change Detection

During pre-commit, pkg-ext compares {pkg}.api.yaml (baseline from last release) against {pkg}.api-dev.yaml (current code) to detect API changes.

Detected Change Types

Change Breaking? change_kind
Parameter removed Yes param_removed
Required parameter added Yes required_param_added
Parameter type changed Yes param_type_changed
Return type changed Yes return_type_changed
Default removed Yes default_removed
Required field added Yes required_field_added
Field removed Yes field_removed
Base class removed Yes base_class_removed
Base class added No base_class_added
Optional parameter added No optional_param_added
Default added No default_added
Default changed No default_changed
Optional field added No optional_field_added

Auto-Generated Actions

API diff creates BreakingChangeAction or AdditionalChangeAction entries with auto_generated: true. These are:

  • Replaced on each pre-commit run
  • Keyed by (name, group, type, change_kind) for deduplication
  • Timestamps preserved for unchanged changes

Interactive actions (from pre-change) are never replaced.

First Release

When no baseline {pkg}.api.yaml exists, diff is skipped (nothing to compare against).

Limitations

Symbol Detection

  • Type aliases require T suffix - e.g., ConfigT not Config
  • Global vars require UPPERCASE - e.g., DEFAULT_TIMEOUT not default_timeout
  • Exceptions require Error suffix - e.g., ParseError not ParseException
  • No relative import support - Only from pkg.module import ... is tracked

Group Handling

  • One group per module - All symbols from a module belong to the same group
  • Cannot move symbols between groups - Once assigned, module-to-group mapping is fixed
  • Root group always exists - Cannot be removed, used for top-level exports

Git Requirements

  • Requires gh CLI for PR info detection
  • Merge commit format expected - Merge pull request #123 from ...
  • Single remote assumed - Uses first remote for URL

Changelog

  • PR-based storage - Each PR gets one .yaml file
  • No conflict resolution - Manual merge of .changelog/ files needed
  • Archiving by PR number - Old entries archived to .changelog/000/*.yaml

Version Bumping

  • SemVer only - No calendar versioning support
  • pyproject.toml or __init__.py - Version must exist in one of these
  • Pre-release suffixes - Supports rc, a (alpha), b (beta)

Interactive Mode

  • Removed reference handling incomplete - select_ref and select_multiple_ref_state raise NotImplementedError. This breaks rename workflows when symbols are removed.
  • Alias creation not implemented - confirm_create_alias always returns False

Stability

  • Non-callable symbols - Constants and type aliases in experimental/deprecated groups don't emit warnings. @experimental and @deprecated only work on functions and classes.
  • Arg-level only for GA groups - Cannot track arg-level stability changes until group is GA.

API Diff

  • No rename detection - Renames are treated as remove + add (two separate actions)
  • Return types always breaking - No semantic analysis (e.g., returning subclass is flagged as breaking)
  • Factory defaults - Defaults using "..." (factory pattern) may cause false positives

Dependencies

  • ask-shell - Interactive prompts and shell execution
  • model-lib - YAML/TOML parsing and Pydantic models
  • GitPython - Git repository access

Appendix

File Structure

my-repo/
  CHANGELOG.md           # Human-readable changelog
  .groups.yaml           # Group definitions
  .changelog/            # Per-PR changelog actions
    123.yaml             # Actions from PR #123
    000/                 # Archived old entries
      001.yaml
  my_pkg/
    __init__.py          # Generated exports
    my_group.py          # Generated group module
    _warnings.py         # Generated stability module (if needed)
    utils.py             # Source file
    _internal.py         # Private module (ignored)

CI Configuration

GitHub Actions:

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install pkg-ext
      - run: pkg-ext pre-commit

  release:
    if: github.ref == 'refs/heads/main'
    needs: validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: pip install pkg-ext
      - run: pkg-ext post-merge --push

Contributing

See CONTRIBUTING.md for development setup, workflow, and git hooks.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pkg_ext-0.4.0.tar.gz (77.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pkg_ext-0.4.0-py3-none-any.whl (112.1 kB view details)

Uploaded Python 3

File details

Details for the file pkg_ext-0.4.0.tar.gz.

File metadata

  • Download URL: pkg_ext-0.4.0.tar.gz
  • Upload date:
  • Size: 77.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pkg_ext-0.4.0.tar.gz
Algorithm Hash digest
SHA256 ff91f82573a3ae7cd6f107a7064a3cc5e51cbd798b9a21d9fce0f41611d01ff7
MD5 7779cc8e7cafc33d83ce62de45be8172
BLAKE2b-256 43b42e413ae7e0fa7e289dc70a454641cc93006f3bce714139aac9ce49ae4cb8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pkg_ext-0.4.0.tar.gz:

Publisher: release.yaml on EspenAlbert/pkg-ext

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pkg_ext-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: pkg_ext-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 112.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pkg_ext-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 89065e52e243a86e4bc7dbcce8b217ceec165f6bcba11b90ccec343a7c38da95
MD5 0cdee8c0c8290bf60f8b9ef6161971f5
BLAKE2b-256 a649dc1e42b27cb386fc355eacf24ce5bce4212306cdf4836c9453ced4367731

See more details on using hashes here.

Provenance

The following attestation bundles were made for pkg_ext-0.4.0-py3-none-any.whl:

Publisher: release.yaml on EspenAlbert/pkg-ext

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page