diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..e051851 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,83 @@ +name: Test + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip tox + - name: Lint with flake8 + run: | + tox -e lint + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.8", "3.9", "3.10"] + django-version: [ "2.2", "3.2", "4.0"] + db-engine: ["pg", "mysql"] + env: + PY_VER: ${{ matrix.python-version}} + DJ_VER: ${{ matrix.django-version}} + DBENGINE: ${{ matrix.db-engine}} + MYSQL_USER: 'root' + MYSQL_PASSWORD: 'root' + + services: + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: concurrency + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + +# mysql: +# image: mysql:5.7 +# env: +# MYSQL_DATABASE: test_db +# MYSQL_USER: user +# MYSQL_PASSWORD: password +# MYSQL_ROOT_PASSWORD: rootpassword +# ports: +# - 33306:3306 +# options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + + steps: + - uses: actions/checkout@v2 + + - name: Setup MySQL + run: | + sudo /etc/init.d/mysql start + mysql -e 'CREATE DATABASE concurrency;' -uroot -proot + mysql -e 'SHOW DATABASES;' -uroot -proot + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: python -m pip install --upgrade pip tox + + - name: Test with + run: tox -e d${DJ_VER//.}-py${PY_VER//.}-${DBENGINE} + + - uses: codecov/codecov-action@v1 + with: + verbose: true diff --git a/.gitignore b/.gitignore index da2fd5b..6e5933e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ __pycache__ Pipfile.lock poetry.lock pyproject.toml +.venv/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e4dc816..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -language: python -sudo: true -python: - - 3.6 - - 3.7 - - 3.8 - -addons: - postgresql: "9.4" - -cache: - directories: - - $HOME/.cache/pip - -services: - - MySQL - - PostgreSQL - - -env: - - DJANGO=3.0 DB=pg - - DJANGO=3.0 DB=mysql - - -install: - - pip install tox "coverage<=4.0" codecov - -script: - - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- pytest tests -v - -before_success: - - coverage erase - -after_success: - - coverage combine - - codecov - - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/bf3806c14c6efcff7da1 - on_success: always # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: never # options: [always|never|change] default: always diff --git a/CHANGES b/CHANGES index 0b53f9d..0c855f8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,9 @@ +Release 2.4 +----------- +* add support Django 4 +* add support Python 3.10 + + Release 2.3 ----------- * Removes code producing DeprecationError diff --git a/Makefile b/Makefile index bc7a855..a7e574a 100644 --- a/Makefile +++ b/Makefile @@ -28,8 +28,6 @@ test: lint: pre-commit run --all-files - pipenv run isort -rc src/ --check-only - pipenv run check-manifest travis: docker run --privileged --name travis-debug -it -u travis travisci/ci-amethyst:packer-1512508255-986baf0 /bin/bash -l @@ -48,6 +46,7 @@ fullclean: docs: .mkbuilddir mkdir -p ${BUILDDIR}/docs + rm -fr ${BUILDDIR}/docs/* sphinx-build -aE docs/ ${BUILDDIR}/docs ifdef BROWSE firefox ${BUILDDIR}/docs/index.html diff --git a/Pipfile b/Pipfile index 63b18f9..21c3423 100644 --- a/Pipfile +++ b/Pipfile @@ -24,4 +24,4 @@ sphinx-issues = "*" twine="*" [requires] -python_version = "3.6" +python_version = "3.8" diff --git a/README.rst b/README.rst index 9a8550d..0158db4 100644 --- a/README.rst +++ b/README.rst @@ -10,12 +10,6 @@ Django Concurrency django-concurrency is an optimistic lock [1]_ implementation for Django. -Supported Django versions: - - - <=2.1.1 supports 1.11.x, 2.1.x, 2.2.x, 3.x - - >=2.2 supports 3.x - - It prevents users from doing concurrent editing in Django both from UI and from a django command. diff --git a/docs/_ext/version.py b/docs/_ext/version.py index d1af0b1..5493939 100644 --- a/docs/_ext/version.py +++ b/docs/_ext/version.py @@ -2,10 +2,10 @@ from docutils.parsers.rst import Directive, directives from sphinx import addnodes, roles +from sphinx.errors import ExtensionError from sphinx.util.console import bold # RE for option descriptions without a '--' prefix from sphinx.writers.html import HTMLTranslator -from sphinx.errors import ExtensionError simple_option_desc_re = re.compile( r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') diff --git a/docs/api.rst b/docs/api.rst index a27496d..4fe1e32 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -196,7 +196,7 @@ examples @disable_concurrency() def recover_view(self, request, version_id, extra_context=None): - return super(ReversionConcurrentModelAdmin, self).recover_view(request, + return super().recover_view(request, version_id, extra_context) diff --git a/docs/conf.py b/docs/conf.py index f28f3d7..7c6afc6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import sys diff --git a/docs/cookbook.rst b/docs/cookbook.rst index e527c89..10a2b14 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -104,10 +104,8 @@ To avoid this simply disable concurrency, by using a mixin: @disable_concurrency() def revision_view(self, request, object_id, version_id, extra_context=None): - return super(ConcurrencyVersionAdmin, self).revision_view( - request, object_id, version_id, extra_context=None) + return super().revision_view(request, object_id, version_id, extra_context=None) @disable_concurrency() def recover_view(self, request, version_id, extra_context=None): - return super(ConcurrencyVersionAdmin, self).recover_view( - request, version_id, extra_context) + return super().recover_view(request, version_id, extra_context) diff --git a/docs/middleware.rst b/docs/middleware.rst index fc97e99..69070e6 100644 --- a/docs/middleware.rst +++ b/docs/middleware.rst @@ -110,7 +110,7 @@ Each time a :class:`RecordModifiedError -If you want to use ConcurrentMiddleware in the admin and you are using +If you want to use ConcurrencyMiddleware in the admin and you are using :class:`concurrency.admin.ConcurrentModelAdmin` remember to set your ModelAdmin to NOT use :class:`concurrency.forms.ConcurrentForm` diff --git a/docs/requirements.pip b/docs/requirements.pip index e2a34d6..10f8f8b 100644 --- a/docs/requirements.pip +++ b/docs/requirements.pip @@ -1,2 +1,3 @@ sphinx==3.5.2 django==3.1 +sphinx_issues diff --git a/docs/settings.rst b/docs/settings.rst index 410ed65..6b9d3df 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -173,6 +173,3 @@ Default:: dict to customise :ref:`TriggerFactory`. Use this to customise the SQL clause to create triggers. - - - diff --git a/setup.cfg b/setup.cfg index 1725c71..91f4880 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,23 +1,21 @@ [isort] -line_length=90 combine_as_imports = true -multi_line_output = 5 -default_section = FIRSTPARTY -indent=' ' - -known_future_library= -known_standard_library=six -known_third_party=django -known_first_party=demo - -known_concurrency=concurrency -sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,CONCURRENCY,LOCALFOLDER +default_section = THIRDPARTY +include_trailing_comma = true +line_length = 80 +known_future_library = future,pies +known_standard_library = +known_third_party = django +known_first_party = sos +multi_line_output = 0 +balanced_wrapping = true +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER [flake8] max-complexity = 12 max-line-length = 160 exclude = .tox,migrations,.git,docs,diff_match_patch.py, deploy/**,settings -ignore = E501,E401,W391,E128,E261,E731 +ignore = E501,E401,W391,E128,E261,E731,W504 [aliases] test=pytest diff --git a/setup.py b/setup.py index 48ccf2f..be43ede 100755 --- a/setup.py +++ b/setup.py @@ -28,9 +28,10 @@ def finalize_options(self): def run_tests(self): # import here, cause outside the eggs aren't loaded - import pytest import sys + import pytest + sys.path.insert(0, os.path.join(ROOT, 'tests', 'demoapp')) errno = pytest.main(self.test_args) sys.exit(errno) @@ -59,10 +60,12 @@ def run_tests(self): 'Programming Language :: Python', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.0', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Python Modules', ], diff --git a/src/concurrency/__init__.py b/src/concurrency/__init__.py index 18e47e0..40f1684 100755 --- a/src/concurrency/__init__.py +++ b/src/concurrency/__init__.py @@ -1,5 +1,5 @@ __author__ = 'sax' default_app_config = 'concurrency.apps.ConcurrencyConfig' -VERSION = __version__ = "2.3" +VERSION = __version__ = "2.4" NAME = 'django-concurrency' diff --git a/src/concurrency/admin.py b/src/concurrency/admin.py index c92b509..2a3bbe9 100644 --- a/src/concurrency/admin.py +++ b/src/concurrency/admin.py @@ -2,22 +2,23 @@ import re from functools import reduce +import django from django.contrib import admin, messages from django.contrib.admin import helpers from django.core.checks import Error from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db.models import Q -from django.forms.formsets import ( - INITIAL_FORM_COUNT, MAX_NUM_FORM_COUNT, TOTAL_FORM_COUNT, ManagementForm -) +from django.forms.formsets import (INITIAL_FORM_COUNT, MAX_NUM_FORM_COUNT, + TOTAL_FORM_COUNT, ManagementForm,) from django.forms.models import BaseModelFormSet from django.http import HttpResponse, HttpResponseRedirect -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.safestring import mark_safe -from django.utils.translation import ungettext +from django.utils.translation import ngettext from concurrency import core, forms from concurrency.api import get_revision_of_object +from concurrency.compat import concurrency_param_name from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, conf from concurrency.exceptions import RecordModifiedError from concurrency.forms import ConcurrentForm, VersionWidget @@ -26,7 +27,7 @@ ALL = object() -class ConcurrencyActionMixin(object): +class ConcurrencyActionMixin: check_concurrent_action = True def action_checkbox(self, obj): @@ -35,10 +36,9 @@ def action_checkbox(self, obj): """ if self.check_concurrent_action: return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, - force_text("%s,%s" % (obj.pk, - get_revision_of_object(obj)))) + force_str("%s,%s" % (obj.pk, get_revision_of_object(obj)))) else: # pragma: no cover - return super(ConcurrencyActionMixin, self).action_checkbox(obj) + return super().action_checkbox(obj) action_checkbox.short_description = mark_safe('') action_checkbox.allow_tags = True @@ -133,15 +133,29 @@ def response_action(self, request, queryset): # noqa class ConcurrentManagementForm(ManagementForm): def __init__(self, *args, **kwargs): self._versions = kwargs.pop('versions', []) - super(ConcurrentManagementForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) - def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): - ret = super(ConcurrentManagementForm, self)._html_output(normal_row, error_row, row_ender, help_text_html, - errors_on_separate_row) + def _get_concurrency_fields(self): v = [] for pk, version in self._versions: - v.append(''.format(pk, version)) - return mark_safe("{0}{1}".format(ret, "".join(v))) + v.append(f'') + return mark_safe("".join(v)) + + def render(self, template_name=None, context=None, renderer=None): + out = super().render(template_name, context, renderer) + return out + self._get_concurrency_fields() + + def __str__(self): + if django.VERSION[:2] >= (4, 0): + return self.render() + else: + return super().__str__() + + __html__ = __str__ + + def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): + ret = super()._html_output(normal_row, error_row, row_ender, help_text_html, errors_on_separate_row) + return mark_safe("{0}{1}".format(ret, self._get_concurrency_fields())) class ConcurrentBaseModelFormSet(BaseModelFormSet): @@ -165,12 +179,12 @@ def _management_form(self): management_form = property(_management_form) -class ConcurrencyListEditableMixin(object): +class ConcurrencyListEditableMixin: list_editable_policy = conf.POLICY def get_changelist_formset(self, request, **kwargs): kwargs['formset'] = ConcurrentBaseModelFormSet - return super(ConcurrencyListEditableMixin, self).get_changelist_formset(request, **kwargs) + return super().get_changelist_formset(request, **kwargs) def _add_conflict(self, request, obj): if hasattr(request, '_concurrency_list_editable_errors'): @@ -187,10 +201,10 @@ def _get_conflicts(self, request): def save_model(self, request, obj, form, change): try: if change: - version = request.POST.get('_concurrency_version_{0.pk}'.format(obj), None) + version = request.POST.get(f'{concurrency_param_name}_{obj.pk}', None) if version: core._set_version(obj, version) - super(ConcurrencyListEditableMixin, self).save_model(request, obj, form, change) + super().save_model(request, obj, form, change) except RecordModifiedError: self._add_conflict(request, obj) # If policy is set to 'silent' the user will be informed using message_user @@ -204,12 +218,12 @@ def save_model(self, request, obj, form, change): def log_change(self, request, object, message): if object.pk in self._get_conflicts(request): return - return super(ConcurrencyListEditableMixin, self).log_change(request, object, message) + return super().log_change(request, object, message) def log_deletion(self, request, object, object_repr): if object.pk in self._get_conflicts(request): return - return super(ConcurrencyListEditableMixin, self).log_deletion(request, object, object_repr) + return super().log_deletion(request, object, object_repr) def message_user(self, request, message, *args, **kwargs): # This is ugly but we do not want to touch the changelist_view() code. @@ -217,7 +231,7 @@ def message_user(self, request, message, *args, **kwargs): opts = self.model._meta conflicts = self._get_conflicts(request) if conflicts: - names = force_text(opts.verbose_name), force_text(opts.verbose_name_plural) + names = force_str(opts.verbose_name), force_str(opts.verbose_name_plural) pattern = r"(?P\d+) ({0}|{1})".format(*names) rex = re.compile(pattern) m = rex.match(message) @@ -227,22 +241,22 @@ def message_user(self, request, message, *args, **kwargs): ids = ",".join(map(str, conflicts)) messages.error(request, - ungettext("Record with pk `{0}` has been modified and was not updated", - "Records `{0}` have been modified and were not updated", - concurrency_errros).format(ids)) + ngettext("Record with pk `{0}` has been modified and was not updated", + "Records `{0}` have been modified and were not updated", + concurrency_errros).format(ids)) if updated_record == 1: - name = force_text(opts.verbose_name) + name = force_str(opts.verbose_name) else: - name = force_text(opts.verbose_name_plural) + name = force_str(opts.verbose_name_plural) message = None if updated_record > 0: - message = ungettext("%(count)s %(name)s was changed successfully.", - "%(count)s %(name)s were changed successfully.", - updated_record) % {'count': updated_record, - 'name': name} + message = ngettext("%(count)s %(name)s was changed successfully.", + "%(count)s %(name)s were changed successfully.", + updated_record) % {'count': updated_record, + 'name': name} - return super(ConcurrencyListEditableMixin, self).message_user(request, message, *args, **kwargs) + return super().message_user(request, message, *args, **kwargs) class ConcurrentModelAdmin(ConcurrencyActionMixin, diff --git a/src/concurrency/api.py b/src/concurrency/api.py index ccc301b..ede4863 100644 --- a/src/concurrency/api.py +++ b/src/concurrency/api.py @@ -67,7 +67,7 @@ def apply_concurrency_check(model, fieldname, versionclass): # versionclass._wrap_model_save(model) -class concurrency_disable_increment(object): +class concurrency_disable_increment: def __init__(self, model): self.model = model self.old_value = model._concurrencymeta.increment @@ -93,7 +93,7 @@ def wrapper(*args, **kwds): return wrapper -class disable_concurrency(object): +class disable_concurrency: """ temporary disable concurrency diff --git a/src/concurrency/compat.py b/src/concurrency/compat.py index 757da54..33274aa 100644 --- a/src/concurrency/compat.py +++ b/src/concurrency/compat.py @@ -1,2 +1,8 @@ +import django from django.template.exceptions import TemplateDoesNotExist # noqa from django.urls.utils import get_callable # noqa + +if django.VERSION[:2] >= (4, 0): + concurrency_param_name = 'form-_concurrency_version' +else: + concurrency_param_name = '_concurrency_version' diff --git a/src/concurrency/config.py b/src/concurrency/config.py index a94fc64..4ee9743 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -17,7 +17,7 @@ LIST_EDITABLE_POLICIES = [CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL] -class AppSettings(object): +class AppSettings: defaults = { 'ENABLED': True, 'AUTO_CREATE_TRIGGERS': True, @@ -27,11 +27,12 @@ class AppSettings(object): 'CALLBACK': 'concurrency.views.callback', 'HANDLER409': 'concurrency.views.conflict', 'VERSION_FIELD_REQUIRED': True, - 'TRIGGERS_FACTORY': {'postgresql': "concurrency.triggers.PostgreSQL", - 'mysql': "concurrency.triggers.MySQL", - 'sqlite3': "concurrency.triggers.Sqlite3", - 'sqlite': "concurrency.triggers.Sqlite3", - } + 'TRIGGERS_FACTORY': { + 'postgresql': "concurrency.triggers.PostgreSQL", + 'mysql': "concurrency.triggers.MySQL", + 'sqlite3': "concurrency.triggers.Sqlite3", + 'sqlite': "concurrency.triggers.Sqlite3", + } } def __init__(self, prefix): diff --git a/src/concurrency/core.py b/src/concurrency/core.py index 94051f5..b0919f7 100644 --- a/src/concurrency/core.py +++ b/src/concurrency/core.py @@ -1,15 +1,8 @@ import logging +from logging import NullHandler from concurrency.config import conf -# Set default logging handler to avoid "No handler found" warnings. -try: # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - logging.getLogger('concurrency').addHandler(NullHandler()) logger = logging.getLogger(__name__) diff --git a/src/concurrency/exceptions.py b/src/concurrency/exceptions.py index 4b1c8be..dbb683f 100644 --- a/src/concurrency/exceptions.py +++ b/src/concurrency/exceptions.py @@ -10,7 +10,7 @@ class VersionChangedError(ValidationError): class RecordModifiedError(DatabaseError): def __init__(self, *args, **kwargs): self.target = kwargs.pop('target') - super(RecordModifiedError, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class VersionError(SuspiciousOperation): diff --git a/src/concurrency/fields.py b/src/concurrency/fields.py index 8dfe5cd..73d9b11 100755 --- a/src/concurrency/fields.py +++ b/src/concurrency/fields.py @@ -10,7 +10,7 @@ from django.db.models import signals from django.db.models.fields import Field from django.db.models.signals import class_prepared, post_migrate -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ from concurrency import forms @@ -19,6 +19,8 @@ from concurrency.core import ConcurrencyOptions from concurrency.utils import fqn, refetch +from .triggers import _TRIGGERS + logger = logging.getLogger(__name__) OFFSET = int(time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, 0))) @@ -50,9 +52,10 @@ def class_prepared_concurrency_handler(sender, **kwargs): def post_syncdb_concurrency_handler(sender, **kwargs): - from concurrency.triggers import create_triggers from django.db import connections + from concurrency.triggers import create_triggers + databases = [alias for alias in connections] create_triggers(databases) @@ -76,11 +79,11 @@ def __init__(self, *args, **kwargs): db_column = kwargs.get('db_column', None) help_text = kwargs.get('help_text', _('record revision number')) - super(VersionField, self).__init__(verbose_name, name, - help_text=help_text, - default=0, - db_tablespace=db_tablespace, - db_column=db_column) + super().__init__(verbose_name, name, + help_text=help_text, + default=0, + db_tablespace=db_tablespace, + db_column=db_column) def get_internal_type(self): return "BigIntegerField" @@ -94,10 +97,10 @@ def validate(self, value, model_instance): def formfield(self, **kwargs): kwargs['form_class'] = self.form_class kwargs['widget'] = forms.VersionField.widget - return super(VersionField, self).formfield(**kwargs) + return super().formfield(**kwargs) def contribute_to_class(self, cls, *args, **kwargs): - super(VersionField, self).contribute_to_class(cls, *args, **kwargs) + super().contribute_to_class(cls, *args, **kwargs) if hasattr(cls, '_concurrencymeta') or cls._meta.abstract: return setattr(cls, '_concurrencymeta', ConcurrencyOptions()) @@ -131,7 +134,6 @@ def _wrap_do_update(self, func): def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, forced_update): version_field = model_instance._concurrencymeta.field old_version = get_revision_of_object(model_instance) - if not version_field.model._meta.abstract: if version_field.model is not base_qs.model: return func(model_instance, base_qs, using, pk_val, values, update_fields, forced_update) @@ -202,7 +204,6 @@ class AutoIncVersionField(VersionField): def _get_next_version(self, model_instance): return int(getattr(model_instance, self.attname, 0)) + 1 -from .triggers import _TRIGGERS class TriggerVersionField(VersionField): """ @@ -214,10 +215,10 @@ class TriggerVersionField(VersionField): def __init__(self, *args, **kwargs): self._trigger_name = kwargs.pop('trigger_name', None) self._trigger_exists = False - super(TriggerVersionField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def contribute_to_class(self, cls, *args, **kwargs): - super(TriggerVersionField, self).contribute_to_class(cls, *args, **kwargs) + super().contribute_to_class(cls, *args, **kwargs) if not cls._meta.abstract or cls._meta.proxy: if self not in _TRIGGERS: _TRIGGERS.append(self) @@ -225,9 +226,10 @@ def contribute_to_class(self, cls, *args, **kwargs): def check(self, **kwargs): errors = [] model = self.model - from django.db import router, connections - from concurrency.triggers import factory from django.core.checks import Warning + from django.db import connections, router + + from concurrency.triggers import factory alias = router.db_for_write(model) connection = connections[alias] @@ -299,7 +301,7 @@ def filter_fields(instance, field): class ConditionalVersionField(AutoIncVersionField): def contribute_to_class(self, cls, *args, **kwargs): - super(ConditionalVersionField, self).contribute_to_class(cls, *args, **kwargs) + super().contribute_to_class(cls, *args, **kwargs) signals.post_init.connect(self._load_model, sender=cls, dispatch_uid=fqn(cls)) @@ -338,7 +340,7 @@ def _get_hash(self, instance): values[field_name] = getattr(instance, field_name).values_list('pk', flat=True) else: values[field_name] = field.value_from_object(instance) - return hashlib.sha1(force_text(values).encode('utf-8')).hexdigest() + return hashlib.sha1(force_str(values).encode('utf-8')).hexdigest() def _get_next_version(self, model_instance): if not model_instance.pk: diff --git a/src/concurrency/forms.py b/src/concurrency/forms.py index 0245637..1ca2fbe 100644 --- a/src/concurrency/forms.py +++ b/src/concurrency/forms.py @@ -1,7 +1,8 @@ from importlib import import_module from django import forms -from django.core.exceptions import NON_FIELD_ERRORS, ImproperlyConfigured, ValidationError +from django.core.exceptions import (NON_FIELD_ERRORS, ImproperlyConfigured, + ValidationError,) from django.core.signing import BadSignature, Signer from django.forms import HiddenInput, ModelForm from django.utils.safestring import mark_safe @@ -25,9 +26,9 @@ def clean(self): _select_lock(self.instance, self.cleaned_data[self.instance._concurrencymeta.field.name]) except RecordModifiedError: - self._update_errors(ValidationError({NON_FIELD_ERRORS: self.error_class([_('Record Modified')])})) + self._update_errors(ValidationError({NON_FIELD_ERRORS: self.error_class([_('Record Modified')])})) - return super(ConcurrentForm, self).clean() + return super().clean() class VersionWidget(HiddenInput): @@ -47,7 +48,7 @@ def format_value(self, value): _format_value = format_value def render(self, name, value, attrs=None): - ret = super(VersionWidget, self).render(name, value, attrs) + ret = super().render(name, value, attrs) label = '' if isinstance(value, SignedValue): label = str(value).split(':')[0] @@ -61,7 +62,7 @@ class VersionFieldSigner(Signer): def sign(self, value): if not value: return None - return super(VersionFieldSigner, self).sign(value) + return super().sign(value) def get_signer(): @@ -79,7 +80,7 @@ def get_signer(): return signer_class() -class SignedValue(object): +class SignedValue: def __init__(self, value): self.value = value @@ -101,7 +102,7 @@ def __init__(self, *args, **kwargs): kwargs['required'] = True kwargs['initial'] = None kwargs.setdefault('widget', HiddenInput) - super(VersionField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def bound_data(self, data, initial): return SignedValue(data) diff --git a/src/concurrency/middleware.py b/src/concurrency/middleware.py index 2e9b5e7..67e4011 100644 --- a/src/concurrency/middleware.py +++ b/src/concurrency/middleware.py @@ -1,15 +1,11 @@ from django.core.signals import got_request_exception +from django.urls.utils import get_callable from concurrency.config import conf from concurrency.exceptions import RecordModifiedError -try: - from django.urls.utils import get_callable -except ImportError: - from django.core.urlresolvers import get_callable - -class ConcurrencyMiddleware(object): +class ConcurrencyMiddleware: """ Intercept :ref:`RecordModifiedError` and invoke a callable defined in :setting:`CONCURRECY_HANDLER409` passing the request and the object. diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index 5831e25..0573a55 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -23,6 +23,7 @@ def __contains__(self, field): _TRIGGERS = TriggerRegistry() + def get_trigger_name(field): """ @@ -206,4 +207,3 @@ def factory(conn): return mapping[conn.vendor](conn) except KeyError: # pragma: no cover raise ValueError('{} is not supported by TriggerVersionField'.format(conn)) - diff --git a/src/concurrency/utils.py b/src/concurrency/utils.py index 0d17033..b2b4ecd 100644 --- a/src/concurrency/utils.py +++ b/src/concurrency/utils.py @@ -48,7 +48,7 @@ def inner(*args, **kwargs): return outer -class ConcurrencyTestMixin(object): +class ConcurrencyTestMixin: """ Mixin class to test Models that use `VersionField` @@ -104,7 +104,7 @@ def test_concurrency_management(self): "%s: version field not in meta.fields" % self.concurrency_model) -class ConcurrencyAdminTestMixin(object): +class ConcurrencyAdminTestMixin: pass @@ -144,7 +144,7 @@ def fqn(o): Traceback (most recent call last): ... ValueError: Invalid argument `str` - >>> class A(object): + >>> class A: ... def method(self): ... pass >>> str(fqn(A)) diff --git a/tests/conftest.py b/tests/conftest.py index 9a2c385..c099101 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,6 @@ import platform import sys -import django - import pytest py_impl = getattr(platform, 'python_implementation', lambda: None) @@ -21,7 +19,6 @@ def pytest_configure(): - from django.contrib.auth.models import Group from django.conf import settings settings.SILENCED_SYSTEM_CHECKS = ['concurrency.W001'] diff --git a/tests/demoapp/demo/__init__.py b/tests/demoapp/demo/__init__.py index d169e2c..e69de29 100644 --- a/tests/demoapp/demo/__init__.py +++ b/tests/demoapp/demo/__init__.py @@ -1 +0,0 @@ -default_app_config = 'demo.apps.ConcurrencyTestConfig' diff --git a/tests/demoapp/demo/admin.py b/tests/demoapp/demo/admin.py index ca0cd7e..81081a1 100644 --- a/tests/demoapp/demo/admin.py +++ b/tests/demoapp/demo/admin.py @@ -1,20 +1,16 @@ -from __future__ import unicode_literals - +from demo.models import (InheritedModel, ListEditableConcurrentModel, + NoActionsConcurrentModel, ProxyModel, + ReversionConcurrentModel, SimpleConcurrentModel,) from django.contrib import admin from django.contrib.admin.sites import NotRegistered -from demo.models import ( - InheritedModel, ListEditableConcurrentModel, NoActionsConcurrentModel, ProxyModel, - ReversionConcurrentModel, SimpleConcurrentModel -) - from concurrency.admin import ConcurrentModelAdmin from concurrency.api import disable_concurrency try: from reversion.admin import VersionAdmin except ImportError: - class VersionAdmin(object): + class VersionAdmin: pass @@ -39,9 +35,7 @@ class ReversionConcurrentModelAdmin(VersionAdmin, ConcurrentModelAdmin): @disable_concurrency() def recover_view(self, request, version_id, extra_context=None): - return super(ReversionConcurrentModelAdmin, self).recover_view(request, - version_id, - extra_context) + return super().recover_view(request, version_id, extra_context) class ActionsModelAdmin(ConcurrentModelAdmin): diff --git a/tests/demoapp/demo/auth_migrations/0001_initial.py b/tests/demoapp/demo/auth_migrations/0001_initial.py index ae4b48e..d80313c 100644 --- a/tests/demoapp/demo/auth_migrations/0001_initial.py +++ b/tests/demoapp/demo/auth_migrations/0001_initial.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.6 on 2016-09-09 15:22 -from __future__ import unicode_literals - import django.contrib.auth.models import django.core.validators import django.db.models.deletion diff --git a/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py b/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py index 48f49ff..6077010 100644 --- a/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py +++ b/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations from concurrency.fields import IntegerVersionField diff --git a/tests/demoapp/demo/base.py b/tests/demoapp/demo/base.py index d259bb9..37a351e 100644 --- a/tests/demoapp/demo/base.py +++ b/tests/demoapp/demo/base.py @@ -1,11 +1,7 @@ -from __future__ import unicode_literals - -import django +from demo.admin import admin_register_models from django.contrib.auth.models import Group, User from django.test import TransactionTestCase from django.utils import timezone - -from demo.admin import admin_register_models from django_webtest import WebTestMixin from concurrency.api import apply_concurrency_check @@ -16,14 +12,12 @@ apply_concurrency_check(Group, 'version', IntegerVersionField) -DJANGO_TRUNK = django.VERSION[:2] >= (1, 8) - class AdminTestCase(WebTestMixin, TransactionTestCase): urls = 'demo.urls' def setUp(self): - super(AdminTestCase, self).setUp() + super().setUp() self.user, __ = User.objects.get_or_create(is_superuser=True, is_staff=True, @@ -31,44 +25,4 @@ def setUp(self): last_login=timezone.now(), email='sax@example.com', username='sax') - # self.user.set_password('123') - # self.user.save() admin_register_models() - - -# class DjangoAdminTestCase(TransactionTestCase): -# urls = 'concurrency.tests.urls' -# MIDDLEWARE_CLASSES = global_settings.MIDDLEWARE_CLASSES -# AUTHENTICATION_BACKENDS = global_settings.AUTHENTICATION_BACKENDS -# -# def setUp(self): -# super(DjangoAdminTestCase, self).setUp() -# self.sett = self.settings( -# #INSTALLED_APPS=INSTALLED_APPS, -# MIDDLEWARE_CLASSES=self.MIDDLEWARE_CLASSES, -# AUTHENTICATION_BACKENDS=self.AUTHENTICATION_BACKENDS, -# PASSWORD_HASHERS=('django.contrib.auth.hashers.MD5PasswordHasher',), # fastest hasher -# STATIC_URL='/static/', -# SOUTH_TESTS_MIGRATE=False, -# TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),)) -# self.sett.enable() -# django.core.management._commands = None # reset commands cache -# django.core.management.call_command('syncdb', verbosity=0) -# -# # admin_register(TestModel0) -# # admin_register(TestModel1, TestModel1Admin) -# -# self.user, __ = User.objects.get_or_create(username='sax', -# is_active=True, -# is_staff=True, -# is_superuser=True) -# self.user.set_password('123') -# self.user.save() -# self.client.login(username=self.user.username, password='123') -# # self.target, __ = TestModel0.objects.get_or_create(username='aaa') -# # self.target1, __ = TestModel1.objects.get_or_create(username='bbb') -# -# def tearDown(self): -# super(DjangoAdminTestCase, self).tearDown() -# self.sett.disable() -# # admin_unregister(TestModel0, TestModel1) diff --git a/tests/demoapp/demo/migrations/0001_initial.py b/tests/demoapp/demo/migrations/0001_initial.py index a36747e..0520297 100644 --- a/tests/demoapp/demo/migrations/0001_initial.py +++ b/tests/demoapp/demo/migrations/0001_initial.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.6 on 2016-09-09 15:41 -from __future__ import unicode_literals - import django.db.models.deletion from django.conf import settings from django.db import migrations, models diff --git a/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py b/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py index 2240448..902b167 100644 --- a/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py +++ b/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.6 on 2016-09-09 15:44 -from __future__ import unicode_literals - import django.db.models.deletion from django.db import migrations, models diff --git a/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py b/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py index f1e3db2..a819d98 100644 --- a/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py +++ b/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/tests/demoapp/demo/migrations/__init__.py b/tests/demoapp/demo/migrations/__init__.py index 6aa2bd1..e69de29 100644 --- a/tests/demoapp/demo/migrations/__init__.py +++ b/tests/demoapp/demo/migrations/__init__.py @@ -1,21 +0,0 @@ -""" -Django migrations - -This package does not contain South migrations. South migrations can be found -in the ``south_migrations`` package. -""" - -SOUTH_ERROR_MESSAGE = """\n -For South support, customize the SOUTH_MIGRATION_MODULES setting like so: - - SOUTH_MIGRATION_MODULES = { - 'tests': 'tests.south_migrations', - } -""" - -# Ensure the user is not using Django 1.6 or below with South -try: - from django.db import migrations # noqa -except ImportError: - from django.core.exceptions import ImproperlyConfigured - raise ImproperlyConfigured(SOUTH_ERROR_MESSAGE) diff --git a/tests/demoapp/demo/models.py b/tests/demoapp/demo/models.py index 8e1b6e1..434612a 100644 --- a/tests/demoapp/demo/models.py +++ b/tests/demoapp/demo/models.py @@ -1,13 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib.auth.models import Group, User from django.db import models -from concurrency.fields import ( - AutoIncVersionField, ConditionalVersionField, IntegerVersionField, - TriggerVersionField -) +from concurrency.fields import (AutoIncVersionField, ConditionalVersionField, + IntegerVersionField, TriggerVersionField,) __all__ = ['SimpleConcurrentModel', 'AutoIncConcurrentModel', 'ProxyModel', 'InheritedModel', 'CustomSaveModel', @@ -91,7 +86,7 @@ class CustomSaveModel(SimpleConcurrentModel): extra_field = models.CharField(max_length=30, blank=True, null=True, unique=True) def save(self, *args, **kwargs): - super(CustomSaveModel, self).save(*args, **kwargs) + super().save(*args, **kwargs) class Meta: app_label = 'demo' @@ -139,7 +134,7 @@ class Meta: # app_label = 'demo' # # def save(self, *args, **kwargs): -# super(TestModelGroupWithCustomSave, self).save(*args, **kwargs) +# super().save(*args, **kwargs) # return 222 diff --git a/tests/demoapp/demo/settings.py b/tests/demoapp/demo/settings.py index 3c1fae5..e682056 100644 --- a/tests/demoapp/demo/settings.py +++ b/tests/demoapp/demo/settings.py @@ -1,8 +1,6 @@ import os from tempfile import mktemp -import django - try: from psycopg2cffi import compat @@ -118,10 +116,10 @@ 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': dbname, - 'HOST': '127.0.0.1', - 'PORT': '', + 'HOST': os.environ.get('PGHOST', '127.0.0.1'), + 'PORT': os.environ.get('PGPORT', '5432'), 'USER': 'postgres', - 'PASSWORD': ''}} + 'PASSWORD': 'postgres'}} elif db == 'mysql': DATABASES = { 'default': { @@ -130,7 +128,7 @@ 'HOST': '127.0.0.1', 'PORT': '', 'USER': 'root', - 'PASSWORD': '', + 'PASSWORD': 'root', 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci'}} else: diff --git a/tests/demoapp/demo/urls.py b/tests/demoapp/demo/urls.py index e8d0943..58f8f40 100644 --- a/tests/demoapp/demo/urls.py +++ b/tests/demoapp/demo/urls.py @@ -1,21 +1,11 @@ -from django.conf.urls import url +from demo.models import SimpleConcurrentModel from django.contrib import admin +from django.urls import re_path from django.views.generic.edit import UpdateView -from demo.models import SimpleConcurrentModel - -# try: -# from django.apps import AppConfig # noqa -# import django -# django.setup() -# except ImportError: -# pass - admin.autodiscover() -urlpatterns = (url('cm/(?P\d+)/', - UpdateView.as_view(model=SimpleConcurrentModel), - name='concurrent-edit'), - url(r'^admin/', - admin.site.urls) - ) +urlpatterns = (re_path(r'cm/(?P\d+)/', + UpdateView.as_view(model=SimpleConcurrentModel), + name='concurrent-edit'), + re_path(r'^admin/', admin.site.urls)) diff --git a/tests/demoapp/demo/util.py b/tests/demoapp/demo/util.py index 2d704f9..3076de9 100644 --- a/tests/demoapp/demo/util.py +++ b/tests/demoapp/demo/util.py @@ -3,13 +3,11 @@ from functools import partial, update_wrapper from itertools import count -from django import db - import pytest -from demo.models import ( - AutoIncConcurrentModel, ConcreteModel, CustomSaveModel, InheritedModel, ProxyModel, - SimpleConcurrentModel, TriggerConcurrentModel -) +from demo.models import (AutoIncConcurrentModel, ConcreteModel, + CustomSaveModel, InheritedModel, ProxyModel, + SimpleConcurrentModel, TriggerConcurrentModel,) +from django import db from concurrency.config import conf diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py index 8459017..27d163b 100644 --- a/tests/test_admin_actions.py +++ b/tests/test_admin_actions.py @@ -1,13 +1,8 @@ -# -*- coding: utf-8 -*- import pytest from demo.base import SENTINEL, AdminTestCase from demo.models import SimpleConcurrentModel from demo.util import unique_id - -try: - from django.core.urlresolvers import reverse -except ImportError: - from django.urls import reverse +from django.urls import reverse class TestAdminActions(AdminTestCase): @@ -56,7 +51,6 @@ def test_dummy_action_select_across(self): self.assertIn('Selecting all records, you will avoid the concurrency check', res) - # @pytest.mark.skipif(django.VERSION[:2] >= (1, 7), reason="Skip django>=1.9") def test_delete_allowed_if_no_updates(self): id = next(unique_id) SimpleConcurrentModel.objects.get_or_create(pk=id) @@ -76,7 +70,6 @@ def test_delete_allowed_if_no_updates(self): res = res.form.submit() assert 'SimpleConcurrentModel #%s' % id not in res - # @pytest.mark.skipif(django.VERSION[:2] >= (1, 10), reason="Skip django>=1.10") def test_delete_not_allowed_if_updates(self): id = next(unique_id) diff --git a/tests/test_admin_edit.py b/tests/test_admin_edit.py index b900a1f..854fd5e 100644 --- a/tests/test_admin_edit.py +++ b/tests/test_admin_edit.py @@ -1,17 +1,12 @@ -from django.utils.translation import gettext as _ - import pytest from demo.base import SENTINEL, AdminTestCase from demo.models import SimpleConcurrentModel from demo.util import nextname +from django.urls import reverse +from django.utils.translation import gettext as _ from concurrency.forms import VersionFieldSigner -try: - from django.core.urlresolvers import reverse -except ImportError: - from django.urls import reverse - @pytest.mark.django_db @pytest.mark.admin diff --git a/tests/test_admin_list_editable.py b/tests/test_admin_list_editable.py index 6ea7849..9cbec33 100644 --- a/tests/test_admin_list_editable.py +++ b/tests/test_admin_list_editable.py @@ -1,24 +1,19 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -from django.contrib.admin.models import LogEntry -from django.contrib.admin.sites import site -from django.contrib.contenttypes.models import ContentType -from django.db import transaction -from django.utils.encoding import force_text - import pytest from demo.base import SENTINEL, AdminTestCase from demo.models import ListEditableConcurrentModel from demo.util import attributes, unique_id +from django.contrib.admin.models import LogEntry +from django.contrib.admin.sites import site +from django.contrib.contenttypes.models import ContentType +from django.db import transaction +from django.utils.encoding import force_str -from concurrency.config import ( - CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, CONCURRENCY_LIST_EDITABLE_POLICY_SILENT -) +from concurrency.compat import concurrency_param_name +from concurrency.config import (CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, + CONCURRENCY_LIST_EDITABLE_POLICY_SILENT,) from concurrency.exceptions import RecordModifiedError -# @pytest.mark.xfail(django.VERSION[:2] == (1, 10), reason="Django 1.10") class TestListEditable(AdminTestCase): TARGET = ListEditableConcurrentModel @@ -34,7 +29,7 @@ def test_normal_add(self): res = res.click('Add', href=f'/admin/demo/{self.TARGET._meta.model_name}/add/', index=0) form = res.form form['username'] = 'CHAR' - res = form.submit().follow() + form.submit().follow() def test_normal_update(self): self.TARGET.objects.get_or_create(pk=next(unique_id)) @@ -42,7 +37,7 @@ def test_normal_update(self): res = res.click(self.TARGET._meta.verbose_name_plural) form = res.forms['changelist-form'] form['form-0-username'] = 'CHAR' - res = form.submit('_save').follow() + form.submit('_save').follow() self.assertTrue(self.TARGET.objects.filter(username='CHAR').exists()) def test_concurrency_policy_abort(self): @@ -53,7 +48,6 @@ def test_concurrency_policy_abort(self): res = self.app.get('/admin/', user='sax') res = res.click(self.TARGET._meta.verbose_name_plural) self._create_conflict(id) - form = res.forms['changelist-form'] form['form-0-username'] = 'CHAR' @@ -71,12 +65,13 @@ def test_concurrency_policy_silent(self): res = self.app.get('/admin/', user='sax') res = res.click(self.TARGET._meta.verbose_name_plural) self._create_conflict(id) - form = res.forms['changelist-form'] form['form-0-username'] = 'CHAR' + version = int(form[f'{concurrency_param_name}_{id}'].value) res = form.submit('_save').follow() - self.assertTrue(self.TARGET.objects.filter(username=SENTINEL).exists()) - self.assertFalse(self.TARGET.objects.filter(username='CHAR').exists()) + changed = self.TARGET.objects.filter(username=SENTINEL).first() + self.assertTrue(changed) + self.assertGreater(changed.version, version) def test_message_user(self): id1 = next(unique_id) @@ -97,7 +92,7 @@ def test_message_user(self): self.assertIn('Record with pk `%s` has been modified and was not updated' % id1, messages) - self.assertIn('1 %s was changed successfully.' % force_text(self.TARGET._meta.verbose_name), + self.assertIn('1 %s was changed successfully.' % force_str(self.TARGET._meta.verbose_name), messages) def test_message_user_no_changes(self): @@ -115,8 +110,8 @@ def test_message_user_no_changes(self): messages = list(map(str, list(res.context['messages']))) - self.assertIn('Record with pk `%s` has been modified and was not updated' % id, messages) - self.assertEqual(len(messages), 1) + self.assertIn('Record with pk `%s` has been modified and was not updated' % id, set(messages)) + self.assertEqual(len(set(messages)), 1) def test_log_change(self): id = next(unique_id) @@ -137,6 +132,3 @@ def test_log_change(self): new_logs = LogEntry.objects.filter(**log_filter).exclude(id__in=logs).exists() self.assertFalse(new_logs, "LogEntry created even if conflict error") transaction.rollback() - -# class TestListEditableWithNoActions(TestListEditable): -# TARGET = NoActionsConcurrentModel diff --git a/tests/test_api.py b/tests/test_api.py index 6a2729f..40f7209 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,12 +1,10 @@ -from django.contrib.auth.models import Group - import pytest from demo.models import SimpleConcurrentModel from demo.util import nextgroup, nextname +from django.contrib.auth.models import Group -from concurrency.api import ( - apply_concurrency_check, get_revision_of_object, get_version, is_changed -) +from concurrency.api import (apply_concurrency_check, get_revision_of_object, + get_version, is_changed,) from concurrency.exceptions import RecordModifiedError from concurrency.fields import IntegerVersionField from concurrency.utils import refetch diff --git a/tests/test_base.py b/tests/test_base.py index 8b39ff7..9f70a92 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ -from django.test import override_settings - import pytest -from demo.util import concurrent_model, unique_id, with_all_models, with_std_models +from demo.util import (concurrent_model, unique_id, + with_all_models, with_std_models,) +from django.test import override_settings from concurrency.core import _set_version from concurrency.exceptions import RecordModifiedError diff --git a/tests/test_checks.py b/tests/test_checks.py index 9cc4541..b859e4e 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -1,10 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals - import logging -import django - import pytest from demo.models import TriggerConcurrentModel @@ -16,8 +11,6 @@ def obj(): return TriggerConcurrentModel.objects.create() -@pytest.mark.skipif(django.VERSION[:2] < (1, 7), - reason="Skip if django< 1.7") @pytest.mark.django_db def test_check(obj, monkeypatch): from django.core.checks import Warning diff --git a/tests/test_command.py b/tests/test_command.py index e4afa48..effd638 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -import logging import io - -from django.core.management import call_command +import logging import pytest +from django.core.management import call_command from mock import Mock import concurrency.management.commands.triggers as command diff --git a/tests/test_concurrencymetainfo.py b/tests/test_concurrencymetainfo.py index 0836776..004f75a 100644 --- a/tests/test_concurrencymetainfo.py +++ b/tests/test_concurrencymetainfo.py @@ -1,6 +1,5 @@ -from django.test import TransactionTestCase - from demo.models import ConcurrencyDisabledModel, SimpleConcurrentModel +from django.test import TransactionTestCase from concurrency.exceptions import RecordModifiedError @@ -10,7 +9,7 @@ class TestCustomConcurrencyMeta(TransactionTestCase): concurrency_kwargs = {'username': 'test'} def setUp(self): - super(TestCustomConcurrencyMeta, self).setUp() + super().setUp() self.TARGET = self._get_concurrency_target() def _get_concurrency_target(self, **kwargs): diff --git a/tests/test_conditional.py b/tests/test_conditional.py index aa832c8..ffc2a63 100644 --- a/tests/test_conditional.py +++ b/tests/test_conditional.py @@ -1,15 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals - import logging -from django.contrib.auth.models import User - import pytest -from demo.models import ( - ConditionalVersionModel, ConditionalVersionModelSelfRelation, - ConditionalVersionModelWithoutMeta, ThroughRelation -) +from demo.models import (ConditionalVersionModel, + ConditionalVersionModelSelfRelation, + ConditionalVersionModelWithoutMeta, ThroughRelation,) +from django.contrib.auth.models import User from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch diff --git a/tests/test_config.py b/tests/test_config.py index 1b0b1c0..2788e5e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,5 @@ -from django.core.exceptions import ImproperlyConfigured - import pytest +from django.core.exceptions import ImproperlyConfigured from concurrency.config import AppSettings from concurrency.utils import fqn diff --git a/tests/test_core.py b/tests/test_core.py index 730c535..864dff1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - import pytest from demo.models import SimpleConcurrentModel diff --git a/tests/test_enable_disable.py b/tests/test_enable_disable.py index 9856737..9b2775e 100644 --- a/tests/test_enable_disable.py +++ b/tests/test_enable_disable.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from django.contrib.auth.models import User -from django.test.utils import override_settings - import pytest from demo.models import AutoIncConcurrentModel, SimpleConcurrentModel from demo.util import nextname +from django.contrib.auth.models import User +from django.test.utils import override_settings from concurrency.api import concurrency_disable_increment, disable_concurrency from concurrency.exceptions import RecordModifiedError diff --git a/tests/test_forms.py b/tests/test_forms.py index a6fcaa5..e8721d5 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,3 +1,5 @@ +import pytest +from demo.models import Issue3TestModel, SimpleConcurrentModel from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.forms.models import modelform_factory from django.forms.widgets import HiddenInput, TextInput @@ -6,13 +8,9 @@ from django.utils.encoding import smart_str from django.utils.translation import gettext as _ -import pytest -from demo.models import Issue3TestModel, SimpleConcurrentModel - from concurrency.exceptions import VersionError -from concurrency.forms import ( - ConcurrentForm, VersionField, VersionFieldSigner, VersionWidget -) +from concurrency.forms import (ConcurrentForm, VersionField, + VersionFieldSigner, VersionWidget,) __all__ = ['WidgetTest', 'FormFieldTest', 'ConcurrentFormTest'] diff --git a/tests/test_issues.py b/tests/test_issues.py index 77905c1..05eb193 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -1,7 +1,10 @@ -# -*- coding: utf-8 -*- import re -import django +import pytest +from demo.admin import ActionsModelAdmin, admin_register +from demo.base import AdminTestCase +from demo.models import ListEditableConcurrentModel, SimpleConcurrentModel +from demo.util import attributes, unique_id from django.contrib.admin.sites import site from django.contrib.auth.models import User from django.core.management import call_command @@ -10,18 +13,10 @@ from django.test import override_settings from django.test.client import RequestFactory from django.test.testcases import SimpleTestCase -from django.utils.encoding import force_text - -import pytest -from conftest import skipIfDjangoVersion -from demo.admin import ActionsModelAdmin, admin_register -from demo.base import AdminTestCase -from demo.models import ( - ListEditableConcurrentModel, ReversionConcurrentModel, SimpleConcurrentModel -) -from demo.util import attributes, unique_id +from django.utils.encoding import force_str from concurrency.admin import ConcurrentModelAdmin +from concurrency.compat import concurrency_param_name from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_SILENT from concurrency.exceptions import RecordModifiedError from concurrency.forms import ConcurrentForm @@ -51,14 +46,18 @@ def test_concurrency(self): with attributes((ConcurrentModelAdmin, 'list_editable_policy', CONCURRENCY_LIST_EDITABLE_POLICY_SILENT), (ConcurrentModelAdmin, 'form', ConcurrentForm), ): obj, __ = ListEditableConcurrentModel.objects.get_or_create(pk=id) - request1 = get_fake_request('pk=%s&_concurrency_version_1=2' % id) + + # post_param = 'form-_concurrency_version' if django.VERSION[:2] >= (4, 0) else '_concurrency_version' + + # request1 = get_fake_request('pk={}&{}_1=2'.format(id, post_param)) + request1 = get_fake_request(f'pk={id}&{concurrency_param_name}_1=2') model_admin.save_model(request1, obj, None, True) self.assertIn(obj.pk, model_admin._get_conflicts(request1)) obj = refetch(obj) - request2 = get_fake_request('pk=%s&_concurrency_version_1=%s' % (id, obj.version)) + request2 = get_fake_request(f'pk={id}&{concurrency_param_name}_1={obj.version}') model_admin.save_model(request2, obj, None, True) self.assertNotIn(obj.pk, model_admin._get_conflicts(request2)) @@ -71,7 +70,7 @@ def test_identity_tag(self): self.assertTrue(re.match(r"^%s,\d+$" % id, identity(obj))) g = User(username='UserTest', pk=3) - self.assertEqual(identity(g), force_text(g.pk)) + self.assertEqual(identity(g), force_str(g.pk)) @pytest.mark.django_db() @@ -99,7 +98,6 @@ def test_issue_54(): m2.save() -@skipIfDjangoVersion("!=(1,11)") @pytest.mark.django_db() def test_issue_81a(monkeypatch): monkeypatch.setattr('demo.admin.ActionsModelAdmin.fields', ('id',)) @@ -108,7 +106,6 @@ def test_issue_81a(monkeypatch): assert 'concurrency.A001' in str(e.value) -@skipIfDjangoVersion("<(1,11)") @pytest.mark.django_db() def test_issue_81b(monkeypatch): fieldsets = ( diff --git a/tests/test_loaddata_dumpdata.py b/tests/test_loaddata_dumpdata.py index f5c24f4..59fcc61 100644 --- a/tests/test_loaddata_dumpdata.py +++ b/tests/test_loaddata_dumpdata.py @@ -1,15 +1,11 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals - import json import logging import os -from six import StringIO - -from django.core.management import call_command +from io import StringIO import pytest from demo.models import SimpleConcurrentModel +from django.core.management import call_command logger = logging.getLogger(__name__) diff --git a/tests/test_manager.py b/tests/test_manager.py index 35b196c..b437db9 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,8 +1,6 @@ import pytest -from demo.models import ( - AutoIncConcurrentModel, ConcreteModel, CustomSaveModel, InheritedModel, ProxyModel, - SimpleConcurrentModel -) +from demo.models import (AutoIncConcurrentModel, ConcreteModel, CustomSaveModel, + InheritedModel, ProxyModel, SimpleConcurrentModel,) from demo.util import nextname, unique_id, with_models, with_std_models from concurrency.exceptions import RecordModifiedError diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 74701a7..a1e0a16 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,24 +1,18 @@ -# -*- coding: utf-8 -*- -from django.conf import settings -from django.contrib.admin.sites import site -from django.http import HttpRequest -from django.test.utils import override_settings - import mock from demo.base import AdminTestCase from demo.models import SimpleConcurrentModel from demo.util import DELETE_ATTRIBUTE, attributes, unique_id +from django.conf import settings +from django.contrib.admin.sites import site +from django.http import HttpRequest +from django.test.utils import override_settings +from django.urls import reverse from concurrency.admin import ConcurrentModelAdmin from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL from concurrency.exceptions import RecordModifiedError from concurrency.middleware import ConcurrencyMiddleware -try: - from django.core.urlresolvers import reverse -except ImportError: - from django.urls import reverse - def _get_request(path): request = HttpRequest() @@ -98,38 +92,3 @@ def test_in_admin(self): self.assertEqual(res.context['target'].version, target.version) self.assertEqual(res.context['saved'].version, saved.version) self.assertEqual(res.context['request_path'], url) - -# -# class TestFullStack(DjangoAdminTestCase): -# MIDDLEWARE_CLASSES = ('django.middleware.common.CommonMiddleware', -# 'django.contrib.sessions.middleware.SessionMiddleware', -# 'django.contrib.auth.middleware.AuthenticationMiddleware', -# 'django.contrib.messages.middleware.MessageMiddleware', -# 'concurrency.middleware.ConcurrencyMiddleware',) -# -# @mock.patch('django.core.signals.got_request_exception.send', mock.Mock()) -# def test_stack(self): -# admin_register(TestModel0, ModelAdmin) -# -# with self.settings(MIDDLEWARE_CLASSES=self.MIDDLEWARE_CLASSES): -# m, __ = TestModel0.objects.get_or_create(username="New", last_name="1") -# copy = TestModel0.objects.get(pk=m.pk) -# assert copy.version == m.version -# print 111111111111, m.version -# url = reverse('admin:concurrency_testmodel0_change', args=[m.pk]) -# data = {'username': 'new_username', -# 'last_name': None, -# 'version': VersionFieldSigner().sign(m.version), -# 'char_field': None, -# '_continue': 1, -# 'date_field': '2010-09-01'} -# copy.save() -# assert copy.version > m.version -# -# r = self.client.post(url, data, follow=True) -# self.assertEqual(r.status_code, 409) -# self.assertIn('target', r.context) -# self.assertIn('saved', r.context) -# self.assertEqual(r.context['saved'].version, copy.version) -# self.assertEqual(r.context['target'].version, m.version) -# self.assertEqual(r.context['request_path'], url) diff --git a/tests/test_reversion.py b/tests/test_reversion.py index 2526311..19def81 100644 --- a/tests/test_reversion.py +++ b/tests/test_reversion.py @@ -1,16 +1,9 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - import pytest from demo.models import ReversionConcurrentModel +from django.urls import reverse from reversion import add_to_revision, revisions, set_comment from reversion.models import Version -try: - from django.core.urlresolvers import reverse -except ImportError: - from django.urls import reverse - @pytest.mark.django_db @pytest.mark.functional diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index f88ecbf..66536ef 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals - import logging import pytest diff --git a/tests/test_threads.py b/tests/test_threads.py index 96b7f74..b0bf874 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -1,10 +1,9 @@ -from django import db -from django.db import transaction - import pytest from conftest import skippypy from demo.models import TriggerConcurrentModel from demo.util import concurrently +from django import db +from django.db import transaction from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch diff --git a/tests/test_triggers.py b/tests/test_triggers.py index 979c233..e69de29 100644 --- a/tests/test_triggers.py +++ b/tests/test_triggers.py @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -from django.db import connections - -import pytest -from demo.models import DropTriggerConcurrentModel, TriggerConcurrentModel # noqa - -from concurrency.triggers import drop_triggers, factory, get_triggers - -logger = logging.getLogger(__name__) - - -@pytest.mark.django_db -def test_list_triggers(): - conn = connections['default'] - - assert factory(conn).get_list() == [ - u'concurrency_demo_droptriggerconcurrentmodel_version', - u'concurrency_demo_triggerconcurrentmodel_version'] - - -@pytest.mark.django_db -def test_get_triggers(): - assert get_triggers(['default']) == {'default': [u'concurrency_demo_droptriggerconcurrentmodel_version', - u'concurrency_demo_triggerconcurrentmodel_version']} - assert get_triggers() == {'default': [u'concurrency_demo_droptriggerconcurrentmodel_version', - u'concurrency_demo_triggerconcurrentmodel_version']} - - -@pytest.mark.django_db -def test_get_trigger(monkeypatch): - conn = connections['default'] - f = factory(conn) - version_field = TriggerConcurrentModel._concurrencymeta.field - trigger = f.get_trigger(version_field) - assert trigger == 'concurrency_demo_triggerconcurrentmodel_version' - - monkeypatch.setattr(version_field, '_trigger_name', 'aaa') - assert f.get_trigger(version_field) is None - - -@pytest.mark.skipif('connections["default"].vendor=="mysql"', - reason="Mysql is not able to drop tringger inside trasaction") -@pytest.mark.django_db -def test_drop_trigger(): - conn = connections['default'] - f = [f for f in DropTriggerConcurrentModel._meta.fields if f.name == 'version'][0] - ret = factory(conn).drop(f) - assert ret == [u'concurrency_demo_droptriggerconcurrentmodel_version'] - assert factory(conn).get_list() == [u'concurrency_demo_triggerconcurrentmodel_version'] - - -@pytest.mark.skipif('connections["default"].vendor=="mysql"', - reason="Mysql is not able to drop tringger inside trasaction") -@pytest.mark.django_db -def test_drop_triggers(db): - conn = connections['default'] - ret = drop_triggers('default') - assert sorted([i[0].__name__ for i in ret['default']]) == ['DropTriggerConcurrentModel', - 'TriggerConcurrentModel'] - assert factory(conn).get_list() == [] diff --git a/tests/test_triggerversionfield.py b/tests/test_triggerversionfield.py index 285d0b8..de19f1c 100644 --- a/tests/test_triggerversionfield.py +++ b/tests/test_triggerversionfield.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- -from django.core.signals import request_started -from django.db import IntegrityError, connection, connections - import mock import pytest from demo.models import TriggerConcurrentModel # Register an event to reset saved queries when a Django request is started. from demo.util import nextname +from django.core.signals import request_started +from django.db import IntegrityError, connection, connections from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch @@ -17,7 +15,7 @@ def reset_queries(**kwargs): conn.queries = [] -class CaptureQueriesContext(object): +class CaptureQueriesContext: """ Context manager that captures queries executed by the specified connection. """ diff --git a/tests/test_utils.py b/tests/test_utils.py index efc6b99..4c6125b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals - import logging -from django.test import TestCase - import pytest from demo.models import SimpleConcurrentModel +from django.test import TestCase import concurrency.fields from concurrency.utils import ConcurrencyTestMixin, deprecated, fqn diff --git a/tox.ini b/tox.ini index 979f1b2..1eba000 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = d{30,31}-py{36,37,38}-{pg,sqlite,mysql} +envlist = d{22,32,40}-py{38,39,310}-{pg,sqlite,mysql} [pytest] @@ -26,7 +26,7 @@ markers = [testenv] ;install_command=pip install {opts} {packages} -passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH PYTHONDONTWRITEBYTECODE +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH PYTHONDONTWRITEBYTECODE PGHOST PGPORT whitelist_externals = /usr/local/bin/psql @@ -45,23 +45,21 @@ setenv = deps = django-reversion django-webtest + flake8 + isort mock pytest pytest-cov pytest-django pytest-echo pytest-pythonpath -; py{27,34,35,36,37,38}-pg: psycopg2-binary pypy-pg: psycopg2cffi mysql: mysqlclient docs: -rdocs/requirements.pip -; d111: django>=1.11,<1.12 -; d20: django>=2.0,<2.1 -; d21: django>=2.1,<2.2 -; d22: django>=2.2,<2.3 - d30: django>=3.0,<3.1 - d31: django>=3.1,<3.2 + d22: django==2.2.* + d32: django==3.2.* + d40: django==4.0.* commands = @@ -75,8 +73,8 @@ commands = [testenv:pg] commands = - - psql -h 127.0.0.1 -c 'DROP DATABASE "concurrency";' -U postgres - - psql -h 127.0.0.1 -c 'CREATE DATABASE "concurrency";' -U postgres + - psql -h $PGHOST -p $PGPORT -c 'DROP DATABASE "concurrency";' -U postgres + - psql -h $PGHOST -p $PGPORT -c 'CREATE DATABASE "concurrency";' -U postgres {[testenv]commands} [testenv:clean] @@ -89,3 +87,11 @@ commands = commands = mkdir -p {toxinidir}/~build/docs pipenv run sphinx-build -aE docs/ {toxinidir}/~build/docs + +[testenv:lint] +envdir={toxworkdir}/d32-py39/ +skip_install = true +commands = + pip install flake8 isort + flake8 src tests + isort -c src tests