Skip to content
This repository has been archived by the owner on Jul 21, 2022. It is now read-only.

Commit

Permalink
Merge ee303da into 39fa5c8
Browse files Browse the repository at this point in the history
  • Loading branch information
justanr committed Apr 28, 2019
2 parents 39fa5c8 + ee303da commit 1ad65da
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 48 deletions.
11 changes: 5 additions & 6 deletions src/flask_allows/allows.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from .additional import Additional, AdditionalManager
from .overrides import Override, OverrideManager


__all__ = ("Allows", "allows")


Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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")
47 changes: 37 additions & 10 deletions src/flask_allows/requirements.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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__)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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), {})
83 changes: 52 additions & 31 deletions test/test_requirement.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_cant_create_Requirement():


def test_call_fulfills_with_call(spy):
spy(object(), object())
spy(object())
assert spy.called


Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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()

Expand All @@ -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,), {}))
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 1ad65da

Please sign in to comment.