Skip to content

v4.0.0

Latest

Choose a tag to compare

@janluke janluke released this 12 Sep 03:22
· 11 commits to master since this release

v4.0.0

This release realigns Cloup with Click 8.5, removes the compatibility layers accumulated across the Click 8.1-8.4 line, and hands a few long-standing Cloup features back to Click now that Click implements them itself. The headline addition is that Cloup now re-exports Click's public top-level namespace, so most applications can import from cloup alone.

Cloup follows semantic versioning rigorously: any known breaking change causes a major-version bump, even when it is unlikely to affect most users. Apart from the new minimum Python and Click versions, most projects should be able to upgrade from Cloup 3.x without changing their code.

This release also includes a comprehensive overhaul of the development tooling. See the final section if you contribute to Cloup.

Dropped support

  • Python >= 3.10 is now required. Python 3.9 reached end-of-life in October 2025 and has been dropped. Cloup is tested on Python 3.10-3.14, with Python 3.14 newly added. (#213)
  • Click >= 8.5.0, < 9.0 is now required (previously >= 8.1.0, < 9.0). The compatibility code for older Click versions has been removed. (#219)

Breaking changes

Apart from the new minimum Python and Click versions, no supported public calling interface has been removed or narrowed. The intentional API-level compatibility changes affect subclasses and method overrides:

  • Command.deprecated and Group.deprecated now accept bool | str. Subclasses that narrow this attribute or the corresponding constructor argument to bool should widen their annotation. (#212)
  • HelpFormatter.write_dl() now accepts an Iterable of rows instead of a Sequence, matching Click. Callers are unaffected because every Sequence is an Iterable, but an override annotated with Sequence is now narrower than the base method. Such overrides should accept Iterable and materialize it first if they need to traverse the rows more than once. (#214)
  • HelpTheme.dark() and HelpTheme.light() are now class methods rather than static methods, and HelpTheme.with_() returns Self. Existing calls are unaffected. Subclasses overriding a preset should use @classmethod to satisfy type checkers and preserve the subclass type.

Some uncommon patterns can observe additional compatibility changes even though no supported function or class calling convention is broken:

  • cloup.Argument is now an alias of click.Argument, so isinstance(arg, cloup.Argument) matches every Click argument. Code that used this check to distinguish Cloup's former argument subclass from a plain Click argument should be updated.
  • from cloup import * no longer binds _version or warnings. They remain available as cloup._version and cloup.warnings, and cloup.__version__ is unchanged.
  • The private alias cloup._params.GroupedOption has been removed. It has not been available as cloup.GroupedOption since v0.14.0 and was never visible to type checkers; use cloup.Option.
  • Command deprecation labels in help output now use Click's uppercase suffix form, (DEPRECATED), instead of the former (Deprecated) prefix. Tests that assert exact help output may need updating.

New Features and enhancements

  • Cloup now re-exports Click's public top-level namespace. Public Click core classes, decorators, exceptions, parameter types, terminal helpers, and utilities are available from cloup and included in cloup.__all__, while Cloup's enhanced implementations intentionally replace the corresponding Click symbols. Click's lazily exposed deprecated names (BaseCommand, MultiCommand, OptionParser, get_binary_stream, and get_text_stream) are mirrored without emitting warnings merely from importing Cloup.

  • Command decorators work without parentheses. @cloup.command is now equivalent to @cloup.command(), matching Click, and the same applies to cloup.group, Group.command, and Group.group.

    @cloup.command
    def cli():
        ...
  • The cls argument can be passed positionally to command and group decorators, matching Click:

    @cloup.command("cli", CustomCommand)
    def cli():
        ...
  • Commands, groups, and arguments accept a string for deprecated. A string produces a custom label such as (DEPRECATED: use new-command instead), while deprecated=True continues to produce (DEPRECATED). (#212, #223)

  • cloup.Argument now delegates to Click. It is an alias of click.Argument, which natively supports argument help and deprecation in Click 8.5. The @cloup.argument decorator remains available with more detailed annotations, and Cloup's positional-argument help section continues to work as before.

  • Subcommand suggestions now use Click's NoSuchCommand. Aliases participate in the suggestions, the exception exposes the possible matches, and message formatting follows Click.

  • A wrapped option-group constraint is now separated from the option list that follows it by a blank line. (#208)

  • ArgumentKwargs and OptionKwargs are new public TypedDicts. They document the standard arguments accepted by @argument and @option, improve IDE completion, and allow type checkers to report misspelled or invalid keyword arguments. Custom parameter classes can still accept their own additional keywords.

  • HelpTheme.dark(), HelpTheme.light(), and HelpTheme.with_() now preserve subclasses. with_() also forwards additional keyword arguments to dataclasses.replace(), allowing subclasses to define and replace their own fields.

Fixes

  • Positional arguments implemented with click.Argument or a custom subclass no longer lose their help text in Cloup's argument help section. (#210, #223)

The following fixes are mostly refinements to Cloup's public typing contract and alignment with Click 8.5. They are unlikely to affect most users:

  • Style remains hashable after it has been called. Previously, its internal cache mutated a frozen dataclass field into an unhashable dictionary. The cache and its private field have been removed. (#224)
  • HelpFormatter.write_dl() signature was aligned to Click one and now accepts any Iterable of rows, not necessarily a Sequence of rows. (#214)
  • Option groups now honor custom Option.get_help_record() implementations that return None, allowing an option subclass to omit itself from help output for reasons other than being hidden.
  • HelpFormatter.write() now matches Click's keyword-compatible write(string="", *strings) signature while preserving Cloup's multi-string convenience.
  • The annotations for Style.fg and Style.bg now accept Click's complete color specification: int | tuple[int, int, int] | str | None. This fixes false type errors for 256-color and truecolor values. (#229)
  • Command and group decorator overloads now infer concrete return types when a custom cls is supplied and expose the complete set of Cloup-specific group options. The formatter_settings annotation no longer advertises a mutable default.
  • Parameter decorators preserve the decorated callback's signature, and Option.group is now visible to static type checkers.
  • get_current_context() now mirrors Click's Literal overload, so silent=False produces a non-optional Context; pass_context() also preserves the wrapped callback's parameter and return types.
  • Section, constraint, formatter, parameter, and command annotations have been aligned more closely with Click 8.5 and with Cloup's actual runtime checks.
  • Types for _params.py are now inline rather than stored in a shadowing _params.pyi. This ensures the implementation itself is type-checked and makes its complete public surface visible to type checkers and generated API documentation.

For contributors

Most of the changes in this release are not user-visible: the development, packaging, documentation, and CI infrastructure has been extensively modernized. CONTRIBUTING.rst has been rewritten for the new workflow and is the best starting point for an existing checkout or open pull request.

Project layout and packaging

  • The package has moved from cloup/ to a src/cloup/ layout. Open pull requests that modify package sources will need to be rebased accordingly. (#220)
  • The build backend has migrated from setuptools to Hatchling, with hatch-vcs replacing setuptools-scm for versioning and a small metadata hook generating the PyPI README. setup.py and setup.cfg have been removed; project metadata and build configuration now live in pyproject.toml. (#206)
  • Development dependencies are scoped to the Hatch environments that use them. Reproducibility-sensitive linting, documentation, and package-checking environments use committed PEP 751 lockfiles: pylock.code.toml, pylock.docs.toml, and pylock.package.toml.
  • _params.pyi has been removed in favor of inline types so static checkers and AutoAPI inspect the same implementation.

Development commands: Tox and Make to Hatch and Task

  • Tox has been replaced by Hatch environments in hatch.toml, and the Makefile has been replaced by Task configuration in taskfile.yml. Run task --list to list the available commands.
  • The test matrix covers Python 3.10-3.14 with the latest supported Click, plus a pinned click == 8.5.* environment on Python 3.14. task test-envs:upgrade-click refreshes Click in existing environments.
  • Aggregate test, typing, coverage, and QA tasks run independent environments concurrently, and each test or typing run prints the resolved Click version.
  • uv is used as the installer for Hatch environments.

Linting and formatting

  • Flake8 has been replaced by Ruff, and the codebase has been reformatted with Ruff.
  • Imports are now sorted and formatted consistently.
  • Ruff's Pyupgrade rules modernized collection, union, and optional annotations; removed deprecated typing imports and unnecessary quoted annotations; replaced percent formatting with f-strings; and organized imports.

Type checking

  • Type checking runs in every compatibility environment rather than only once in the development environment.
  • Pyrefly has been added alongside Mypy. task typing runs Pyrefly first and then Mypy, while the full workflows run both checkers across every compatibility environment.
  • Pyrefly runs in strict mode for package sources and checks the same source, test, and example trees as Mypy. Dynamic, unannotated test helpers retain behavior comparable to Mypy's default mode, and unused Pyrefly suppressions are errors.
  • Concrete overrides of Click and Cloup methods are marked explicitly with @override.
  • Broad casts and blanket suppressions have been removed or narrowed. The remaining exceptions document deliberate differences from Click or cases where Mypy and Pyrefly narrow types differently.
  • Dedicated tests/test_typing.py and tests/test_namespace.py modules protect the public typing contract and Click namespace re-export, including calls expected to be rejected.

CI and coverage

  • The GitHub Actions workflow is now ci.yml, and all YAML files use the .yml extension.
  • Linting, formatting, documentation, and distribution checks run as separate parallel matrix jobs alongside the Python and Click test matrix.
  • Coverage is collected across the entire test matrix, combined into one report, and uploaded to Codecov. The generated _version.py module is excluded from coverage.
  • The Click matrix uses the label latest-supported to reflect Cloup's < 9 upper bound.

Documentation and repository maintenance

  • The documentation dependency stack has been upgraded, including Sphinx, sphinx-autoapi, Furo, and related extensions. sphinx-panels has been replaced by sphinx-design, with accompanying template and CSS fixes.
  • task docs:serve now watches docs/_static/styles, so stylesheet changes trigger live rebuilds.
  • Read the Docs installs the locked documentation environment with uv and remains a separate deployment path from the local Hatch documentation environment. The configuration includes a workaround for pypa/hatch#2372.
  • The installation guide now states Cloup's semantic-versioning policy, and the README describes the Click namespace re-export and Cloup's current feature set.
  • AGENTS.md documents the repository's sources of truth and expected development workflow. YAML naming, TOML editor settings, and local-tool ignore rules have also been standardized.
  • Test-suite warnings exposed by newer Python and pytest versions have been removed.

New Contributors

Full Changelog: v3.1.0...v4.0.0