Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Fix typing and other bugs #126

Merged
merged 12 commits into from
Oct 7, 2023
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
2 changes: 2 additions & 0 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- name: Install dependencies
run: pip install '.[dev]'
- uses: pre-commit/action@v2.0.3
6 changes: 2 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ repos:
name: mypy - Static type checking
description: Mypy helps ensure that we use our functions and variables correctly by checking the types.
entry: mypy
language: python
language: system
types: [python]
exclude: ^examples/|^tests/fixtures/
exclude: ^examples/
require_serial: true
additional_dependencies:
- mypy
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@ This release comes after merging a huge pull-request from [@BrutalSimplicity](ht
This PR closes a number of issues:

- [#25](https://github.com/ewels/rich-click/issues/25): Add tests!
- [#38](https://github.com/ewels/rich-click/issues/38): Support `click.MultiCommand`
- [#90](https://github.com/ewels/rich-click/issues/90): `click.ClickException` should output to `stderr`
- [#88](https://github.com/ewels/rich-click/issues/88): Rich Click breaks contract of Click's `format_help` and its callers
- [#18](https://github.com/ewels/rich-click/issues/18): Options inherited from context settings aren't applied
- [#114](https://github.com/ewels/rich-click/issues/114): `ctx.exit(exit_code)` not showing nonzero exit codes.

In addition, we merged another large pull-request that adds **full static type-checking support** (see issue [#85](https://github.com/ewels/rich-click/issues/85)), and fixes many bugs - see PR [#126](https://github.com/ewels/rich-click/pull/126).

In addition:

- Add new style option `STYLE_COMMAND` [[#102](https://github.com/ewels/rich-click/pull/102)]
- Add new style option `WIDTH` (in addition to `MAX_WIDTH`), thanks to [@ealap](httpsd://github.com/ealap) [[#110](https://github.com/ewels/rich-click/pull/110)]
- Updated styling for `Usage:` line to avoid off-target effects [[#108](https://github.com/ewels/rich-click/issues/108)]
- Click 7.x support has been deprecated. [[#117](https://github.com/ewels/rich-click/pull/117)]
- Fixed error where `ctx.exit(exit_code)` would not show nonzero exit codes.[[#114](https://github.com/ewels/rich-click/issues/114)]
- Support `click.MultiCommand`. [[#38](https://github.com/ewels/rich-click/issues/38)]:

## Version 1.6.1 (2023-01-19)

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ sections = [
profile = "black"

[tool.mypy]
python_version = "3.7"
ignore_missing_imports = "True"
python_version = "3.8"
scripts_are_modules = "True"
strict = "True"

[tool.pyright]
include = ["src"]
Expand Down
11 changes: 10 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
setup(
install_requires=["click>=7", "rich>=10.7.0", "importlib-metadata; python_version < '3.8'", "typing_extensions"],
extras_require={
"dev": ["pre-commit", "pytest", "flake8", "flake8-docstrings", "pytest-cov", "packaging"],
"dev": [
"mypy",
"pre-commit",
"pytest",
"flake8",
"flake8-docstrings",
"pytest-cov",
"packaging",
"types-setuptools",
],
},
)
233 changes: 95 additions & 138 deletions src/rich_click/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# flake8: noqa: F401
"""
rich-click is a minimal Python module to combine the efforts of the excellent packages 'rich' and 'click'.

Expand All @@ -7,141 +8,97 @@

__version__ = "1.7.0dev"

from typing import Any, Callable, cast, Optional, overload, TYPE_CHECKING, Union

from click import * # noqa: F401, F403
from click import Command
from click import command as click_command
from click import Group
from click import group as click_group
from rich.console import Console

from . import rich_click # noqa: F401

from rich_click._compat_click import CLICK_IS_BEFORE_VERSION_8X as _CLICK_IS_BEFORE_VERSION_8X
from rich_click.rich_command import RichBaseCommand, RichCommand, RichGroup, RichMultiCommand # noqa: F401
from rich_click.rich_context import RichContext
from rich_click.rich_help_configuration import RichHelpConfiguration

# MyPy does not like star imports. Therefore when we are type checking, we import each individual module
# from click here. This way MyPy will recognize the import and not throw any errors. Furthermore, because of
# the TYPE_CHECKING check, it does not influence the start routine at all.
if TYPE_CHECKING:
from click import argument, Choice, option, pass_context, Path, version_option # noqa: F401

__all__ = [
"argument",
"Choice",
"option",
"Path",
"version_option",
"group",
"command",
"rich_config",
"RichContext",
"RichHelpConfiguration",
"pass_context",
]


def group(name=None, cls=RichGroup, **attrs) -> Callable[..., RichGroup]:
"""
Group decorator function.

Defines the group() function so that it uses the RichGroup class by default.
"""

def wrapper(fn):
if hasattr(fn, "__rich_context_settings__"):
rich_context_settings = getattr(fn, "__rich_context_settings__", {})
console = rich_context_settings.get("rich_console", None)
help_config = rich_context_settings.get("help_config", None)
context_settings = attrs.get("context_settings", {})
context_settings.update(rich_console=console, rich_help_config=help_config)
attrs.update(context_settings=context_settings)
del fn.__rich_context_settings__
if callable(name) and cls:
group = click_group(cls=cls, **attrs)(name)
else:
group = click_group(name, cls=cls, **attrs)
cmd = cast(RichGroup, group(fn))
return cmd

return wrapper


def command(name=None, cls=RichCommand, **attrs) -> Callable[..., RichCommand]:
"""
Command decorator function.

Defines the command() function so that it uses the RichCommand class by default.
"""

def wrapper(fn):
if hasattr(fn, "__rich_context_settings__"):
rich_context_settings = getattr(fn, "__rich_context_settings__", {})
console = rich_context_settings.get("rich_console", None)
help_config = rich_context_settings.get("help_config", None)
context_settings = attrs.get("context_settings", {})
context_settings.update(rich_console=console, rich_help_config=help_config)
attrs.update(context_settings=context_settings)
del fn.__rich_context_settings__
if callable(name) and cls:
command = click_command(cls=cls, **attrs)(name)
else:
command = click_command(name, cls=cls, **attrs)
cmd = cast(RichCommand, command(fn))
return cmd

return wrapper


class NotSupportedError(Exception):
"""Not Supported Error."""

pass


def rich_config(console: Optional[Console] = None, help_config: Optional[RichHelpConfiguration] = None):
"""Use decorator to configure Rich Click settings.

Args:
console: A Rich Console that will be accessible from the `RichContext`, `RichCommand`, and `RichGroup` instances
Defaults to None.
help_config: Rich help configuration that is used internally to format help messages and exceptions
Defaults to None.
"""
if _CLICK_IS_BEFORE_VERSION_8X:

def decorator_with_warning(obj):
import warnings

warnings.warn(
"`rich_config()` does not work with versions of click prior to version 8.0.0."
" Please update to a newer version of click to use this functionality.",
RuntimeWarning,
)
return obj

return decorator_with_warning

@overload
def decorator(obj: Union[RichCommand, RichGroup]) -> Union[RichCommand, RichGroup]:
...

@overload
def decorator(obj: Callable[..., Any]) -> Callable[..., Any]:
...

def decorator(obj):
if isinstance(obj, (RichCommand, RichGroup)):
obj.context_settings.update({"rich_console": console, "rich_help_config": help_config})
elif callable(obj) and not isinstance(obj, (Command, Group)):
setattr(obj, "__rich_context_settings__", {"rich_console": console, "rich_help_config": help_config})
else:
raise NotSupportedError("`rich_config` requires a `RichCommand` or `RichGroup`. Try using the cls keyword")

decorator.__doc__ = obj.__doc__
return obj

return decorator
# Import the entire click API here.
# We need to manually import these instead of `from click import *` to force mypy to recognize a few type annotation overrides for the rich_click decorators.
from click.core import Argument as Argument
from click.core import Command as Command
from click.core import CommandCollection as CommandCollection
from click.core import Context as Context
from click.core import Group as Group
from click.core import Option as Option
from click.core import Parameter as Parameter
from click.decorators import argument as argument
from click.decorators import confirmation_option as confirmation_option
from click.decorators import help_option as help_option
from click.decorators import make_pass_decorator as make_pass_decorator
from click.decorators import option as option
from click.decorators import pass_obj as pass_obj
from click.decorators import password_option as password_option
from click.decorators import version_option as version_option
from click.exceptions import Abort as Abort
from click.exceptions import BadArgumentUsage as BadArgumentUsage
from click.exceptions import BadOptionUsage as BadOptionUsage
from click.exceptions import BadParameter as BadParameter
from click.exceptions import ClickException as ClickException
from click.exceptions import FileError as FileError
from click.exceptions import MissingParameter as MissingParameter
from click.exceptions import NoSuchOption as NoSuchOption
from click.exceptions import UsageError as UsageError
from click.formatting import HelpFormatter as HelpFormatter
from click.formatting import wrap_text as wrap_text
from click.globals import get_current_context as get_current_context
from click.termui import clear as clear
from click.termui import confirm as confirm
from click.termui import echo_via_pager as echo_via_pager
from click.termui import edit as edit
from click.termui import getchar as getchar
from click.termui import launch as launch
from click.termui import pause as pause
from click.termui import progressbar as progressbar
from click.termui import prompt as prompt
from click.termui import secho as secho
from click.termui import style as style
from click.termui import unstyle as unstyle
from click.types import BOOL as BOOL
from click.types import Choice as Choice
from click.types import DateTime as DateTime
from click.types import File as File
from click.types import FLOAT as FLOAT
from click.types import FloatRange as FloatRange
from click.types import INT as INT
from click.types import IntRange as IntRange
from click.types import ParamType as ParamType
from click.types import Path as Path
from click.types import STRING as STRING
from click.types import Tuple as Tuple
from click.types import UNPROCESSED as UNPROCESSED
from click.types import UUID as UUID
from click.utils import echo as echo
from click.utils import format_filename as format_filename
from click.utils import get_app_dir as get_app_dir
from click.utils import get_binary_stream as get_binary_stream
from click.utils import get_text_stream as get_text_stream
from click.utils import open_file as open_file

from . import rich_click as rich_click

from rich_click.decorators import command as command
from rich_click.decorators import group as group
from rich_click.decorators import pass_context as pass_context
from rich_click.decorators import rich_config as rich_config
from rich_click.rich_command import RichCommand as RichCommand
from rich_click.rich_command import RichCommandCollection as RichCommandCollection
from rich_click.rich_command import RichGroup as RichGroup
from rich_click.rich_context import RichContext as RichContext
from rich_click.rich_help_configuration import RichHelpConfiguration as RichHelpConfiguration


def __getattr__(name: str) -> object:
from rich_click._compat_click import CLICK_IS_BEFORE_VERSION_9X

if name == "RichMultiCommand" and CLICK_IS_BEFORE_VERSION_9X:
import warnings

warnings.warn(
"'RichMultiCommand' is deprecated and will be removed in Click 9.0. Use 'RichGroup' instead.",
DeprecationWarning,
stacklevel=2,
)
from rich_click.rich_command import RichMultiCommand

return RichMultiCommand

else:
import click

return getattr(click, name)
5 changes: 3 additions & 2 deletions src/rich_click/_compat_click.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
try:
from importlib import metadata # type: ignore
from importlib import metadata # type: ignore[import,unused-ignore]
except ImportError:
# Python < 3.8
import importlib_metadata as metadata # type: ignore
import importlib_metadata as metadata # type: ignore[no-redef,import]


click_version = metadata.version("click")
Expand All @@ -11,6 +11,7 @@


CLICK_IS_BEFORE_VERSION_8X = _major < 8
CLICK_IS_BEFORE_VERSION_9X = _major < 9
CLICK_IS_VERSION_80 = _major == 8 and _minor == 0


Expand Down
17 changes: 9 additions & 8 deletions src/rich_click/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
from typing import Any, List, Optional

try:
from importlib.metadata import entry_points
from importlib.metadata import entry_points # type: ignore[import,unused-ignore]
except ImportError:
# Support Python <3.8
from importlib_metadata import entry_points
from importlib_metadata import entry_points # type: ignore[import,no-redef]

import click
from rich.console import Console
Expand All @@ -19,7 +19,7 @@

from rich_click import command as rich_command
from rich_click import group as rich_group
from rich_click import RichBaseCommand, RichCommand, RichGroup, RichMultiCommand
from rich_click import RichCommand, RichCommandCollection, RichGroup, RichMultiCommand
from rich_click.rich_click import (
ALIGN_ERRORS_PANEL,
ERRORS_PANEL_TITLE,
Expand Down Expand Up @@ -66,10 +66,11 @@ def patch() -> None:
"""Patch Click internals to use Rich-Click types."""
click.group = rich_group
click.command = rich_command
click.Group = RichGroup
click.Command = RichCommand
click.BaseCommand = RichBaseCommand
click.MultiCommand = RichMultiCommand
click.Group = RichGroup # type: ignore[misc]
click.Command = RichCommand # type: ignore[misc]
click.CommandCollection = RichCommandCollection # type: ignore[misc]
if "MultiCommand" in dir(click):
click.MultiCommand = RichMultiCommand # type: ignore[assignment,misc]


def main(args: Optional[List[str]] = None) -> Any:
Expand Down Expand Up @@ -99,7 +100,7 @@ def main(args: Optional[List[str]] = None) -> Any:
sys.exit(0)
else:
script_name = args[0]
scripts = {script.name: script for script in entry_points().get("console_scripts")}
scripts = {script.name: script for script in entry_points().get("console_scripts", [])}
if script_name in scripts:
# a valid script was passed
script = scripts[script_name]
Expand Down
Loading
Loading