diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9f31653 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: Test + +on: + pull_request: + types: [ opened, synchronize, reopened ] + branches: + - master + push: + branches: + - master + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python: "3.7" + tox_env: py37-django22 + - python: "3.7" + tox_env: py37-django30 + - python: "3.7" + tox_env: py37-django31 + - python: "3.8" + tox_env: py38-django22 + - python: "3.8" + tox_env: py38-django30 + - python: "3.8" + tox_env: py38-django31 + - python: "3.7" + tox_env: checks + name: ${{ matrix.tox_env }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: pip install tox + - name: Run ${{ matrix.tox_env }} job + run: tox -e ${{ matrix.tox_env }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 09c99ed..0000000 --- a/.travis.yml +++ /dev/null @@ -1,70 +0,0 @@ -sudo: true - -language: python - -cache: - apt: true - -addons: - apt_packages: - - sqlite3 - -matrix: - include: - - env: ENV=py27-django111 - python: 2.7 - - - env: ENV=py34-django111 - python: 3.4 - sudo: false - dist: trusty - - env: ENV=py34-django20 - python: 3.4 - sudo: false - dist: trusty - - - env: ENV=py35-django111 - python: 3.5 - - env: ENV=py35-django20 - python: 3.5 - - env: ENV=py35-django21 - python: 3.5 - - env: ENV=py35-django22 - python: 3.5 - - - env: ENV=py36-django111 - python: 3.6 - - env: ENV=py36-django20 - python: 3.6 - - env: ENV=py36-django21 - python: 3.6 - - env: ENV=py36-django22 - python: 3.6 - - env: ENV=py36-django30 - python: 3.6 - - - env: ENV=py37-django20 - python: 3.7 - - env: ENV=py37-django21 - python: 3.7 - - env: ENV=py37-django22 - python: 3.7 - - env: ENV=py37-django30 - python: 3.7 - - - env: ENV=py38-django20 - python: 3.8 - - env: ENV=py38-django21 - python: 3.8 - - env: ENV=py38-django22 - python: 3.8 - - env: ENV=py38-django30 - python: 3.8 - - - env: ENV=checks - python: 3.7 - -install: - - pip install tox - -script: tox -e $ENV diff --git a/Makefile b/Makefile index 9c74503..e28e38b 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ black-check: black --check django_enumfield run_tests.py setup.py .PHONY: checks -checks: flake8 mypy black-check +checks: mypy flake8 black-check .PHONY: format format: black isort diff --git a/django_enumfield/contrib/drf.py b/django_enumfield/contrib/drf.py index 05cdb9f..10a28cb 100644 --- a/django_enumfield/contrib/drf.py +++ b/django_enumfield/contrib/drf.py @@ -1,4 +1,3 @@ -import six from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -18,7 +17,7 @@ def get_choice_value(self, enum_value): return enum_value.value def to_internal_value(self, data): - if isinstance(data, six.string_types) and data.isdigit(): + if isinstance(data, str) and data.isdigit(): data = int(data) try: diff --git a/django_enumfield/db/fields.py b/django_enumfield/db/fields.py index 65e0aca..3953bab 100644 --- a/django_enumfield/db/fields.py +++ b/django_enumfield/db/fields.py @@ -2,7 +2,6 @@ from functools import partial from typing import Any, Callable # noqa: F401 -import six from django import forms from django.db import models from django.utils.encoding import force_text @@ -19,7 +18,6 @@ def partialishmethod(method): return _partialmethod(method) - except ImportError: # pragma: no cover from django.utils.functional import curry @@ -85,7 +83,7 @@ def from_db_value(self, value, *_): def to_python(self, value): if value is not None: - if isinstance(value, six.text_type) and value.isdigit(): + if isinstance(value, str) and value.isdigit(): value = int(value) return self.enum.get(value) @@ -120,10 +118,8 @@ def set_enum(self, new_value): except ValueError: raise InvalidStatusOperationError( ugettext( - six.text_type( - "{value!r} is not one of the available choices " - "for enum {enum}." - ) + "{value!r} is not one of the available choices " + "for enum {enum}." ).format(value=new_value, enum=enum) ) setattr(self, private_att_name, new_value) diff --git a/django_enumfield/enum.py b/django_enumfield/enum.py index 196eac1..dcf0971 100644 --- a/django_enumfield/enum.py +++ b/django_enumfield/enum.py @@ -1,10 +1,10 @@ from __future__ import absolute_import import logging -from enum import Enum as NativeEnum, IntEnum as NativeIntEnum -from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union, cast - -import six +from collections import abc +import enum +from typing import Any, List, Optional, Sequence, Tuple, TypeVar, Union, cast, Mapping +from django.utils.encoding import force_str try: from django.utils.functional import classproperty # type: ignore @@ -20,7 +20,7 @@ RAISE = object() -class BlankEnum(NativeEnum): +class BlankEnum(enum.Enum): BLANK = "" @property @@ -45,13 +45,15 @@ def __get__(self, instance, cls=None): T = TypeVar("T", bound="Enum") -@six.python_2_unicode_compatible -class Enum(NativeIntEnum): - """ A container for holding and restoring enum values """ +class Enum(enum.IntEnum): + """A container for holding and restoring enum values""" - __labels__ = {} # type: Dict[int, six.text_type] - __default__ = None # type: Optional[int] - __transitions__ = {} # type: Dict[int, Sequence[int]] + # TODO: Can be uncommented once https://github.com/python/mypy/issues/12132 + # has been resolved. If we keep these uncommented we'd pollute with mypy errors + # everywhere someone declares these attributes on their own enum class. + # __labels__ = {} # type: Dict[int, str] + # __default__ = None # type: Optional[int] + # __transitions__ = {} # type: Dict[int, Sequence[int]] def __str__(self): return self.label @@ -68,8 +70,12 @@ def label(self): :return: label for value :rtype: str """ - label = cast(str, self.__class__.__labels__.get(self.value, self.name)) - return six.text_type(label) + # TODO: Can be uncommented once https://github.com/python/mypy/issues/12132 + # has been resolved. + # labels = self.__class__.__labels__ + labels: Mapping[int, str] = getattr(self.__class__, "__labels__", {}) + assert isinstance(labels, abc.Mapping) + return force_str(labels.get(self.value, self.name)) @classproperty def do_not_call_in_templates(cls): @@ -103,13 +109,13 @@ def items(cls): @classmethod def choices(cls, blank=False): - # type: (bool) -> List[Tuple[Union[int, str], NativeEnum]] + # type: (bool) -> List[Tuple[Union[int, str], enum.Enum]] """Choices for Enum :return: List of tuples (, ) """ choices = sorted( [(member.value, member) for member in cls], key=lambda x: x[0] - ) # type: List[Tuple[Union[str, int], NativeEnum]] + ) # type: List[Tuple[Union[str, int], enum.Enum]] if blank: choices.insert(0, (BlankEnum.BLANK.value, BlankEnum.BLANK)) return choices @@ -124,8 +130,13 @@ def default(cls): IntegerField(choices=my_enum.choices(), default=my_enum.default(), ... :return Default value, if set. """ - if cls.__default__ is not None: - return cast(Enum, cls(cls.__default__)) + # TODO: Can be uncommented once https://github.com/python/mypy/issues/12132 + # has been resolved. + # value = cls.__default__ + value: Optional[int] = getattr(cls, "__default__", None) + assert value is None or isinstance(value, int) + if value is not None: + return cast(Enum, cls(value)) return None @classmethod @@ -164,7 +175,7 @@ def get( return cls(name_or_numeric) except ValueError: pass - elif isinstance(name_or_numeric, six.string_types): + elif isinstance(name_or_numeric, str): try: return cls[name_or_numeric] except KeyError: @@ -211,9 +222,14 @@ def is_valid_transition(cls, from_value, to_value): if isinstance(to_value, cls): to_value = to_value.value + # TODO: Can be uncommented once https://github.com/python/mypy/issues/12132 + # has been resolved. + # transitions = cls.__transitions__ + transitions: Mapping[int, Sequence[int]] = getattr(cls, "__transitions__", {}) + assert isinstance(transitions, abc.Mapping) return ( from_value == to_value - or not cls.__transitions__ + or not transitions or (from_value in cls.transition_origins(to_value)) ) @@ -225,4 +241,10 @@ def transition_origins(cls, to_value): """ if isinstance(to_value, cls): to_value = to_value.value - return cast(Sequence[int], cls.__transitions__.get(to_value, [])) + + # TODO: Can be uncommented once https://github.com/python/mypy/issues/12132 + # has been resolved. + # transitions = cls.__transitions__ + transitions: Mapping[int, Sequence[int]] = getattr(cls, "__transitions__", {}) + assert isinstance(transitions, abc.Mapping) + return transitions.get(to_value, []) diff --git a/django_enumfield/tests/test_enum.py b/django_enumfield/tests/test_enum.py index 9f9a5ca..35d4dbf 100644 --- a/django_enumfield/tests/test_enum.py +++ b/django_enumfield/tests/test_enum.py @@ -1,7 +1,6 @@ from contextlib import contextmanager from os.path import abspath, dirname, exists, join -import six from django import forms from django.core.management import call_command from django.db import IntegrityError, connection @@ -190,8 +189,8 @@ def test_enum_field_modelform_initial(self): form = PersonForm(instance=person) self.assertEqual(form.fields["status"].initial, PersonStatus.ALIVE.value) self.assertIn( - u'