"""
This modules contains predicates with an associated description that you can use
as conditions of conditional constraints (see :class:`cloup.constraints.If`).
Predicates should be treated as immutable objects, even though immutability
is not (at the moment) enforced.
"""
import abc
from typing import Any, Generic, TypeVar
from click import Context
from ._support import ensure_constraints_support
from .common import (
get_param_labels,
get_param_name,
join_with_and,
param_label_by_name,
param_value_by_name,
param_value_is_set,
)
from .._util import make_repr
P = TypeVar('P', bound='Predicate')
[docs]class Predicate(abc.ABC):
"""
A ``Callable`` that takes a ``Context`` and returns a boolean, with an
associated description. Meant to be used as condition in a conditional
constraint (see :class:`~cloup.constraints.If`).
"""
[docs] @abc.abstractmethod
def description(self, ctx: Context) -> str:
"""Succint description of the predicate (alias: `desc`)."""
[docs] def negated_description(self, ctx: Context) -> str:
"""Succint description of the negation of this predicate (alias: `neg_desc`)."""
return 'NOT(%s)' % self.description(ctx)
[docs] def desc(self, ctx: Context) -> str:
"""Short alias for :meth:`description`."""
return self.description(ctx)
[docs] def neg_desc(self, ctx: Context) -> str:
"""Short alias for :meth:`negated_description`."""
return self.negated_description(ctx)
[docs] def negated(self):
return ~self
[docs] @abc.abstractmethod
def __call__(self, ctx: Context) -> bool:
"""Evaluate the predicate on the given context."""
[docs] def __invert__(self) -> 'Predicate':
return Not(self)
[docs] def __or__(self, other: 'Predicate') -> '_Or':
return _Or(self, other)
[docs] def __and__(self, other: 'Predicate') -> '_And':
return _And(self, other)
[docs] def __repr__(self):
return make_repr(self, *self._public_fields().values())
def _public_fields(self):
return {k: v for k, v in vars(self).items() if not k.startswith('_')}
[docs] def __eq__(self, other):
return isinstance(other, self.__class__) and (
self._public_fields() == other._public_fields()
)
[docs]class Not(Predicate, Generic[P]):
"""Logical NOT of a predicate."""
def __init__(self, predicate: P):
self.predicate = predicate
[docs] def description(self, ctx: Context) -> str:
return self.predicate.negated_description(ctx)
[docs] def negated_description(self, ctx: Context) -> str:
return self.predicate.description(ctx)
[docs] def __call__(self, ctx: Context) -> bool:
return not self.predicate(ctx)
[docs] def __invert__(self) -> P:
return self.predicate
[docs] def __repr__(self) -> str:
return 'Not(%r)' % self.predicate
class _Operator(Predicate, metaclass=abc.ABCMeta):
"""Operator between two or more predicates."""
DESC_SEP: str
def __init__(self, *predicates: Predicate):
if len(predicates) < 2:
raise ValueError('provide at least 2 predicates')
self.predicates = predicates
def description(self, ctx: Context) -> str:
return self.DESC_SEP.join(
'(%s)' % p.description(ctx) if isinstance(p, _Operator)
else p.description(ctx)
for p in self.predicates
)
def __repr__(self):
return make_repr(self, *self.predicates)
class _And(_Operator):
"""Logical AND of two or more predicates."""
DESC_SEP = ' and '
def negated_description(self, ctx: Context) -> str:
return ' or '.join(
'(%s)' % p.neg_desc(ctx) if isinstance(p, _Operator) else p.neg_desc(ctx)
for p in self.predicates
)
def __call__(self, ctx: Context) -> bool:
return all(p(ctx) for p in self.predicates)
def __and__(self, other: 'Predicate') -> '_And':
if isinstance(other, _And):
return _And(*self.predicates, *other.predicates)
return _And(*self.predicates, other)
class _Or(_Operator):
"""Logical OR of two or more predicates."""
DESC_SEP = ' or '
def negated_description(self, ctx: Context) -> str:
return ' and '.join(
'(%s)' % p.neg_desc(ctx) if isinstance(p, _Operator) else p.neg_desc(ctx)
for p in self.predicates
)
def __call__(self, ctx: Context) -> bool:
return any(p(ctx) for p in self.predicates)
def __or__(self, other: 'Predicate') -> '_Or':
if isinstance(other, _Or):
return _Or(*self.predicates, *other.predicates)
return _Or(*self.predicates, other)
[docs]class IsSet(Predicate):
"""True if the parameter is set."""
def __init__(self, param_name: str):
self.param_name = param_name
[docs] def description(self, ctx: Context) -> str:
return '%s is set' % param_label_by_name(ctx, self.param_name)
[docs] def negated_description(self, ctx: Context) -> str:
return '%s is not set' % param_label_by_name(ctx, self.param_name)
[docs] def __call__(self, ctx: Context) -> bool:
command = ensure_constraints_support(ctx.command)
param = command.get_param_by_name(self.param_name)
value = param_value_by_name(ctx, self.param_name)
return param_value_is_set(param, value)
[docs] def __and__(self, other: Predicate):
if isinstance(other, IsSet):
return AllSet(self.param_name, other.param_name)
return super().__and__(other)
[docs] def __or__(self, other: Predicate):
if isinstance(other, IsSet):
return AnySet(self.param_name, other.param_name)
return super().__or__(other)
[docs]class AllSet(Predicate):
"""True if all listed parameters are set.
.. versionadded:: 0.8.0
"""
def __init__(self, *param_names: str):
if not param_names:
raise ValueError('you must provide at least one param name')
self.param_names = param_names
[docs] def negated_description(self, ctx: Context) -> str:
labels = get_param_labels(ctx, self.param_names)
if len(labels) == 1:
return f'{labels[0]} is not set'
pronoun = 'both' if len(labels) == 2 else 'all'
return f'{join_with_and(labels)} are not {pronoun} set'
[docs] def description(self, ctx: Context) -> str:
labels = get_param_labels(ctx, self.param_names)
if len(labels) == 1:
return f'{labels[0]} is set'
pronoun = 'both' if len(labels) == 2 else 'all'
return f'{join_with_and(labels)} are {pronoun} set'
[docs] def __call__(self, ctx: Context) -> bool:
command = ensure_constraints_support(ctx.command)
params = command.get_params_by_name(self.param_names)
return all(param_value_is_set(param, ctx.params[get_param_name(param)])
for param in params)
[docs] def __and__(self, other: Predicate):
if isinstance(other, AllSet):
return AllSet(*self.param_names, *other.param_names)
return super().__and__(other)
[docs]class AnySet(Predicate):
"""True if any of the listed parameters is set.
.. versionadded:: 0.8.0
"""
def __init__(self, *param_names: str):
if not param_names:
raise ValueError('you must provide at least one param name')
self.param_names = param_names
[docs] def negated_description(self, ctx: Context) -> str:
labels = get_param_labels(ctx, self.param_names)
if len(labels) == 1:
return f'{labels[0]} is not set'
if len(labels) == 2:
return 'neither {} nor {} is set'.format(*labels)
return f'none of {join_with_and(labels)} is set'
[docs] def description(self, ctx: Context) -> str:
labels = get_param_labels(ctx, self.param_names)
if len(labels) == 1:
return f'{labels[0]} is set'
if len(labels) == 2:
return 'either {} or {} is set'.format(*labels)
return f'any of {join_with_and(labels)} is set'
[docs] def __call__(self, ctx: Context) -> bool:
command = ensure_constraints_support(ctx.command)
params = command.get_params_by_name(self.param_names)
return any(param_value_is_set(param, ctx.params[get_param_name(param)])
for param in params)
[docs] def __or__(self, other: Predicate):
if isinstance(other, AnySet):
return AnySet(*self.param_names, *other.param_names)
return super().__or__(other)
[docs]class Equal(Predicate):
"""True if the parameter value equals ``value``."""
def __init__(self, param_name: str, value: Any):
self.param_name = param_name
self.value = value
[docs] def description(self, ctx: Context) -> str:
param_label = param_label_by_name(ctx, self.param_name)
return f'{param_label}="{self.value}"'
[docs] def negated_description(self, ctx: Context) -> str:
param_label = param_label_by_name(ctx, self.param_name)
return f'{param_label}!="{self.value}"'
[docs] def __call__(self, ctx: Context) -> bool:
return param_value_by_name(ctx, self.param_name) == self.value