Skip to content
This repository was archived by the owner on Jul 21, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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