diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6546a3e..0000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[report] -exclude_lines = - pragma: no cover -omit = - */python?.?/* - */site-packages/nose/* - */rules/compat/* - */rules/apps.py diff --git a/.travis.yml b/.travis.yml index 4d9efc6..a9e7c48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,47 +1,20 @@ -# --------------------------------------------------------------------------- -# Python support matrix per Django version -# -# 2.6 1.5 1.6 -# 2.7 1.5 1.6 1.7 1.8 1.9 1.10 1.11 -# 3.3 1.5 1.6 1.7 1.8 -# 3.4 1.7 1.8 1.9 1.10 1.11 2.0 -# 3.5 1.8 1.9 1.10 1.11 2.0 -# 3.6 1.10 1.11 2.0 -# --------------------------------------------------------------------------- sudo: false language: python +python: + - "2.7" + - "3.4" + - "3.5" + - "3.6" + - "pypy" + - "pypy3" + matrix: include: - - { python: 2.6, env: TOXENV=py26-django15 } - - { python: 2.6, env: TOXENV=py26-django16 } - - { python: 2.7, env: TOXENV=py27-django15 } - - { python: 2.7, env: TOXENV=py27-django16 } - - { python: 2.7, env: TOXENV=py27-django17 } - - { python: 2.7, env: TOXENV=py27-django18 } - - { python: 2.7, env: TOXENV=py27-django19 } - - { python: 2.7, env: TOXENV=py27-django110 } - - { python: 2.7, env: TOXENV=py27-django111 } - - { python: 3.3, env: TOXENV=py33-django15 } - - { python: 3.3, env: TOXENV=py33-django16 } - - { python: 3.3, env: TOXENV=py33-django17 } - - { python: 3.3, env: TOXENV=py33-django18 } - - { python: 3.4, env: TOXENV=py34-django17 } - - { python: 3.4, env: TOXENV=py34-django18 } - - { python: 3.4, env: TOXENV=py34-django19 } - - { python: 3.4, env: TOXENV=py34-django110 } - - { python: 3.4, env: TOXENV=py34-django20 } - - { python: 3.4, env: TOXENV=py34-django111 } - - { python: 3.5, env: TOXENV=py35-django18 } - - { python: 3.5, env: TOXENV=py35-django19 } - - { python: 3.5, env: TOXENV=py35-django110 } - - { python: 3.5, env: TOXENV=py35-django111 } - - { python: 3.5, env: TOXENV=py35-django20 } - - { python: 3.6, env: TOXENV=py36-django110 } - - { python: 3.6, env: TOXENV=py36-django111 } - - { python: 3.6, env: TOXENV=py36-django20 } + - {python: "3.6", env: TOXENV=packaging} + install: - - if [[ $TRAVIS_PYTHON_VERSION == 3.3 ]]; then pip install virtualenv==15.2.0; fi - - pip install tox + - pip install tox tox-travis tox-venv + - pip install coveralls script: tox after_success: - coveralls diff --git a/README.rst b/README.rst index f5a155d..e307ae8 100644 --- a/README.rst +++ b/README.rst @@ -79,8 +79,8 @@ Table of Contents Requirements ============ -``rules`` requires Python 2.6/3.3 or newer. It can optionally integrate with -Django, in which case requires Django 1.5 or newer. +``rules`` requires Python 2.7/3.4 or newer. It can optionally integrate with +Django, in which case requires Django 1.11 or newer. How to install diff --git a/rules/compat/access_mixins.py b/rules/compat/access_mixins.py deleted file mode 100644 index 8638c6f..0000000 --- a/rules/compat/access_mixins.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -This is a copy of django.contrib.auth.mixins module, shipped with django-rules -for compatibility with Django versions before 1.9. Used under permission by -the Django Software Foundation. - -Copyright (c) Django Software Foundation and individual contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of Django nor the names of its contributors may be used - to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" - -from django.conf import settings -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login -from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.utils import six -from django.utils.encoding import force_text - - -class AccessMixin(object): - """ - Abstract CBV mixin that gives access mixins the same customizable - functionality. - """ - login_url = None - permission_denied_message = '' - raise_exception = False - redirect_field_name = REDIRECT_FIELD_NAME - - def get_login_url(self): - """ - Override this method to override the login_url attribute. - """ - login_url = self.login_url or settings.LOGIN_URL - if not login_url: - raise ImproperlyConfigured( - '{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override ' - '{0}.get_login_url().'.format(self.__class__.__name__) - ) - return force_text(login_url) - - def get_permission_denied_message(self): - """ - Override this method to override the permission_denied_message attribute. - """ - return self.permission_denied_message - - def get_redirect_field_name(self): - """ - Override this method to override the redirect_field_name attribute. - """ - return self.redirect_field_name - - def handle_no_permission(self): - if self.raise_exception: - raise PermissionDenied(self.get_permission_denied_message()) - return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) - - -class LoginRequiredMixin(AccessMixin): - """ - CBV mixin which verifies that the current user is authenticated. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated(): - return self.handle_no_permission() - return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) - - -class PermissionRequiredMixin(AccessMixin): - """ - CBV mixin which verifies that the current user has all specified - permissions. - """ - permission_required = None - - def get_permission_required(self): - """ - Override this method to override the permission_required attribute. - Must return an iterable. - """ - if self.permission_required is None: - raise ImproperlyConfigured( - '{0} is missing the permission_required attribute. Define {0}.permission_required, or override ' - '{0}.get_permission_required().'.format(self.__class__.__name__) - ) - if isinstance(self.permission_required, six.string_types): - perms = (self.permission_required, ) - else: - perms = self.permission_required - return perms - - def has_permission(self): - """ - Override this method to customize the way permissions are checked. - """ - perms = self.get_permission_required() - return self.request.user.has_perms(perms) - - def dispatch(self, request, *args, **kwargs): - if not self.has_permission(): - return self.handle_no_permission() - return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs) - - -class UserPassesTestMixin(AccessMixin): - """ - CBV Mixin that allows you to define a test function which must return True - if the current user can access the view. - """ - - def test_func(self): - raise NotImplementedError( - '{0} is missing the implementation of the test_func() method.'.format(self.__class__.__name__) - ) - - def get_test_func(self): - """ - Override this method to use a different test_func method. - """ - return self.test_func - - def dispatch(self, request, *args, **kwargs): - user_test_result = self.get_test_func()() - if not user_test_result: - return self.handle_no_permission() - return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs) diff --git a/rules/contrib/admin.py b/rules/contrib/admin.py index 794dff1..0ba4745 100644 --- a/rules/contrib/admin.py +++ b/rules/contrib/admin.py @@ -1,11 +1,5 @@ from django.contrib import admin - -try: - from django.contrib.auth import get_permission_codename -except ImportError: # pragma: no cover - # Django < 1.6 - def get_permission_codename(action, opts): - return '%s_%s' % (action, opts.object_name.lower()) +from django.contrib.auth import get_permission_codename class ObjectPermissionsModelAdminMixin(object): diff --git a/rules/contrib/views.py b/rules/contrib/views.py index 136690c..0440a8e 100644 --- a/rules/contrib/views.py +++ b/rules/contrib/views.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import mixins from django.contrib.auth.views import redirect_to_login from django.core.exceptions import PermissionDenied, ImproperlyConfigured, FieldError from django.shortcuts import get_object_or_404 @@ -9,12 +10,6 @@ from django.utils.decorators import available_attrs from django.utils.encoding import force_text -try: - from django.contrib.auth import mixins -except ImportError: # pragma: no cover - # Django < 1.9 - from ..compat import access_mixins as mixins - # These are made available for convenience, as well as for use in Django # versions before 1.9. For usage help see Django's docs for 1.9 or later. @@ -124,7 +119,7 @@ def _wrapped_view(request, *args, **kwargs): # Get the object to check permissions against if callable(fn): obj = fn(request, *args, **kwargs) - else: + else: # pragma: no cover obj = fn # Get the user diff --git a/rules/templatetags/rules.py b/rules/templatetags/rules.py index 8e15eb4..99470bf 100644 --- a/rules/templatetags/rules.py +++ b/rules/templatetags/rules.py @@ -4,19 +4,14 @@ register = template.Library() -try: - # Django < 2.0 - simple_tag = register.assignment_tag -except AttributeError: # pragma: no cover - simple_tag = register.simple_tag -@simple_tag +@register.simple_tag def test_rule(name, obj=None, target=None): return default_rules.test_rule(name, obj, target) -@simple_tag +@register.simple_tag def has_perm(perm, user, obj=None): if not hasattr(user, 'has_perm'): # pragma: no cover return False # swapped user model that doesn't support permissions diff --git a/runtests.py b/runtests.py deleted file mode 100755 index b765901..0000000 --- a/runtests.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import sys -from os import environ -from os.path import abspath, dirname, join - -import nose - - -def main(): - project_dir = dirname(abspath(__file__)) - - # setup path - sys.path.insert(0, project_dir) # project dir - sys.path.insert(0, join(project_dir, 'tests')) # tests dir - - environ['DJANGO_SETTINGS_MODULE'] = 'testapp.settings' - - try: - # django >= 1.7 - from django import setup - except ImportError: - pass - else: - setup() - - # setup test env - from django.test.utils import setup_test_environment - setup_test_environment() - - # setup db - from django.core.management import call_command, CommandError - options = { - 'interactive': False, - 'verbosity': 1, - } - try: - call_command('migrate', **options) - except CommandError: # Django < 1.7 - call_command('syncdb', **options) - - # run tests - return nose.main() - - -if __name__ == '__main__': - main() diff --git a/runtests.sh b/runtests.sh index 1d33c60..c6363c2 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,7 +1,6 @@ #!/bin/sh -coverage run --source=rules runtests.py --nologcapture --nocapture "$@" -result=$? -echo -coverage report -m -echo -exit $result + +# NOTE: Make sure you `pip install -e .` first +coverage run tests/manage.py test --failfast -v2 testsuite "$@" \ + && echo \ + && coverage report -m diff --git a/setup.cfg b/setup.cfg index e8b1e70..2f8ffce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,12 @@ -[nosetests] -verbosity=2 -cover-package=rules -cover-erase=1 -#cover-branches=1 +[bdist_wheel] +universal = 1 + +[coverage:run] +source = rules + +[coverage:report] +exclude_lines = + pragma: no cover +omit = + */rules/apps.py + */rules/compat/* diff --git a/setup.py b/setup.py index 76bd70e..aebe181 100644 --- a/setup.py +++ b/setup.py @@ -52,15 +52,6 @@ def get_version(version): 'rules.compat', ], - install_requires=[ - # 'Django >= 1.5', - ], - tests_require=[ - 'nose', - 'coverage', - 'Django >= 1.5', - ], - classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -69,9 +60,9 @@ def get_version(version): 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', diff --git a/tests/manage.py b/tests/manage.py index 19ecff9..838b40a 100755 --- a/tests/manage.py +++ b/tests/manage.py @@ -2,13 +2,8 @@ import os import sys -from os.path import abspath, dirname, join if __name__ == "__main__": - project_dir = dirname(dirname(abspath(__file__))) - sys.path.insert(0, project_dir) - sys.path.insert(0, join(project_dir, 'tests')) - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") from django.core.management import execute_from_command_line diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index e2e4b08..e78b032 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -3,7 +3,6 @@ BASE_DIR = dirname(dirname(abspath(__file__))) DEBUG = True -TEMPLATE_DEBUG = DEBUG ADMINS = [ ('test@example.com', 'Administrator'), @@ -32,9 +31,6 @@ 'django.contrib.messages.middleware.MessageMiddleware', ] -# Django < 2.0 -MIDDLEWARE_CLASSES = MIDDLEWARE - AUTHENTICATION_BACKENDS = [ 'rules.permissions.ObjectPermissionBackend', 'django.contrib.auth.backends.ModelBackend', @@ -52,6 +48,7 @@ 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { + 'debug': DEBUG, 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', diff --git a/tests/testapp/views.py b/tests/testapp/views.py index f294d7b..fb9ecec 100644 --- a/tests/testapp/views.py +++ b/tests/testapp/views.py @@ -67,6 +67,6 @@ class ViewWithPermissionList(LoginRequiredMixin, PermissionRequiredMixin, BookMi permission_required = ['testapp.change_book', 'testapp.delete_book'] -@permission_required('testapp.delete_book', fn=Book.objects.get(pk=1)) +@permission_required('testapp.delete_book', fn=objectgetter(Book, 'book_id')) def view_with_object(request, book_id): return HttpResponse('OK') diff --git a/tests/testsuite/_test_predicates_kwonly.py b/tests/testsuite/_test_predicates_kwonly.py index b57f05f..7e05530 100644 --- a/tests/testsuite/_test_predicates_kwonly.py +++ b/tests/testsuite/_test_predicates_kwonly.py @@ -1,19 +1,21 @@ -from nose.tools import assert_raises -from functools import partial +from unittest import TestCase from rules.predicates import predicate -def test_predicate_kwargonly(): - def p(foo, *, bar): - return True - assert_raises(TypeError, predicate, p) +class PredicateKwonlyTests(TestCase): + def test_predicate_kwargonly(self): + def p(foo, *, bar): + return True + with self.assertRaises(TypeError): + predicate(p) - def p2(foo, *a, bar): - return True - assert_raises(TypeError, predicate, p2) + def p2(foo, *a, bar): + return True + with self.assertRaises(TypeError): + predicate(p2) - def p3(foo, *, bar='bar'): - return True - # Should not fail - predicate(p3) + def p3(foo, *, bar='bar'): + return True + # Should not fail + predicate(p3) diff --git a/tests/testsuite/contrib/__init__.py b/tests/testsuite/contrib/__init__.py index 1b012fd..af84c82 100644 --- a/tests/testsuite/contrib/__init__.py +++ b/tests/testsuite/contrib/__init__.py @@ -7,27 +7,24 @@ ISBN = '978-1-4302-1936-1' -def setup_package(): - adrian = User.objects.create_user('adrian', password='secr3t') - adrian.is_superuser = True - adrian.is_staff = True - adrian.save() - - martin = User.objects.create_user('martin', password='secr3t') - martin.is_staff = True - martin.save() - - editors = Group.objects.create(name='editors') - martin.groups.add(editors) - - Book.objects.create( - isbn=ISBN, - title='The Definitive Guide to Django', - author=adrian) - - -def teardown_package(): - Book.objects.get(isbn=ISBN).delete() - Group.objects.get(name='editors').delete() - User.objects.get(username='adrian').delete() - User.objects.get(username='martin').delete() +class TestData: + @classmethod + def setUpTestData(cls): + adrian = User.objects.create_user( + 'adrian', + password='secr3t', + is_superuser=True, + is_staff=True) + + martin = User.objects.create_user( + 'martin', + password='secr3t', + is_staff=True) + + editors = Group.objects.create(name='editors') + martin.groups.add(editors) + + Book.objects.create( + isbn=ISBN, + title='The Definitive Guide to Django', + author=adrian) diff --git a/tests/testsuite/contrib/test_admin.py b/tests/testsuite/contrib/test_admin.py index e32108c..79873d6 100644 --- a/tests/testsuite/contrib/test_admin.py +++ b/tests/testsuite/contrib/test_admin.py @@ -1,18 +1,10 @@ from django.test import TestCase -try: - from django.urls import reverse -except ImportError: - # django < 1.10 - from django.core.urlresolvers import reverse +from django.urls import reverse -try: - from django.urls import reverse -except ImportError: - # Django < 2.0 - from django.core.urlresolvers import reverse +from . import TestData -class ModelAdminTests(TestCase): +class ModelAdminTests(TestData, TestCase): def test_change_book(self): # adrian can change his book as its author self.assertTrue(self.client.login(username='adrian', password='secr3t')) diff --git a/tests/testsuite/contrib/test_predicates.py b/tests/testsuite/contrib/test_predicates.py index 7459a99..795e53c 100644 --- a/tests/testsuite/contrib/test_predicates.py +++ b/tests/testsuite/contrib/test_predicates.py @@ -1,44 +1,45 @@ from django.contrib.auth.models import User +from django.test import TestCase + from rules.predicates import (is_authenticated, is_superuser, is_staff, is_active, is_group_member) +from . import TestData + class SwappedUser(object): pass -def test_is_authenticated(): - assert is_authenticated(User.objects.get(username='adrian')) - assert not is_authenticated(SwappedUser()) - - -def test_is_superuser(): - assert is_superuser(User.objects.get(username='adrian')) - assert not is_superuser(SwappedUser()) - - -def test_is_staff(): - assert is_staff(User.objects.get(username='adrian')) - assert not is_staff(SwappedUser()) +class PredicateTests(TestData, TestCase): + def test_is_authenticated(self): + assert is_authenticated(User.objects.get(username='adrian')) + assert not is_authenticated(SwappedUser()) + def test_is_superuser(self): + assert is_superuser(User.objects.get(username='adrian')) + assert not is_superuser(SwappedUser()) -def test_is_active(): - assert is_active(User.objects.get(username='adrian')) - assert not is_active(SwappedUser()) + def test_is_staff(self): + assert is_staff(User.objects.get(username='adrian')) + assert not is_staff(SwappedUser()) + def test_is_active(self): + assert is_active(User.objects.get(username='adrian')) + assert not is_active(SwappedUser()) -def test_is_group_member(): - p1 = is_group_member('somegroup') - assert p1.name == 'is_group_member:somegroup' - assert p1.num_args == 1 + def test_is_group_member(self): + p1 = is_group_member('somegroup') + assert p1.name == 'is_group_member:somegroup' + assert p1.num_args == 1 - p2 = is_group_member('g1', 'g2', 'g3', 'g4') - assert p2.name == 'is_group_member:g1,g2,g3,...' + p2 = is_group_member('g1', 'g2', 'g3', 'g4') + assert p2.name == 'is_group_member:g1,g2,g3,...' - p = is_group_member('editors') - assert p(User.objects.get(username='martin')) - assert not p(SwappedUser()) + p = is_group_member('editors') + assert p(User.objects.get(username='martin')) + assert not p(SwappedUser()) - p = is_group_member('editors', 'staff') - assert not p(User.objects.get(username='martin')) - assert not p(SwappedUser()) + p = is_group_member('editors', 'staff') + assert not p(User.objects.get(username='martin')) + assert not p(SwappedUser()) diff --git a/tests/testsuite/contrib/test_templatetags.py b/tests/testsuite/contrib/test_templatetags.py index 856b8b8..2a4b9bd 100644 --- a/tests/testsuite/contrib/test_templatetags.py +++ b/tests/testsuite/contrib/test_templatetags.py @@ -4,10 +4,10 @@ from testapp.models import Book -from . import ISBN +from . import ISBN, TestData -class TemplateTagTests(TestCase): +class TemplateTagTests(TestData, TestCase): tpl_format = """{{% spaceless %}} {{% load rules %}} {{% {tag} "{name}" user book as can_update %}} diff --git a/tests/testsuite/contrib/test_views.py b/tests/testsuite/contrib/test_views.py index 2b0d6e9..15a6af3 100644 --- a/tests/testsuite/contrib/test_views.py +++ b/tests/testsuite/contrib/test_views.py @@ -3,20 +3,17 @@ from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest, Http404 from django.test import TestCase +from django.urls import reverse from django.utils.encoding import force_str -try: - from django.urls import reverse -except ImportError: - # Django < 2.0 - from django.core.urlresolvers import reverse - from rules.contrib.views import objectgetter from testapp.models import Book +from . import TestData + -class FBVDecoratorTests(TestCase): +class FBVDecoratorTests(TestData, TestCase): def test_objectgetter(self): request = HttpRequest() book = Book.objects.get(pk=1) @@ -92,7 +89,7 @@ def test_permission_required(self): self.assertEqual(response.status_code, 302) -class CBVMixinTests(TestCase): +class CBVMixinTests(TestData, TestCase): def test_get_object_error(self): self.assertTrue(self.client.login(username='adrian', password='secr3t')) with self.assertRaises(AttributeError): diff --git a/tests/testsuite/test_permissions.py b/tests/testsuite/test_permissions.py index 5e3e08c..e35c6b4 100644 --- a/tests/testsuite/test_permissions.py +++ b/tests/testsuite/test_permissions.py @@ -1,42 +1,49 @@ -from nose.tools import with_setup, assert_raises +from unittest import TestCase from rules.predicates import always_true, always_false from rules.permissions import (permissions, add_perm, replace_perm, remove_perm, perm_exists, has_perm, ObjectPermissionBackend) -def reset_ruleset(ruleset): - def fn(): +class PermissionsTests(TestCase): + @staticmethod + def reset_ruleset(ruleset): for k in list(ruleset.keys()): ruleset.pop(k) - return fn - - -@with_setup(reset_ruleset(permissions), reset_ruleset(permissions)) -def test_permissions_ruleset(): - add_perm('can_edit_book', always_true) - assert 'can_edit_book' in permissions - assert perm_exists('can_edit_book') - assert has_perm('can_edit_book') - replace_perm('can_edit_book', always_false) - assert not has_perm('can_edit_book') - assert_raises(KeyError, replace_perm, 'someotherperm', always_false) - remove_perm('can_edit_book') - assert not perm_exists('can_edit_book') - - -@with_setup(reset_ruleset(permissions), reset_ruleset(permissions)) -def test_backend(): - backend = ObjectPermissionBackend() - assert backend.authenticate('someuser', 'password') is None - - add_perm('can_edit_book', always_true) - assert 'can_edit_book' in permissions - assert backend.has_perm(None, 'can_edit_book') - assert backend.has_module_perms(None, 'can_edit_book') - assert_raises(KeyError, add_perm, 'can_edit_book', always_true) - replace_perm('can_edit_book', always_false) - assert not backend.has_perm(None, 'can_edit_book') - assert_raises(KeyError, replace_perm, 'someotherperm', always_false) - remove_perm('can_edit_book') - assert not perm_exists('can_edit_book') + + def setUp(self): + self.reset_ruleset(permissions) + + def tearDown(self): + self.reset_ruleset(permissions) + + def test_permissions_ruleset(self): + add_perm('can_edit_book', always_true) + assert 'can_edit_book' in permissions + assert perm_exists('can_edit_book') + assert has_perm('can_edit_book') + with self.assertRaises(KeyError): + add_perm('can_edit_book', always_false) + replace_perm('can_edit_book', always_false) + assert not has_perm('can_edit_book') + with self.assertRaises(KeyError): + replace_perm('someotherperm', always_false) + remove_perm('can_edit_book') + assert not perm_exists('can_edit_book') + + def test_backend(self): + backend = ObjectPermissionBackend() + assert backend.authenticate('someuser', 'password') is None + + add_perm('can_edit_book', always_true) + assert 'can_edit_book' in permissions + assert backend.has_perm(None, 'can_edit_book') + assert backend.has_module_perms(None, 'can_edit_book') + with self.assertRaises(KeyError): + add_perm('can_edit_book', always_true) + replace_perm('can_edit_book', always_false) + assert not backend.has_perm(None, 'can_edit_book') + with self.assertRaises(KeyError): + replace_perm('someotherperm', always_false) + remove_perm('can_edit_book') + assert not perm_exists('can_edit_book') diff --git a/tests/testsuite/test_predicates.py b/tests/testsuite/test_predicates.py index 9ac4c7a..f0f28e0 100644 --- a/tests/testsuite/test_predicates.py +++ b/tests/testsuite/test_predicates.py @@ -1,6 +1,7 @@ import sys import functools import warnings +from unittest import TestCase from rules.predicates import ( NO_VALUE, @@ -16,433 +17,399 @@ from ._test_predicates_kwonly import * -def test_always_true(): - assert always_true() - - -def test_always_false(): - assert not always_false() - - -def test_always_allow(): - assert always_allow() - - -def test_always_deny(): - assert not always_deny() - - -def test_lambda_predicate(): - p = Predicate(lambda x: x == 'a') - assert p.name == '' - assert p.num_args == 1 - assert p('a') - - -def test_lambda_predicate_custom_name(): - p = Predicate(lambda x: x == 'a', name='mypred') - assert p.name == 'mypred' - assert p.num_args == 1 - assert p('a') - - -def test_function_predicate(): - def mypred(x): - return x == 'a' - p = Predicate(mypred) - assert p.name == 'mypred' - assert p.num_args == 1 - assert p('a') - - -def test_function_predicate_custom_name(): - def mypred(x): - return x == 'a' - p = Predicate(mypred, name='foo') - assert p.name == 'foo' - assert p.num_args == 1 - assert p('a') - - -def test_partial_function_predicate(): - def mypred(one, two, three): - return one < two < three - p = Predicate(functools.partial(mypred, 1)) - assert p.name == 'mypred' - assert p.num_args == 2 # 3 - 1 partial - assert p(2, 3) - p = Predicate(functools.partial(mypred, 1, 2)) - assert p.name == 'mypred' - assert p.num_args == 1 # 3 - 2 partial - assert p(3) - - -def test_method_predicate(): - class SomeClass(object): - def some_method(self, arg1, arg2): - return arg1 == arg2 - obj = SomeClass() - p = Predicate(obj.some_method) - assert p.name == 'some_method' - assert p.num_args == 2 - assert p(2, 2) - - -def test_partial_method_predicate(): - class SomeClass(object): - def some_method(self, arg1, arg2): - return arg1 == arg2 - obj = SomeClass() - p = Predicate(functools.partial(obj.some_method, 2)) - assert p.name == 'some_method' - assert p.num_args == 1 - assert p(2) - - -def test_class_predicate(): - class callableclass(object): - def __call__(self, arg1, arg2): - return arg1 == arg2 - fn = callableclass() - p = Predicate(fn) - assert p.name == 'callableclass' - assert p.num_args == 2 - assert p('a', 'a') - - -def test_class_predicate_custom_name(): - class callableclass(object): - def __call__(self, arg): - return arg == 'a' - fn = callableclass() - p = Predicate(fn, name='bar') - assert p.name == 'bar' - assert p.num_args == 1 - assert p('a') - - -def test_predicate_predicate(): - def mypred(x): - return x == 'a' - p = Predicate(Predicate(mypred)) - assert p.name == 'mypred' - assert p.num_args == 1 - assert p('a') - - -def test_predicate_predicate_custom_name(): - def mypred(x): - return x == 'a' - p = Predicate(Predicate(mypred, name='foo')) - assert p.name == 'foo' - assert p.num_args == 1 - assert p('a') - - -def test_predicate_bind(): - @predicate(bind=True) - def is_bound(self): - return self is is_bound - - assert is_bound() - - p = None - - def mypred(self): - return self is p - - p = Predicate(mypred, bind=True) - assert p() - - -def test_decorator(): - @predicate - def mypred(arg1, arg2): - return True - assert mypred.name == 'mypred' - assert mypred.num_args == 2 - - -def test_decorator_noargs(): - @predicate() - def mypred(arg1, arg2): - return True - assert mypred.name == 'mypred' - assert mypred.num_args == 2 - - -def test_decorator_custom_name(): - @predicate('foo') - def mypred(): - return True - assert mypred.name == 'foo' - assert mypred.num_args == 0 - - @predicate(name='bar') - def myotherpred(): - return False - assert myotherpred.name == 'bar' - assert myotherpred.num_args == 0 - - -def test_repr(): - @predicate - def mypred(arg1, arg2): - return True - assert repr(mypred).startswith(' 0 - assert len(kwargs) == 0 - assert p.num_args == 0 - p.test('a') - p.test('a', 'b') - - -def test_no_args(): - @predicate - def p(*args, **kwargs): - assert len(args) == 0 - assert len(kwargs) == 0 - assert p.num_args == 0 - p.test() - - -def test_one_arg(): - @predicate - def p(a=None, *args, **kwargs): - assert len(args) == 0 - assert len(kwargs) == 0 - assert a == 'a' - assert p.num_args == 1 - p.test('a') - - -def test_two_args(): - @predicate - def p(a=None, b=None, *args, **kwargs): - assert len(args) == 0 - assert len(kwargs) == 0 - assert a == 'a' - assert b == 'b' - assert p.num_args == 2 - p.test('a', 'b') - - -def test_no_mask(): - @predicate - def p(a=None, b=None, *args, **kwargs): - assert len(args) == 0 - assert len(kwargs) == 1 - 'c' in kwargs - assert a == 'a' - assert b == 'b' - p('a', b='b', c='c') - - -def test_no_value_marker(): - @predicate - def p(a, b=None): - assert a == 'a' - assert b is None - - assert not NO_VALUE - p.test('a') - p.test('a', NO_VALUE) - - -def test_short_circuit(): - @predicate - def skipped_predicate(self): - return None - - @predicate - def shorted_predicate(self): - raise Exception('this predicate should not be evaluated') - - assert (always_false & shorted_predicate).test() is False - assert (always_true | shorted_predicate).test() is True - - def raises(pred): - try: - pred.test() +class PredicateTests(TestCase): + def test_always_true(self): + assert always_true() + + def test_always_false(self): + assert not always_false() + + def test_always_allow(self): + assert always_allow() + + def test_always_deny(self): + assert not always_deny() + + def test_lambda_predicate(self): + p = Predicate(lambda x: x == 'a') + assert p.name == '' + assert p.num_args == 1 + assert p('a') + + def test_lambda_predicate_custom_name(self): + p = Predicate(lambda x: x == 'a', name='mypred') + assert p.name == 'mypred' + assert p.num_args == 1 + assert p('a') + + def test_function_predicate(self): + def mypred(x): + return x == 'a' + p = Predicate(mypred) + assert p.name == 'mypred' + assert p.num_args == 1 + assert p('a') + + def test_function_predicate_custom_name(self): + def mypred(x): + return x == 'a' + p = Predicate(mypred, name='foo') + assert p.name == 'foo' + assert p.num_args == 1 + assert p('a') + + def test_partial_function_predicate(self): + def mypred(one, two, three): + return one < two < three + p = Predicate(functools.partial(mypred, 1)) + assert p.name == 'mypred' + assert p.num_args == 2 # 3 - 1 partial + assert p(2, 3) + p = Predicate(functools.partial(mypred, 1, 2)) + assert p.name == 'mypred' + assert p.num_args == 1 # 3 - 2 partial + assert p(3) + + def test_method_predicate(self): + class SomeClass(object): + def some_method(self, arg1, arg2): + return arg1 == arg2 + obj = SomeClass() + p = Predicate(obj.some_method) + assert p.name == 'some_method' + assert p.num_args == 2 + assert p(2, 2) + + def test_partial_method_predicate(self): + class SomeClass(object): + def some_method(self, arg1, arg2): + return arg1 == arg2 + obj = SomeClass() + p = Predicate(functools.partial(obj.some_method, 2)) + assert p.name == 'some_method' + assert p.num_args == 1 + assert p(2) + + def test_class_predicate(self): + class callableclass(object): + def __call__(self, arg1, arg2): + return arg1 == arg2 + fn = callableclass() + p = Predicate(fn) + assert p.name == 'callableclass' + assert p.num_args == 2 + assert p('a', 'a') + + def test_class_predicate_custom_name(self): + class callableclass(object): + def __call__(self, arg): + return arg == 'a' + fn = callableclass() + p = Predicate(fn, name='bar') + assert p.name == 'bar' + assert p.num_args == 1 + assert p('a') + + def test_predicate_predicate(self): + def mypred(x): + return x == 'a' + p = Predicate(Predicate(mypred)) + assert p.name == 'mypred' + assert p.num_args == 1 + assert p('a') + + def test_predicate_predicate_custom_name(self): + def mypred(x): + return x == 'a' + p = Predicate(Predicate(mypred, name='foo')) + assert p.name == 'foo' + assert p.num_args == 1 + assert p('a') + + def test_predicate_bind(self): + @predicate(bind=True) + def is_bound(self): + return self is is_bound + + assert is_bound() + + p = None + + def mypred(self): + return self is p + + p = Predicate(mypred, bind=True) + assert p() + + def test_decorator(self): + @predicate + def mypred(arg1, arg2): + return True + assert mypred.name == 'mypred' + assert mypred.num_args == 2 + + def test_decorator_noargs(self): + @predicate() + def mypred(arg1, arg2): + return True + assert mypred.name == 'mypred' + assert mypred.num_args == 2 + + def test_decorator_custom_name(self): + @predicate('foo') + def mypred(): + return True + assert mypred.name == 'foo' + assert mypred.num_args == 0 + + @predicate(name='bar') + def myotherpred(): return False - except Exception as e: - return 'evaluated' in str(e) - - assert raises(always_true & shorted_predicate) - assert raises(always_false | shorted_predicate) - assert raises(skipped_predicate & shorted_predicate) - assert raises(skipped_predicate | shorted_predicate) - - -def test_skip_predicate_deprecation(): - @predicate(bind=True) - def skipped_predicate(self): - self.skip() - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - assert skipped_predicate.test() is False - assert len(w) == 1 and 'deprecated' in str(w[-1].message) - - -def test_skip_predicate(): - @predicate(bind=True) - def requires_two_args(self, a, b): - return a == b if len(self.context.args) > 1 else None - - @predicate - def passthrough(a): - return a - - assert (requires_two_args & passthrough).test(True, True) is True - assert (requires_two_args & passthrough).test(True, False) is False - - # because requires_two_args is called with only one argument - # its result is not taken into account, only the result of the - # other predicate matters. - assert (requires_two_args & passthrough).test(True) is True - assert (requires_two_args & passthrough).test(False) is False - assert (requires_two_args | passthrough).test(True) is True - assert (requires_two_args | passthrough).test(False) is False - - # test that order does not matter - assert (passthrough & requires_two_args).test(True) is True - assert (passthrough & requires_two_args).test(False) is False - assert (passthrough | requires_two_args).test(True) is True - assert (passthrough | requires_two_args).test(False) is False - - # test that inversion does not modify the result - assert (~requires_two_args & passthrough).test(True) is True - assert (~requires_two_args & passthrough).test(False) is False - assert (~requires_two_args | passthrough).test(True) is True - assert (~requires_two_args | passthrough).test(False) is False - assert (passthrough & ~requires_two_args).test(True) is True - assert (passthrough & ~requires_two_args).test(False) is False - assert (passthrough | ~requires_two_args).test(True) is True - assert (passthrough | ~requires_two_args).test(False) is False - - # test that when all predicates are skipped, result is False - assert requires_two_args.test(True) is False - assert (requires_two_args | requires_two_args).test(True) is False - assert (requires_two_args & requires_two_args).test(True) is False - - # test that a skipped predicate doesn't alter the result at all - assert (requires_two_args | requires_two_args | passthrough).test(True) is True - assert (requires_two_args & requires_two_args & passthrough).test(True) is True - - -def test_invocation_context(): - @predicate - def p1(): - assert id(p1.context) == id(p2.context) - assert p1.context.args == ('a',) - return True - - @predicate - def p2(): - assert id(p1.context) == id(p2.context) - assert p2.context.args == ('a',) - return True - - p = p1 & p2 - assert p.test('a') - assert p.context is None - - -def test_invocation_context_nested(): - @predicate - def p1(): - assert p1.context.args == ('b1',) - return True - - @predicate - def p2(): - assert p2.context.args == ('b2',) - return True - - @predicate - def p(): - assert p1.context.args == ('a',) - return p1.test('b1') & p2.test('b2') - - assert p.test('a') - assert p.context is None - - -def test_invocation_context_storage(): - @predicate - def p1(a): - p1.context['p1.a'] = a - return True - - @predicate - def p2(a): - return p2.context['p1.a'] == a - - p = p1 & p2 - assert p.test('a') + assert myotherpred.name == 'bar' + assert myotherpred.num_args == 0 + + def test_repr(self): + @predicate + def mypred(arg1, arg2): + return True + assert repr(mypred).startswith(' 0 + assert len(kwargs) == 0 + assert p.num_args == 0 + p.test('a') + p.test('a', 'b') + + def test_no_args(self): + @predicate + def p(*args, **kwargs): + assert len(args) == 0 + assert len(kwargs) == 0 + assert p.num_args == 0 + p.test() + + def test_one_arg(self): + @predicate + def p(a=None, *args, **kwargs): + assert len(args) == 0 + assert len(kwargs) == 0 + assert a == 'a' + assert p.num_args == 1 + p.test('a') + + def test_two_args(self): + @predicate + def p(a=None, b=None, *args, **kwargs): + assert len(args) == 0 + assert len(kwargs) == 0 + assert a == 'a' + assert b == 'b' + assert p.num_args == 2 + p.test('a', 'b') + + def test_no_mask(self): + @predicate + def p(a=None, b=None, *args, **kwargs): + assert len(args) == 0 + assert len(kwargs) == 1 + 'c' in kwargs + assert a == 'a' + assert b == 'b' + p('a', b='b', c='c') + + def test_no_value_marker(self): + @predicate + def p(a, b=None): + assert a == 'a' + assert b is None + + assert not NO_VALUE + p.test('a') + p.test('a', NO_VALUE) + + def test_short_circuit(self): + @predicate + def skipped_predicate(self): + return None + + @predicate + def shorted_predicate(self): + raise Exception('this predicate should not be evaluated') + + assert (always_false & shorted_predicate).test() is False + assert (always_true | shorted_predicate).test() is True + + def raises(pred): + try: + pred.test() + return False + except Exception as e: + return 'evaluated' in str(e) + + assert raises(always_true & shorted_predicate) + assert raises(always_false | shorted_predicate) + assert raises(skipped_predicate & shorted_predicate) + assert raises(skipped_predicate | shorted_predicate) + + def test_skip_predicate_deprecation(self): + @predicate(bind=True) + def skipped_predicate(self): + self.skip() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + assert skipped_predicate.test() is False + assert len(w) == 1 and 'deprecated' in str(w[-1].message) + + def test_skip_predicate(self): + @predicate(bind=True) + def requires_two_args(self, a, b): + return a == b if len(self.context.args) > 1 else None + + @predicate + def passthrough(a): + return a + + assert (requires_two_args & passthrough).test(True, True) is True + assert (requires_two_args & passthrough).test(True, False) is False + + # because requires_two_args is called with only one argument + # its result is not taken into account, only the result of the + # other predicate matters. + assert (requires_two_args & passthrough).test(True) is True + assert (requires_two_args & passthrough).test(False) is False + assert (requires_two_args | passthrough).test(True) is True + assert (requires_two_args | passthrough).test(False) is False + + # test that order does not matter + assert (passthrough & requires_two_args).test(True) is True + assert (passthrough & requires_two_args).test(False) is False + assert (passthrough | requires_two_args).test(True) is True + assert (passthrough | requires_two_args).test(False) is False + + # test that inversion does not modify the result + assert (~requires_two_args & passthrough).test(True) is True + assert (~requires_two_args & passthrough).test(False) is False + assert (~requires_two_args | passthrough).test(True) is True + assert (~requires_two_args | passthrough).test(False) is False + assert (passthrough & ~requires_two_args).test(True) is True + assert (passthrough & ~requires_two_args).test(False) is False + assert (passthrough | ~requires_two_args).test(True) is True + assert (passthrough | ~requires_two_args).test(False) is False + + # test that when all predicates are skipped, result is False + assert requires_two_args.test(True) is False + assert (requires_two_args | requires_two_args).test(True) is False + assert (requires_two_args & requires_two_args).test(True) is False + + # test that a skipped predicate doesn't alter the result at all + assert (requires_two_args | requires_two_args | passthrough).test(True) is True + assert (requires_two_args & requires_two_args & passthrough).test(True) is True + + def test_invocation_context(self): + @predicate + def p1(): + assert id(p1.context) == id(p2.context) + assert p1.context.args == ('a',) + return True + + @predicate + def p2(): + assert id(p1.context) == id(p2.context) + assert p2.context.args == ('a',) + return True + + p = p1 & p2 + assert p.test('a') + assert p.context is None + + def test_invocation_context_nested(self): + @predicate + def p1(): + assert p1.context.args == ('b1',) + return True + + @predicate + def p2(): + assert p2.context.args == ('b2',) + return True + + @predicate + def p(): + assert p1.context.args == ('a',) + return p1.test('b1') & p2.test('b2') + + assert p.test('a') + assert p.context is None + + def test_invocation_context_storage(self): + @predicate + def p1(a): + p1.context['p1.a'] = a + return True + + @predicate + def p2(a): + return p2.context['p1.a'] == a + + p = p1 & p2 + assert p.test('a') diff --git a/tests/testsuite/test_rulesets.py b/tests/testsuite/test_rulesets.py index a988322..51f8b91 100644 --- a/tests/testsuite/test_rulesets.py +++ b/tests/testsuite/test_rulesets.py @@ -1,42 +1,48 @@ -from nose.tools import nottest, with_setup, assert_raises +from unittest import TestCase from rules.predicates import always_true, always_false from rules.rulesets import (RuleSet, default_rules, add_rule, remove_rule, replace_rule, rule_exists, test_rule) -test_rule = nottest(test_rule) - - -def reset_ruleset(ruleset): - def fn(): +class RulesetTests(TestCase): + @staticmethod + def reset_ruleset(ruleset): for k in list(ruleset.keys()): ruleset.pop(k) - return fn - - -@with_setup(reset_ruleset(default_rules), reset_ruleset(default_rules)) -def test_shared_ruleset(): - add_rule('somerule', always_true) - assert 'somerule' in default_rules - assert rule_exists('somerule') - assert test_rule('somerule') - replace_rule('somerule', always_false) - assert not test_rule('somerule') - assert_raises(KeyError, replace_rule, 'someotherrule', always_false) - remove_rule('somerule') - assert not rule_exists('somerule') - -def test_ruleset(): - ruleset = RuleSet() - ruleset.add_rule('somerule', always_true) - assert 'somerule' in ruleset - assert ruleset.rule_exists('somerule') - assert ruleset.test_rule('somerule') - assert_raises(KeyError, ruleset.add_rule, 'somerule', always_true) - ruleset.replace_rule('somerule', always_false) - assert not test_rule('somerule') - assert_raises(KeyError, ruleset.replace_rule, 'someotherrule', always_false) - ruleset.remove_rule('somerule') - assert not ruleset.rule_exists('somerule') + def setUp(self): + self.reset_ruleset(default_rules) + + def tearDown(self): + self.reset_ruleset(default_rules) + + def test_shared_ruleset(self): + add_rule('somerule', always_true) + assert 'somerule' in default_rules + assert rule_exists('somerule') + assert test_rule('somerule') + assert test_rule('somerule') + with self.assertRaises(KeyError): + add_rule('somerule', always_false) + replace_rule('somerule', always_false) + assert not test_rule('somerule') + with self.assertRaises(KeyError): + replace_rule('someotherrule', always_false) + remove_rule('somerule') + assert not rule_exists('somerule') + + def test_ruleset(self): + ruleset = RuleSet() + ruleset.add_rule('somerule', always_true) + assert 'somerule' in ruleset + assert ruleset.rule_exists('somerule') + assert ruleset.test_rule('somerule') + with self.assertRaises(KeyError): + ruleset.add_rule('somerule', always_true) + ruleset.replace_rule('somerule', always_false) + assert not test_rule('somerule') + with self.assertRaises(KeyError): + ruleset.replace_rule('someotherrule', always_false) + ruleset.remove_rule('somerule') + assert not ruleset.rule_exists('somerule') diff --git a/tox.ini b/tox.ini index b95dbc9..2ccb053 100644 --- a/tox.ini +++ b/tox.ini @@ -1,25 +1,28 @@ [tox] envlist = - {py26}-django{15,16}, - {py27}-django{15,16,17,18,19,110,111}, - {py33}-django{15,16,17,18}, - {py34}-django{17,18,19,110,111,20}, - {py35}-django{18,19,110,111,20}, - {py36}-django{110,111,20} + py27-django{111}, + py34-django{111,20}, + py35-django{111,20}, + py36-django{111,20}, + pypy-django{111}, + pypy3-django{111,20}, + packaging, [testenv] +usedevelop = true deps = - nose coverage - django15: Django>=1.5,<1.6 - django16: Django>=1.6,<1.7 - django17: Django>=1.7,<1.8 - django18: Django>=1.8,<1.9 - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 - django111: Django>=1.11,<1.12 - django20: Django>=2.0,<2.1 + django111: Django~=1.11 + django20: Django~=2.0 commands = - coverage run --source=rules runtests.py --nologcapture --nocapture {posargs} + coverage run tests/manage.py test testsuite {posargs: -v 2} coverage report -m + +[testenv:packaging] +usedevelop = false +deps = + django + +commands = + python tests/manage.py test testsuite {posargs: -v 2}