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
9 changes: 8 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,16 @@ Click itself:
type supported keyword arguments individually. The settings helpers remain specific
to Cloup

Cloup is **statically type-checked** with MyPy in strict mode and extensively **tested**
Cloup is **statically type-checked** with MyPy and Pyrefly and extensively **tested**
against multiple versions of Python with nearly 100% coverage.

Does it replace Click?
======================

Since version 4.0.0, Cloup makes an effort to re-export the public symbols in Click's
top-level namespace. Applications using Cloup can therefore access most Click
functionality through ``cloup`` without maintaining separate top-level imports from
both packages.

A simple example
================
Expand Down
35 changes: 8 additions & 27 deletions docs/pages/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,13 @@ To install the latest stable release, run::

pip install cloup

Cloup adheres to `semantic versioning <https://semver.org/>`_.
Depending on Cloup
------------------

Depending on Cloup: recommendations
-----------------------------------
Cloup follows `semantic versioning <https://semver.org/>` quite rigorously. Patch and
minor releases are guaranteed to be backward-compatible. Any known breaking change,
even one unlikely to affect most users, causes a major version bump. Therefore, a new
major release does not necessarily require changes to your code.

1. Pin Cloup version
~~~~~~~~~~~~~~~~~~~~
I probably don't need to explain this, but make sure you pin the version you
are using in your requirements file. Dependency management tools like Poetry
will do this automatically but if you still use ``requirements.txt`` or
``setup.py``, you can do it like following:

.. parsed-literal::

cloup ~= \ |release|\

Patch releases are guaranteed to be backward-compatible even before v1.0.
At each new release, you can check the :doc:`changelog` to see what's changed.

2. Add Click to your requirements too
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Cloup is not a replacement for Click.

- Cloup reimplements or just re-exports many Click symbols but not *all*. You
may still need to import click for some stuff.

- Cloup doesn't force you to use a specific version of Click; it only
specifies a range of supported versions; that's an enough reason to add Click
to your dependencies: to have control on its version as well.
Release notes are published on
`GitHub releases <https://github.com/janluke/cloup/releases>`.
4 changes: 1 addition & 3 deletions examples/flat_option_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
Example of option groups, "flat style".
"""

import click

import cloup
from cloup import OptionGroup, option
from cloup.constraints import If, RequireAtLeast, mutually_exclusive
Expand All @@ -30,7 +28,7 @@
@_output.option("--six", help="3rd output option")
# Other options
@option(
"--seven", help="first uncategorized option", type=click.Choice("yes no ask".split())
"--seven", help="first uncategorized option", type=cloup.Choice(["yes", "no", "ask"])
)
@option("--height", help="second uncategorized option")
def main(**kwargs):
Expand Down
4 changes: 1 addition & 3 deletions examples/option_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

from pprint import pprint

from click import Choice

import cloup
from cloup import option, option_group
from cloup.constraints import (
Expand Down Expand Up @@ -43,7 +41,7 @@
option("--six", help="a 6th cool option"),
constraint=If("three", then=RequireExactly(1)), # conditional constraint
)
@option("--seven", help="an uncategorized option", type=Choice(["foo", "bar"]))
@option("--seven", help="an uncategorized option", type=cloup.Choice(["foo", "bar"]))
@option("--eight", help="second uncategorized option")
# Usage of @constraint
@constraint(mutually_exclusive, ["one", "two"])
Expand Down
107 changes: 104 additions & 3 deletions src/cloup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Top-level package for cloup."""

from typing import TYPE_CHECKING

import click as _click

# WARNING: _version.py is generated by hatch-vcs upon package building/installation
from . import _version

Expand All @@ -9,13 +13,46 @@
__version_tuple__ = _version.version_tuple

from click import (
# core
Argument,
CommandCollection,
Parameter,
ParameterSource,
# decorators
confirmation_option,
custom_version_option,
help_option,
make_pass_decorator,
pass_obj,
password_option,
version_option,
# exceptions
Abort,
BadArgumentUsage,
BadOptionUsage,
BadParameter,
ClickException,
FileError,
MissingParameter,
NoSuchCommand,
NoSuchOption,
UsageError,
# formatting
wrap_text,
# termui
clear,
confirm,
echo_via_pager,
edit,
get_pager_file,
getchar,
launch,
pause,
progressbar,
prompt,
secho,
style,
unstyle,
# types
BOOL,
Choice,
Expand All @@ -31,9 +68,37 @@
Tuple,
UNPROCESSED,
UUID,
# utils
echo,
format_filename,
get_app_dir,
open_file,
)

from . import warnings
# Click exposes these deprecated names lazily. Mirror that behavior so importing
# Cloup does not emit their deprecation warnings.
if TYPE_CHECKING:
from click.core import _BaseCommand as BaseCommand
from click.core import _MultiCommand as MultiCommand
from click.parser import _OptionParser as OptionParser
from click.utils import _get_binary_stream as get_binary_stream
from click.utils import _get_text_stream as get_text_stream
else:
_deprecated_click_names = {
"BaseCommand",
"MultiCommand",
"OptionParser",
"get_binary_stream",
"get_text_stream",
}

def __getattr__(name: str) -> object:
if name in _deprecated_click_names:
return getattr(_click, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


from . import warnings as warnings
from .styling import (
HelpTheme,
Style,
Expand Down Expand Up @@ -68,27 +133,42 @@
from .types import dir_path, file_path, path

__all__ = [
"Abort",
"Argument",
"BOOL",
"BadArgumentUsage",
"BadOptionUsage",
"BadParameter",
"BaseCommand",
"Choice",
"ClickException",
"Color",
"Command",
"CommandCollection",
"ConstraintMixin",
"Context",
"DateTime",
"FLOAT",
"File",
"FileError",
"FloatRange",
"Group",
"HelpFormatter",
"HelpSection",
"HelpTheme",
"INT",
"IntRange",
"MissingParameter",
"MultiCommand",
"NoSuchCommand",
"NoSuchOption",
"Option",
"OptionGroup",
"OptionGroupMixin",
"OptionParser",
"ParamType",
"Parameter",
"ParameterSource",
"Path",
"STRING",
"Section",
Expand All @@ -97,23 +177,44 @@
"Tuple",
"UNPROCESSED",
"UUID",
"_version",
"UsageError",
"argument",
"clear",
"command",
"confirm",
"confirmation_option",
"constrained_params",
"constraint",
"custom_version_option",
"dir_path",
"echo",
"echo_via_pager",
"edit",
"file_path",
"format_filename",
"get_app_dir",
"get_binary_stream",
"get_current_context",
"get_pager_file",
"get_text_stream",
"getchar",
"group",
"help_option",
"launch",
"make_pass_decorator",
"open_file",
"option",
"option_group",
"pass_context",
"pass_obj",
"password_option",
"path",
"pause",
"progressbar",
"prompt",
"secho",
"style",
"unstyle",
"version_option",
"warnings",
"wrap_text",
]
86 changes: 86 additions & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import types

import click
import pytest

import cloup


@pytest.fixture(scope="module")
def click_public_names() -> set[str]:
return {
name
for name, value in vars(click).items()
if not name.startswith("_")
and not isinstance(value, types.ModuleType)
and name != "annotations"
}


def test_cloup_namespace_contains_click_public_namespace(
click_public_names: set[str],
) -> None:
assert click_public_names <= set(dir(cloup))


def test_cloup_all_contains_click_public_namespace(
click_public_names: set[str],
) -> None:
assert click_public_names <= set(cloup.__all__)


def test_warnings_module_is_not_exported_by_star_import(
monkeypatch: pytest.MonkeyPatch,
) -> None:
namespace: dict[str, object] = {}
with pytest.warns(DeprecationWarning):
exec("from cloup import *", namespace)

assert "warnings" not in cloup.__all__
assert "warnings" not in namespace

monkeypatch.setattr(cloup.warnings, "formatter_settings_conflict", False)
assert cloup.warnings.formatter_settings_conflict is False


def test_click_names_are_reexported_or_intentionally_overridden(
click_public_names: set[str],
) -> None:
overridden = {
name
for name in click_public_names
if getattr(cloup, name) is not getattr(click, name)
}

assert overridden == {
"Command",
"Context",
"Group",
"HelpFormatter",
"Option",
"argument",
"command",
"get_current_context",
"group",
"option",
"pass_context",
}


@pytest.mark.parametrize(
"name",
[
"BaseCommand",
"MultiCommand",
"OptionParser",
"get_binary_stream",
"get_text_stream",
],
)
def test_deprecated_click_names_are_reexported(name: str) -> None:
assert name in cloup.__all__
with pytest.warns(DeprecationWarning):
cloup_value = getattr(cloup, name)
with pytest.warns(DeprecationWarning):
click_value = getattr(click, name)
assert cloup_value is click_value
Loading