import operator
from abc import ABCMeta
from abc import abstractmethod
from collections.abc import Callable
from typing import Any
from .allows import _call_requirement
from .overrides import current_overrides
__all__ = (
"Requirement",
"ConditionalRequirement",
"C",
"Or",
"And",
"Not",
)
[docs]
class Requirement(metaclass=ABCMeta):
"""
Base for object based Requirements in Flask-Allows. This is quite
useful for requirements that have complex logic that is too much to fit
inside of a single function.
"""
[docs]
@abstractmethod
def fulfill(self, user) -> bool:
"""
Abstract method called to verify the requirement against the current
user and request.
.. versionchanged:: 0.5.0
Passing request is now deprecated, pending removal in version 1.0.0
:param user: The current identity
:param request: The current request.
"""
return NotImplemented
def __call__(self, user):
return _call_requirement(self.fulfill, user)
def __repr__(self):
return f"<{self.__class__.__name__}()>"
[docs]
class ConditionalRequirement(Requirement):
"""
Used to combine requirements together in ways other than all-or-nothing,
such as with an or-reducer (any requirement must be True)::
from flask_allows2 import Or
requires(Or(user_is_admin, user_is_moderator))
or negating a requirement::
from flask_allows2 import Not
requires(Not(user_logged_in))
Combinations may also nested::
Or(user_is_admin, And(user_is_moderator, HasPermission('view_admin')))
Custom combinators may be built by creating an instance of ConditionalRequirement
and supplying any combination of its keyword parameters
This class is also exported under the ``C`` alias.
:param requirements: Collection of requirements to combine into
one logical requirement
:param op: Optional, Keyword only. A binary operator that accepts two
booleans and returns a boolean.
:param until: Optional, Keyword only. A boolean to short circuit on (e.g.
if provided with True, then the first True evaluation to return from a
requirement ends verification)
:param negated: Optional, Keyword only. If true, then the
ConditionalRequirement will return the opposite of what it actually
evaluated to (e.g. ``ConditionalRequirement(user_logged_in, negated=True)``
returns False if the user is logged in)
"""
def __init__(
self, *requirements: Callable[[type["Requirement"]], bool], **kwargs: Any
):
self.requirements = requirements
self.op = kwargs.get("op", operator.and_)
self.until = kwargs.get("until")
self.negated = kwargs.get("negated")
[docs]
@classmethod
def And(cls, *requirements: Callable[[type["Requirement"]], bool]):
"""
Short cut helper to construct a combinator that uses
:meth:`operator.and_` to reduce requirement results and stops
evaluating on the first False.
This is also exported at the module level as ``And``
"""
return cls(*requirements, op=operator.and_, until=False)
[docs]
@classmethod
def Or(cls, *requirements: Callable[[type["Requirement"]], bool]):
"""
Short cut helper to construct a combinator that uses
:meth:`operator.or_` to reduce requirement results and stops evaluating
on the first True.
This is also exported at the module level as ``Or``
"""
return cls(*requirements, op=operator.or_, until=True)
[docs]
@classmethod
def Not(cls, *requirements: Callable[[type["Requirement"]], bool]):
"""
Shortcut helper to negate a requirement or requirements.
This is also exported at the module as ``Not``
"""
return cls(*requirements, negated=True)
[docs]
def fulfill(self, user: Any):
reduced = None
requirements = self.requirements
# can't use is because is a proxy
if current_overrides != None: # noqa: E711
requirements = tuple(r for r in requirements if r not in current_overrides)
for r in requirements:
result = _call_requirement(r, user)
if reduced is None:
reduced = result
else:
reduced = self.op(reduced, result)
if self.until == reduced:
break
if reduced is not None:
return not reduced if self.negated else reduced
return True
def __and__(self, require: Callable[[type["Requirement"]], bool]):
return self.And(self, require)
def __or__(self, require: Callable[[type["Requirement"]], bool]):
return self.Or(self, require)
def __invert__(self):
return self.Not(self)
def __repr__(self):
additional = []
for name in ["op", "negated", "until"]:
value = getattr(self, name)
if not value:
continue
additional.append(f"{name}={value!r}")
if additional:
additional = f" {', '.join(additional)}"
else:
additional = ""
return f"<{self.__class__.__name__} requirements={self.requirements!r}{additional}>" # noqa: E501
def __eq__(self, other):
return (
isinstance(other, ConditionalRequirement)
and self.op == other.op
and self.until == other.until
and self.negated == other.negated
and self.requirements == other.requirements
)
def __hash__(self):
return hash((self.requirements, self.op, self.until, self.negated))
(C, And, Or, Not) = (
ConditionalRequirement,
ConditionalRequirement.And,
ConditionalRequirement.Or,
ConditionalRequirement.Not,
)