diff --git a/src/flask_allows/allows.py b/src/flask_allows/allows.py index 132ed3b..f1a6a66 100644 --- a/src/flask_allows/allows.py +++ b/src/flask_allows/allows.py @@ -10,7 +10,6 @@ from .additional import Additional, AdditionalManager from .overrides import Override, OverrideManager - __all__ = ("Allows", "allows") @@ -150,7 +149,7 @@ def fulfill(self, requirements, identity=None): r for r in all_requirements if r not in self.overrides.current ) - return all(_call_requirement(r, identity, request) for r in all_requirements) + return all(_call_requirement(r, identity) for r in all_requirements) def clear_all_overrides(self): """ @@ -233,18 +232,18 @@ def _make_callable(func_or_value): return func_or_value -def _call_requirement(req, user, request): +def _call_requirement(requirement, user): try: - return req(user) + return requirement(user) except TypeError: warnings.warn( "{!r}: Passing request to requirements is now deprecated" - " and will be removed in 1.0".format(req), + " and will be removed in 1.0".format(requirement), DeprecationWarning, stacklevel=2, ) - return req(user, request) + return requirement(user, request) allows = LocalProxy(__get_allows, name="flask-allows") diff --git a/src/flask_allows/requirements.py b/src/flask_allows/requirements.py index 28ad9ef..ab866b3 100644 --- a/src/flask_allows/requirements.py +++ b/src/flask_allows/requirements.py @@ -1,6 +1,8 @@ import operator from abc import ABCMeta, abstractmethod from functools import wraps +from inspect import isclass +from types import FunctionType from flask import request from flask._compat import with_metaclass @@ -27,7 +29,7 @@ class Requirement(with_metaclass(ABCMeta)): """ @abstractmethod - def fulfill(self, user, request=None): + def fulfill(self, user): """ Abstract method called to verify the requirement against the current user and request. @@ -40,8 +42,8 @@ def fulfill(self, user, request=None): """ return NotImplemented - def __call__(self, user, request): - return _call_requirement(self.fulfill, user, request) + def __call__(self, user): + return _call_requirement(self.fulfill, user) def __repr__(self): return "<{}()>".format(self.__class__.__name__) @@ -122,7 +124,7 @@ def Not(cls, *requirements): """ return cls(*requirements, negated=True) - def fulfill(self, user, request): + def fulfill(self, user): reduced = None requirements = self.requirements @@ -132,7 +134,7 @@ def fulfill(self, user, request): requirements = (r for r in requirements if r not in current_overrides) for r in requirements: - result = _call_requirement(r, user, request) + result = _call_requirement(r, user) if reduced is None: reduced = result @@ -195,23 +197,48 @@ def __hash__(self): ) -def wants_request(f): +def wants_request(f_or_cls): """ Helper decorator for transitioning to user-only requirements, this aids in situations where the request may be marked optional and causes an incorrect flow into user-only requirements. This decorator causes the requirement to look like a user-only requirement - but passes the current request context internally to the requirement. - - This decorator is intended only to assist during a transitionary phase - and will be removed in flask-allows 1.0 + but passes the current request context internally to the requirement. It + can be applied to a function requirement or a subclass of Requirement. See: :issue:`20,27` """ + if isclass(f_or_cls) and issubclass(f_or_cls, Requirement): + return _class_wants_request(f_or_cls) + + if isinstance(f_or_cls, FunctionType): + return _func_wants_request(f_or_cls) + + raise TypeError( + "Expected a function or subclass of Requirement. Got {}".format(f_or_cls) + ) + + +def _func_wants_request(f): @wraps(f) def wrapper(user): return f(user, request) return wrapper + + +class _OldStyleRequirement(Requirement): + """ + Used to provide an adaptation bridge to requirements that want the user + and request provided to fulfill rather than just user. + """ + + def __call__(self, user): + return self.fulfill(user, request) + + +def _class_wants_request(cls): + name = cls.__name__ + return type(name, (_OldStyleRequirement, cls), {}) diff --git a/test/test_requirement.py b/test/test_requirement.py index 8fb8608..e57c914 100644 --- a/test/test_requirement.py +++ b/test/test_requirement.py @@ -23,7 +23,7 @@ def test_cant_create_Requirement(): def test_call_fulfills_with_call(spy): - spy(object(), object()) + spy(object()) assert spy.called @@ -38,9 +38,9 @@ def test_ConditionalRequirement_defaults(always): ) -def test_empty_Conditional_is_True(member, request): +def test_empty_Conditional_is_True(member): Cond = ConditionalRequirement() - assert Cond(member, request) + assert Cond(member) def test_custom_ConditionalRequirement(always): @@ -88,16 +88,16 @@ def test_NotConditional_defaults(always): ) -def test_OrConditional_shortcircuit(always, never, member, request): +def test_OrConditional_shortcircuit(always, never, member): cond = Or(always, never) - cond.fulfill(member, request) + cond.fulfill(member) assert not never.called -def test_OrConditional_fulfills(always, never, member, request): - assert Or(always, never)(member, request) - assert Or(never, always)(member, request) +def test_OrConditional_fulfills(always, never, member): + assert Or(always, never)(member) + assert Or(never, always)(member) def test_OrConditional_shortcut(always): @@ -111,16 +111,16 @@ def test_OrConditional_shortcut(always): ) -def test_AndConditional_shortcircuit(always, never, member, request): +def test_AndConditional_shortcircuit(always, never, member): cond = And(never, always) - cond.fulfill(member, request) + cond.fulfill(member) assert not always.called -def test_AndConditional_fulfills(always, never, member, request): - assert not And(always, never)(member, request) - assert not And(never, always)(member, request) +def test_AndConditional_fulfills(always, never, member): + assert not And(always, never)(member) + assert not And(never, always)(member) def test_AndConditional_shortcut(always): @@ -146,43 +146,43 @@ def test_NotConditional_shortcut(always): ) -def test_NotConditional_singular_true(always, member, request): - assert not Not(always)(member, request) +def test_NotConditional_singular_true(always, member): + assert not Not(always)(member) -def test_NotConditional_singular_false(never, member, request): - assert Not(never)(member, request) +def test_NotConditional_singular_false(never, member): + assert Not(never)(member) -def test_NotConditional_many_all_true(always, member, request): - assert not Not(always, always)(member, request) +def test_NotConditional_many_all_true(always, member): + assert not Not(always, always)(member) -def test_NotConditional_many_all_false(never, member, request): - assert Not(never, never)(member, request) +def test_NotConditional_many_all_false(never, member): + assert Not(never, never)(member) -def test_NotConditional_many_mixed(always, never, member, request): - assert Not(always, never)(member, request) +def test_NotConditional_many_mixed(always, never, member): + assert Not(always, never)(member) -def test_supports_new_style_requirements(member, request): +def test_supports_new_style_requirements(member): class SomeRequirement(Requirement): def fulfill(self, user): return True - assert SomeRequirement()(member, request) + assert SomeRequirement()(member) -def test_ConditionalRequirement_supports_new_style_requirements(member, request): +def test_ConditionalRequirement_supports_new_style_requirements(member): def is_true(user): return True - assert C(is_true)(member, request) + assert C(is_true)(member) @pytest.mark.regression -def test_wants_request_stops_incorrect_useronly_flow(member, request): +def test_wants_request_stops_incorrect_useronly_flow(member): """ When a request parameter has a default value, requirement runners will incorrectly decide it is a user only requirement and not provide the @@ -200,13 +200,13 @@ def my_requirement(user, request=SENTINEL): assert allows.fulfill([wants_request(my_requirement)], member) -def test_conditional_skips_overridden_requirements(member, never, always, request): +def test_conditional_skips_overridden_requirements(member, never, always): manager = OverrideManager() manager.push(Override(never)) reqs = And(never, always) - assert reqs.fulfill(member, request) + assert reqs.fulfill(member) manager.pop() @@ -219,6 +219,27 @@ def test_conditional_skips_overridden_requirements_even_if_nested( reqs = And(And(And(always), Or(never))) - assert reqs.fulfill(member, request) + assert reqs.fulfill(member) manager.pop() + + +def test_wants_request_works_on_classes(request, member): + allows = Allows(app=None, identity_loader=lambda: member) + SENTINEL = object() + + @wants_request + class OldKindOfRequirement(Requirement): + def fulfill(self, user, request=SENTINEL): + return request is not SENTINEL + + assert "OldKindOfRequirement" == OldKindOfRequirement.__name__ + assert allows.fulfill([OldKindOfRequirement()], member) + + +def test_wants_request_errors_on_not_function_or_requirement(): + assert wants_request(lambda u, r: True) + assert wants_request(type("Test", (Requirement,), {})) + + with pytest.raises(TypeError): + wants_request(type("MoreTest", (object,), {})) diff --git a/tox.ini b/tox.ini index f04e9ad..541d41a 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,8 @@ max-line-length = 88 [pytest] norecursedirs = .tox .git .cache *.egg htmlcov -addopts = -vvl --capture fd --strict +addopts = -vvl --capture fd --strict -W error:::flask_allows + markers = regression: issue found that has been corrected but could arise again integration: used to run only integration tests