diff --git a/README.md b/README.md
index 8a6c260..8b454a1 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ somewhat verbose and often requires frequently looking up documentation.
Consider the following example command for serving local files on a HTTP server.
-In red is a typical Click implementation, and in green is the Feud equivalent.
+**In red is a typical Click implementation, and in green is the Feud equivalent.**
```diff
- import click
@@ -472,8 +472,8 @@ $ python blog.py post list --help
### Powerful typing
-Feud is powered by Pydantic – a validation library with extensive support for
-many data types, including:
+Feud is powered by [Pydantic](https://docs.pydantic.dev/latest/) – a
+validation library with extensive support for many data types, including:
- simple types such as integers and dates,
- complex types such as emails, IP addresses, file/directory paths, database
@@ -742,22 +742,26 @@ You can install Feud using `pip`.
The latest stable version of Feud can be installed with the following command.
```console
-pip install feud[all]
+pip install "feud[all]"
```
This installs Feud with the optional dependencies:
-- [`rich-click`](https://github.com/ewels/rich-click) (can install individually with `pip install feud[rich]`)
+- [`rich-click`](https://github.com/ewels/rich-click) (can install individually with `pip install "feud[rich]"`)
_Provides improved formatting for CLIs produced by Feud._
-- [`pydantic-extra-types`](https://github.com/pydantic/pydantic-extra-types) (can install individually with `pip install feud[extra-types]`)
+- [`pydantic-extra-types`](https://github.com/pydantic/pydantic-extra-types) (can install individually with `pip install "feud[extra-types]"`)
_Provides additional types that can be used as type hints for Feud commands._
-- [`email-validator`](https://github.com/JoshData/python-email-validator) (can install individually with `pip install feud[email]`)
+- [`email-validator`](https://github.com/JoshData/python-email-validator) (can install individually with `pip install "feud[email]"`)
_Provides Pydantic support for email validation._
To install Feud without any optional dependencies, simply run `pip install feud`.
> [!CAUTION]
-> Feud **will break** if used with postponed type hint evaluation ([PEP563](https://peps.python.org/pep-0563/)), i.e. `from __future__ import annotations`.
+> Feud **will break** if used with postponed type hint evaluation ([PEP563](https://peps.python.org/pep-0563/)), i.e.:
+>
+> ```python
+> from __future__ import annotations
+> ```
>
> This is because Feud relies on type hint evaluation in order to determine the expected input type for command parameters.
@@ -890,7 +894,7 @@ All contributions to this repository are greatly appreciated. Contribution guide
>
> We're living in an imperfect world!
-> Feud is in a public beta-test phase, likely with lots of bugs. Please leave feedback if you come across anything strange!
+> Feud is in a public beta-test phase, likely with lots of bugs. Please leave feedback if you come across anything strange!
## Licensing
diff --git a/docs/source/sections/config/index.rst b/docs/source/sections/config/index.rst
index 9dfebc6..8213182 100644
--- a/docs/source/sections/config/index.rst
+++ b/docs/source/sections/config/index.rst
@@ -3,6 +3,12 @@
Configuration
=============
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
:doc:`../core/command` are defined by :py:func:`.command`,
which accepts various Feud configuration key-word arguments such as
``negate_flags`` or ``show_help_defaults`` directly.
@@ -15,12 +21,6 @@ object that can be provided to other commands or groups. This functionality is
implemented by :py:func:`.config`, which creates a configuration which can be
provided to :py:func:`.command` or :py:class:`.Group`.
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
API reference
diff --git a/docs/source/sections/core/command.rst b/docs/source/sections/core/command.rst
index 5224c74..3086754 100644
--- a/docs/source/sections/core/command.rst
+++ b/docs/source/sections/core/command.rst
@@ -1,6 +1,12 @@
Commands
========
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
Commands are the core component of a CLI, running a user-defined function that
may be parameterized with arguments or options.
@@ -18,12 +24,6 @@ Commands may be executed using :py:func:`.run`.
- `Arguments `__
- `Options `__
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
Understanding function signatures
diff --git a/docs/source/sections/core/group.rst b/docs/source/sections/core/group.rst
index 9c7a0d1..6a18fcf 100644
--- a/docs/source/sections/core/group.rst
+++ b/docs/source/sections/core/group.rst
@@ -1,6 +1,12 @@
Groups
======
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
Groups are a component of CLIs that allow you to group together related :doc:`command`.
In addition to commands, groups may also contain further nested groups by :py:obj:`.register`\ ing subgroups,
@@ -18,12 +24,6 @@ Groups and their subgroups or commands can be executed using :py:func:`.run`.
- `Arguments `__
- `Options `__
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
API reference
diff --git a/docs/source/sections/decorators/alias.rst b/docs/source/sections/decorators/alias.rst
index c063acc..b3144fa 100644
--- a/docs/source/sections/decorators/alias.rst
+++ b/docs/source/sections/decorators/alias.rst
@@ -1,6 +1,12 @@
Aliasing parameters
===================
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
In CLIs, it is common for options to have an alias allowing
for quicker short-hand usage.
@@ -43,12 +49,6 @@ and can instead rely on type hints and docstrings.
In the case of boolean flags such as ``--verbose`` in this case, the ``--no-verbose``
option will also have a corresponding ``--no-v`` alias automatically defined.
-
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
----
diff --git a/docs/source/sections/decorators/env.rst b/docs/source/sections/decorators/env.rst
index b187caa..7f67476 100644
--- a/docs/source/sections/decorators/env.rst
+++ b/docs/source/sections/decorators/env.rst
@@ -1,6 +1,12 @@
Using environment variables
===========================
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
In CLIs, environment variables are often used as an alternative method of
providing input for options. This is particularly useful for sensitive
information such as API keys, tokens and passwords.
@@ -52,12 +58,6 @@ and can instead rely on type hints and docstrings.
feud.run(my_command)
This can be called with ``SECRET_TOKEN=hello-world python command.py``, for example.
-
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
----
diff --git a/docs/source/sections/decorators/index.rst b/docs/source/sections/decorators/index.rst
index 4ccd6c9..e92a9ce 100644
--- a/docs/source/sections/decorators/index.rst
+++ b/docs/source/sections/decorators/index.rst
@@ -10,4 +10,4 @@ This module consists of decorators that modify :doc:`../core/command` and their
alias.rst
env.rst
- .. rename.rst
+ rename.rst
diff --git a/docs/source/sections/decorators/rename.rst b/docs/source/sections/decorators/rename.rst
index bea3a76..b90ccdf 100644
--- a/docs/source/sections/decorators/rename.rst
+++ b/docs/source/sections/decorators/rename.rst
@@ -1,11 +1,162 @@
-Renaming parameters
-===================
+Renaming commands/parameters
+============================
-TODO
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
+In certain cases, it may be desirable or even necessary for the names of the
+commands or parameters generated by Feud to be different to the names of the
+Python functions (and their parameters) that were used to generate the
+commands.
+
+The :py:func:`.rename` operator can be used in these scenarios to rename commands
+or parameters.
+
+Examples
+--------
+
+Defining commands or parameters with reserved keywords
+******************************************************
+
+Suppose we have the following command, ``sum``,
+which takes a starting number ``from``, and an ending number ``to``,
+and sums all numbers between and including the starting and ending number.
+
+This might be called in the following way:
+
+.. code:: bash
+
+ $ sum --from 1 --to 10
+
+With generating code that might look like:
+
+.. code:: python
+
+ # sum.py
+
+ import feud
+
+ def sum(*, from: int, to: int):
+ """Sums the numbers between and including a start and end number.
+
+ Parameters
+ ----------
+ from:
+ Starting number.
+ to:
+ Ending number.
+ """
+ print(sum(range(from, to + 1)))
+
+ if __name__ == "__main__":
+ feud.run(sum)
+
+There are two problems here:
+
+1. By naming the function ``sum``, we are shadowing the in-built Python
+ function ``sum``. This is also an issue as our function actually relies
+ on the in-built Python ``sum`` function to actually do the addition.
+2. ``from`` is also a reserved Python keyword which is used in module imports,
+ and cannot be used as a function parameter.
+
+We can use the :py:func:`.rename` decorator to rename both the command and parameter.
+
+.. code:: python
+
+ # sum.py
+
+ import feud
+
+ @feud.rename("sum", from_="from")
+ def sum_(*, from_: int, to: int):
+ """Sums the numbers between and including a start and end number.
+
+ Parameters
+ ----------
+ from_:
+ Starting number.
+ to:
+ Ending number.
+ """
+ print(sum(range(from, to + 1)))
+
+ if __name__ == "__main__":
+ feud.run(sum_)
+
+This gives us valid Python code, and also our expected CLI behaviour.
+
+Defining hyphenated commands or parameters
+******************************************
+
+Suppose we have a command that should be called in the following way:
+
+.. code:: bash
+
+ $ say-hi --welcome-message "Hello World!"
+
+As Feud uses the parameter names present in the Python function signature as
+the parameter names for the generated CLI, this means that defining parameters
+with hyphens is *usually* not possible, as Python identifiers cannot have hyphens.
+Similarly, a function name cannot have a hyphen:
+
+.. code:: python
+
+ # hyphen.py
+
+ import feud
+
+ def say-hi(*, welcome-message: str):
+ print(welcome-message)
+
+ if __name__ == "__main__":
+ feud.run(say-hi)
+
+We can use the :py:func:`.rename` decorator to rename both the command and parameter.
+
+.. code:: python
+
+ # hyphen.py
+
+ import feud
+
+ @feud.rename("say-hi", welcome_message="welcome-message")
+ def say_hi(*, welcome_message: str):
+ print(welcome_message)
+
+ if __name__ == "__main__":
+ feud.run(say_hi)
+
+This gives us valid Python code, and also our expected CLI behaviour.
+
+Special use case for maintaining group-level configurations
+***********************************************************
+
+Although :py:func:`.command` accepts a ``name`` argument (passed to Click) that can be
+used to rename a command, this can sometimes be undesirable in the case of :doc:`../core/group`.
+
+In the following example, although ``show_help_defaults`` has been set to
+``False`` at the group level (which would usually mean that all commands
+defined within the group will not have their parameter defaults shown in
+``--help``), this has been overridden by the ``feud.command`` call which
+has ``show_help_defaults=True`` by default.
+
+.. code:: python
+
+ class CLI(feud.Group, show_help_defaults=False):
+ @feud.command(name="my-func")
+ def my_func(*, opt: int = 1):
+ pass
+
+Using ``@feud.rename("my-func")`` instead of ``@feud.command(name="my-func")``
+would allow for the group-level configuration to be used, while still renaming
+the function.
----
API reference
-------------
-TODO
+.. autofunction:: feud.decorators.rename
diff --git a/docs/source/sections/typing/other.rst b/docs/source/sections/typing/other.rst
index 20de8e3..53b35be 100644
--- a/docs/source/sections/typing/other.rst
+++ b/docs/source/sections/typing/other.rst
@@ -1,6 +1,12 @@
Other types
===========
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
Feud provides the following additional types for common CLI needs.
.. tip::
@@ -16,12 +22,6 @@ Feud provides the following additional types for common CLI needs.
t.Counter # feud.typing.custom.Counter
t.concounter # feud.typing.custom.concounter
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
Counting types
diff --git a/docs/source/sections/typing/pydantic.rst b/docs/source/sections/typing/pydantic.rst
index ddebd09..ec0952c 100644
--- a/docs/source/sections/typing/pydantic.rst
+++ b/docs/source/sections/typing/pydantic.rst
@@ -1,6 +1,12 @@
Pydantic types
==============
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
`Pydantic `__ is a validation library that provides
a rich selection of useful types for command-line inputs.
@@ -29,12 +35,6 @@ The following commonly used Pydantic types can be used as type hints for Feud co
t.conint # pydantic.types.conint
t.IPvAnyAddress # pydantic.networks.IPvAnyAddress
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
String types
diff --git a/docs/source/sections/typing/pydantic_extra_types.rst b/docs/source/sections/typing/pydantic_extra_types.rst
index 7b2ada2..0e01157 100644
--- a/docs/source/sections/typing/pydantic_extra_types.rst
+++ b/docs/source/sections/typing/pydantic_extra_types.rst
@@ -1,6 +1,12 @@
Pydantic extra types
====================
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
`Pydantic Extra Types `__
is a package that extends `Pydantic `__
with support for additional types.
@@ -29,12 +35,6 @@ The following types can be used as type hints for Feud commands.
t.Latitude # pydantic_extra_types.coordinate.Latitude
t.Color # pydantic_extra_types.color.Color
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
Color type
diff --git a/docs/source/sections/typing/stdlib.rst b/docs/source/sections/typing/stdlib.rst
index 0b0c1e0..64203fe 100644
--- a/docs/source/sections/typing/stdlib.rst
+++ b/docs/source/sections/typing/stdlib.rst
@@ -1,6 +1,12 @@
Standard library types
======================
+.. contents:: Table of Contents
+ :class: this-will-duplicate-information-and-it-is-still-useful-here
+ :local:
+ :backlinks: none
+ :depth: 3
+
The following Python standard library types can be used as type hints for Feud commands.
.. tip::
@@ -21,12 +27,6 @@ The following Python standard library types can be used as type hints for Feud c
t.NamedTuple # typing.NamedTuple
t.Union # typing.Union
-.. contents:: Table of Contents
- :class: this-will-duplicate-information-and-it-is-still-useful-here
- :local:
- :backlinks: none
- :depth: 3
-
----
String type
diff --git a/feud/_internal/_command.py b/feud/_internal/_command.py
index 3cf9276..d356cd4 100644
--- a/feud/_internal/_command.py
+++ b/feud/_internal/_command.py
@@ -34,24 +34,26 @@ class ParameterSpec:
kwargs: dict[str, t.Any] = dataclasses.field(default_factory=dict)
+class NameDict(t.TypedDict):
+ command: str | None
+ params: dict[str, str]
+
+
@dataclasses.dataclass
class CommandState:
config: Config
click_kwargs: dict[str, t.Any]
is_group: bool
+ names: dict[str, NameDict] # key: parameter name
+ aliases: dict[str, str | list[str]] # key: parameter name
+ envs: dict[str, str] # key: parameter name
+ overrides: dict[str, click.Parameter] # key: parameter name
pass_context: bool = False
# below keys are parameter name
arguments: dict[str, ParameterSpec] = dataclasses.field(
default_factory=dict
)
options: dict[str, ParameterSpec] = dataclasses.field(default_factory=dict)
- aliases: dict[str, str | list[str]] = dataclasses.field(
- default_factory=dict
- )
- envs: dict[str, str] = dataclasses.field(default_factory=dict)
- overrides: dict[str, click.Parameter] = dataclasses.field(
- default_factory=dict
- )
def decorate(self: CommandState, func: t.Callable) -> click.Command:
meta_vars: dict[str, str] = {}
@@ -82,8 +84,18 @@ def decorate(self: CommandState, func: t.Callable) -> click.Command:
hide_input = spec.kwargs.get("hide_input")
envvar = spec.kwargs.get("envvar")
sensitive = hide_input or envvar
- meta_vars[param_name] = self.get_meta_var(param)
- sensitive_vars[param_name] = sensitive
+
+ # get renamed parameter if @feud.rename used
+ name: str = self.names["params"].get(param_name, param_name)
+
+ # set parameter name
+ param.name = name
+
+ # get meta vars and identify sensitive parameters for validate_call
+ meta_vars[name] = self.get_meta_var(param)
+ sensitive_vars[name] = sensitive
+
+ # add the parameter
params.append(param)
# add any overrides that don't appear in function signature
@@ -92,9 +104,14 @@ def decorate(self: CommandState, func: t.Callable) -> click.Command:
if param_name not in sig.parameters:
params.append(param)
+ # rename command if @feud.rename used
+ if command_rename := self.names["command"]:
+ self.click_kwargs = {**self.click_kwargs, "name": command_rename}
+
command = _decorators.validate_call(
func,
name=self.click_kwargs["name"],
+ param_renames=self.names["params"],
meta_vars=meta_vars,
sensitive_vars=sensitive_vars,
pydantic_kwargs=self.config.pydantic_kwargs,
diff --git a/feud/_internal/_decorators.py b/feud/_internal/_decorators.py
index 1a02ac3..a130648 100644
--- a/feud/_internal/_decorators.py
+++ b/feud/_internal/_decorators.py
@@ -23,6 +23,7 @@ def validate_call(
/,
*,
name: str,
+ param_renames: dict[str, str],
meta_vars: dict[str, str],
sensitive_vars: dict[str, bool],
pydantic_kwargs: dict[str, t.Any],
@@ -30,8 +31,10 @@ def validate_call(
@ft.wraps(func)
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Callable:
try:
+ inv_mapping = {v: k for k, v in param_renames.items()}
config = pyd.ConfigDict(**pydantic_kwargs)
- return pyd.validate_call(func, config=config)(*args, **kwargs)
+ true_kwargs = {inv_mapping.get(k, k): v for k, v in kwargs.items()}
+ return pyd.validate_call(func, config=config)(*args, **true_kwargs)
except pyd.ValidationError as e:
msg = re.sub(
r"validation error(s?) for (.*)\n",
diff --git a/feud/core/command.py b/feud/core/command.py
index ab8f1b0..d0b8567 100644
--- a/feud/core/command.py
+++ b/feud/core/command.py
@@ -134,6 +134,9 @@ def build_command_state(
meta = _command.ParameterSpec()
meta.hint: type = spec.annotation
+ # get renamed parameter if @feud.rename used
+ name: str = state.names["params"].get(param, param)
+
if _command.pass_context(sig) and param == _command.CONTEXT_PARAM:
# skip handling for click.Context argument
state.pass_context = True
@@ -143,7 +146,7 @@ def build_command_state(
meta.type = _command.ParameterType.ARGUMENT
# add the argument
- meta.args = [param]
+ meta.args = [name]
# special handling for variable-length collections
is_collection, base_type = _types.click.is_collection_type(
@@ -182,7 +185,7 @@ def build_command_state(
# add the option
meta.args = [
_command.get_option(
- param, hint=meta.hint, negate_flags=config.negate_flags
+ name, hint=meta.hint, negate_flags=config.negate_flags
)
]
@@ -243,6 +246,9 @@ def get_command(
is_group=False,
aliases=getattr(func, "__feud_aliases__", {}),
envs=getattr(func, "__feud_envs__", {}),
+ names=getattr(
+ func, "__feud_names__", _command.NameDict(command=None, params={})
+ ),
overrides={
override.name: override
for override in getattr(func, "__click_params__", [])
diff --git a/feud/core/group.py b/feud/core/group.py
index 36dfae9..88f1896 100644
--- a/feud/core/group.py
+++ b/feud/core/group.py
@@ -63,6 +63,9 @@ class Group(metaclass=_metaclass.GroupBase):
- :py:func:`~descendants`
- :py:func:`~register`
- :py:func:`~subgroups`
+
+ See :py:func:`.rename` if you wish to define a command with one of the
+ above names.
"""
__feud_config__: t.ClassVar[Config]
@@ -458,6 +461,9 @@ def get_group(__cls: type[Group], /) -> click.Group:
is_group=True,
aliases=getattr(func, "__feud_aliases__", {}),
envs=getattr(func, "__feud_envs__", {}),
+ names=getattr(
+ func, "__feud_names__", _command.NameDict(command=None, params={})
+ ),
overrides={
override.name: override
for override in getattr(func, "__click_params__", [])
diff --git a/feud/decorators.py b/feud/decorators.py
index b7b275e..2dd280d 100644
--- a/feud/decorators.py
+++ b/feud/decorators.py
@@ -10,13 +10,13 @@
import inspect
import re
import typing as t
-from functools import partial
import pydantic as pyd
+from feud._internal import _command
from feud.exceptions import CompilationError
-__all__ = ["alias", "env"]
+__all__ = ["alias", "env", "rename"]
@pyd.validate_call
@@ -33,9 +33,7 @@ def alias(**aliases: str | list[str]) -> t.Callable:
Parameters
----------
**aliases:
-
Mapping of option names to aliases.
-
Option names must be keyword-only parameters in the decorated
function signature.
@@ -90,9 +88,7 @@ def alias(**aliases: str | list[str]) -> t.Callable:
3
"""
- def decorator(
- f: t.Callable, *, aliases: dict[str, str | list[str]]
- ) -> t.Callable:
+ def decorator(f: t.Callable) -> t.Callable:
# check provided aliases and parameters match
sig = inspect.signature(f)
specified = set(aliases.keys())
@@ -134,7 +130,7 @@ def decorator(
}
return f
- return partial(decorator, aliases=aliases)
+ return decorator
def env(**envs: str) -> t.Callable:
@@ -150,9 +146,7 @@ def env(**envs: str) -> t.Callable:
Parameters
----------
**envs:
-
Mapping of option names to environment variables.
-
Option names must be keyword-only parameters in the decorated
function signature.
@@ -186,7 +180,7 @@ def env(**envs: str) -> t.Callable:
("Hello world!", "This is a secret key.")
"""
- def decorator(f: t.Callable, *, envs: dict[str, str]) -> t.Callable:
+ def decorator(f: t.Callable) -> t.Callable:
# check provided envs and parameters match
sig = inspect.signature(f)
specified = set(envs.keys())
@@ -204,8 +198,66 @@ def decorator(f: t.Callable, *, envs: dict[str, str]) -> t.Callable:
f.__feud_envs__ = envs
return f
- return partial(decorator, envs=envs)
+ return decorator
+
+
+def rename(command: str | None = None, /, **params: str) -> t.Callable:
+ """Rename a command and/or its parameters.
+
+ Useful for command/parameter names that use hyphens, reserved Python
+ keywords or in-built function names.
+
+ Parameters
+ ----------
+ command:
+ New command name. If ``None``, the command is not renamed.
+
+ **params:
+ Mapping of parameter names to new names.
+ Parameter names must be defined in the decorated function signature.
+
+ Returns
+ -------
+ Function decorated with command/parameter renaming metadata.
+ Examples
+ --------
+ Renaming a command.
+
+ >>> import feud
+ >>> @feud.rename("my-func")
+ ... def my_func(arg_1: int, *, opt_1: str, opt_2: bool):
+ ... pass
+
+ Renaming parameters.
+
+ >>> import feud
+ >>> @feud.rename(arg_1="arg-1", opt_2="opt-2")
+ ... def my_func(arg_1: int, *, opt_1: str, opt_2: bool):
+ ... pass
+
+ Renaming a command and parameters.
+
+ >>> import feud
+ >>> @feud.rename("my-func", arg_1="arg-1", opt_2="opt-2")
+ ... def my_func(arg_1: int, *, opt_1: str, opt_2: bool):
+ ... pass
+ """
+
+ def decorator(f: t.Callable) -> t.Callable:
+ # check provided names and parameters match
+ sig = inspect.signature(f)
+ specified = set(params.keys())
+ received = {p.name for p in sig.parameters.values()}
+ if len(specified - received) > 0:
+ msg = (
+ f"Arguments provided to 'env' decorator must "
+ f"also be parameters for function {f.__name__!r}. "
+ f"Received extra arguments: {specified - received!r}."
+ )
+ raise CompilationError(msg)
+
+ f.__feud_names__ = _command.NameDict(command=command, params=params)
+ return f
-# def rename(command: str | None = None, /, **params: str) -> t.Callable:
-# rename("cmd") renames the command without requiring @feud.command(name="cmd")
+ return decorator
diff --git a/tests/unit/test_decorators.py b/tests/unit/test_decorators.py
index f41ce9a..9cba1e3 100644
--- a/tests/unit/test_decorators.py
+++ b/tests/unit/test_decorators.py
@@ -193,3 +193,177 @@ def f(*, opt: t.constr(max_length=3)) -> str:
msg = "String should have at most 3 characters [input_value=hidden]"
with pytest.raises(click.UsageError, match=re.escape(msg)):
feud.run(f, [], standalone_mode=False)
+
+
+def test_rename_command() -> None:
+ @feud.command
+ @feud.rename("func")
+ def f(*, opt: int) -> int:
+ return opt
+
+ assert f.name == "func"
+
+
+def test_rename_params() -> None:
+ @feud.rename(arg1="arg-1", arg2="arg-2", opt1="opt-1", opt2="opt-2")
+ def f(
+ ctx: click.Context, arg1: int, arg2: str, *, opt1: bool, opt2: float
+ ) -> None:
+ return arg1, arg2, opt1, opt2
+
+ cmd = feud.command(f)
+
+ # check arg1 -> arg-1 rename
+ assert cmd.params[0].name == "arg-1"
+
+ # check arg2 -> arg-2 rename
+ assert cmd.params[1].name == "arg-2"
+
+ # check opt1 -> opt-1 rename
+ # should create options --opt-1/--no-opt-1
+ assert cmd.params[2].name == "opt-1"
+ assert cmd.params[2].opts == ["--opt-1"]
+ assert cmd.params[2].secondary_opts == ["--no-opt-1"]
+
+ # check opt2 -> opt-2 rename
+ # should create option --opt-2
+ assert cmd.params[3].name == "opt-2"
+ assert cmd.params[3].opts == ["--opt-2"]
+
+ # test call
+ assert cmd(
+ ["2", "test", "--no-opt-1", "--opt-2", "0.2"], standalone_mode=False
+ ) == (2, "test", False, 0.2)
+
+
+def test_rename_command_and_params(capsys: pytest.CaptureFixture) -> None:
+ @feud.rename(
+ "func", arg1="arg-1", arg2="arg-2", opt1="opt-1", opt2="opt-2"
+ )
+ def f(
+ ctx: click.Context, arg1: int, arg2: str, *, opt1: bool, opt2: float
+ ) -> None:
+ return arg1, arg2, opt1, opt2
+
+ cmd = feud.command(f)
+
+ # check command name
+ assert cmd.name == "func"
+
+ # check arg1 -> arg-1 rename
+ assert cmd.params[0].name == "arg-1"
+
+ # check arg2 -> arg-2 rename
+ assert cmd.params[1].name == "arg-2"
+
+ # check opt1 -> opt-1 rename
+ # should create options --opt-1/--no-opt-1
+ assert cmd.params[2].name == "opt-1"
+ assert cmd.params[2].opts == ["--opt-1"]
+ assert cmd.params[2].secondary_opts == ["--no-opt-1"]
+
+ # check opt2 -> opt-2 rename
+ # should create option --opt-2
+ assert cmd.params[3].name == "opt-2"
+ assert cmd.params[3].opts == ["--opt-2"]
+
+ # check help
+ assert_help(
+ cmd,
+ capsys=capsys,
+ expected="""
+Usage: pytest [OPTIONS] ARG-1 ARG-2
+
+Options:
+ --opt-1 / --no-opt-1 [required]
+ --opt-2 FLOAT [required]
+ --help Show this message and exit.
+ """,
+ )
+
+ # test call
+ assert cmd(
+ ["2", "test", "--no-opt-1", "--opt-2", "0.2"], standalone_mode=False
+ ) == (2, "test", False, 0.2)
+
+
+@mock.patch.dict(os.environ, {"OPT1": "1", "OPT2": "true"}, clear=True)
+def test_all_decorators(capsys: pytest.CaptureFixture) -> None:
+ @feud.rename("cmd", opt1="opt-1", opt2="opt-2", opt3="opt_3")
+ @feud.env(opt1="OPT1", opt2="OPT2")
+ @feud.alias(opt3="-o")
+ def command(
+ *, opt1: t.PositiveInt, opt2: bool, opt3: t.NegativeFloat
+ ) -> t.Path:
+ """Returns a full path.\f
+
+ Parameters
+ ----------
+ opt1:
+ First option.
+ opt2:
+ Second option.
+ opt3:
+ Third option.
+ """
+ return opt1, opt2, opt3
+
+ cmd = feud.command(command)
+
+ # check command name
+ assert cmd.name == "cmd"
+
+ # check opt1 -> opt-1 rename
+ # should create option --opt-1
+ assert cmd.params[0].name == "opt-1"
+ assert cmd.params[0].opts == ["--opt-1"]
+ assert cmd.params[0].envvar == "OPT1"
+
+ # check opt2 -> opt-2 rename
+ # should create option --opt-2
+ assert cmd.params[1].name == "opt-2"
+ assert cmd.params[1].opts == ["--opt-2"]
+ assert cmd.params[1].envvar == "OPT2"
+
+ # check opt3 -> opt_3 rename
+ # should create option --opt_3
+ assert cmd.params[2].name == "opt_3"
+ assert cmd.params[2].opts == ["--opt_3", "-o"]
+
+ # check help
+ assert_help(
+ cmd,
+ capsys=capsys,
+ expected="""
+Usage: pytest [OPTIONS]
+
+ Returns a full path.
+
+Options:
+ --opt-1 INTEGER RANGE First option. [env var: OPT1; x>0; required]
+ --opt-2 / --no-opt-2 Second option. [env var: OPT2; required]
+ -o, --opt_3 FLOAT RANGE Third option. [x<0; required]
+ --help Show this message and exit.
+ """,
+ )
+
+ # test call
+ assert cmd(["--opt_3", "-1.2"], standalone_mode=False) == (1, True, -1.2)
+
+
+def test_rename_group(capsys: pytest.CaptureFixture) -> None:
+ class Test(feud.Group):
+ @staticmethod
+ @feud.rename("test-group", opt1="opt-1")
+ def __main__(ctx: click.Context, *, opt1: int) -> None:
+ ctx.obj = {"opt1": opt1}
+
+ @staticmethod
+ @feud.rename("func", opt2="opt_2")
+ def f(ctx: click.Context, *, opt2: int) -> int:
+ return ctx.obj["opt1"], opt2
+
+ return Test(
+ ["--opt-1", "1", "func", "--opt_2", "2"],
+ standalone_mode=False,
+ ) == (1, 2)
diff --git a/tests/unit/test_internal/test_decorators.py b/tests/unit/test_internal/test_decorators.py
index 7f701f1..ca458b2 100644
--- a/tests/unit/test_internal/test_decorators.py
+++ b/tests/unit/test_internal/test_decorators.py
@@ -19,6 +19,7 @@ def test_validate_call_single_invalid() -> None:
value.
"""
name = "func"
+ param_renames = {}
meta_vars = {"arg2": "--arg2"}
sensitive_vars = {"arg2": False}
pydantic_kwargs = {}
@@ -30,6 +31,7 @@ def f(*, arg2: t.Literal["a", "b", "c"]) -> None:
_decorators.validate_call(
f,
name=name,
+ param_renames=param_renames,
meta_vars=meta_vars,
sensitive_vars=sensitive_vars,
pydantic_kwargs=pydantic_kwargs,
@@ -50,6 +52,7 @@ def test_validate_call_multiple_invalid() -> None:
input values.
"""
name = "func"
+ param_renames = {}
meta_vars = {"0": "ARG1", "arg2": "--arg2"}
sensitive_vars = {"0": False, "arg2": False}
pydantic_kwargs = {}
@@ -61,6 +64,7 @@ def f(arg1: int, *, arg2: t.Literal["a", "b", "c"]) -> None:
_decorators.validate_call(
f,
name=name,
+ param_renames=param_renames,
meta_vars=meta_vars,
sensitive_vars=sensitive_vars,
pydantic_kwargs=pydantic_kwargs,
@@ -83,6 +87,7 @@ def test_validate_call_list() -> None:
a list argument.
"""
name = "func"
+ param_renames = {}
meta_vars = {"0": "[ARG1]..."}
sensitive_vars = {"0": False}
pydantic_kwargs = {}
@@ -94,6 +99,7 @@ def f(arg1: list[t.conint(multiple_of=2)]) -> None:
_decorators.validate_call(
f,
name=name,
+ param_renames=param_renames,
meta_vars=meta_vars,
sensitive_vars=sensitive_vars,
pydantic_kwargs=pydantic_kwargs,
@@ -116,6 +122,7 @@ def test_validate_call_enum() -> None:
for an enum parameter.
"""
name = "func"
+ param_renames = {}
meta_vars = {"arg2": "--arg2"}
sensitive_vars = {"arg2": False}
pydantic_kwargs = {}
@@ -132,6 +139,7 @@ def f(*, arg2: Choice) -> None:
_decorators.validate_call(
f,
name=name,
+ param_renames=param_renames,
meta_vars=meta_vars,
sensitive_vars=sensitive_vars,
pydantic_kwargs=pydantic_kwargs,
@@ -152,6 +160,7 @@ def test_validate_call_datetime() -> None:
for a datetime parameter.
"""
name = "func"
+ param_renames = {}
meta_vars = {"time": "--time"}
sensitive_vars = {"time": False}
pydantic_kwargs = {}
@@ -163,6 +172,7 @@ def f(*, time: t.FutureDatetime) -> None:
_decorators.validate_call(
f,
name=name,
+ param_renames=param_renames,
meta_vars=meta_vars,
sensitive_vars=sensitive_vars,
pydantic_kwargs=pydantic_kwargs,