diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..6d75b0c
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,90 @@
+name: Test
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "v[0-9]+.*"
+ pull_request:
+ branches:
+ - "*"
+
+ workflow_dispatch:
+
+env:
+ python-version: "3.14"
+
+jobs:
+ test-matrix:
+ name: Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - python-version: "3.10"
+ django-version: "5.2"
+ - python-version: "3.11"
+ django-version: "5.2"
+ - python-version: "3.12"
+ django-version: "5.2"
+ - python-version: "3.13"
+ django-version: "5.2"
+ - python-version: "3.14"
+ django-version: "5.2"
+
+ - python-version: "3.13"
+ django-version: "6.0"
+ - python-version: "3.14"
+ django-version: "6.0"
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+
+ - name: Install
+ shell: bash -l {0}
+ run: |
+ pip install -e.[testing] "Django~=${DJANGO_VERSION}"
+ env:
+ DJANGO_VERSION: ${{ matrix.django-version }}
+
+ - name: Test
+ shell: bash -l {0}
+ run: |
+ ./runtests.py
+
+ lint:
+ name: Lint and docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ env.python-version }}
+ cache: 'pip'
+
+ - name: Install
+ shell: bash -l {0}
+ run: |
+ pip install -e.[testing,docs]
+ env:
+ DJANGO_VERSION: ${{ matrix.django-version }}
+
+ - name: Lint
+ shell: bash -l {0}
+ run: |
+ ruff check enumchoicefield/
+
+ - name: Docs
+ shell: bash -l {0}
+ run: |
+ cd ./docs
+ make SPHINXOPTS=-nW clean html
diff --git a/docs/conf.py b/docs/conf.py
index 938c034..07cc01b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,11 +12,12 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import os
-import sys
+import importlib.metadata
+import pathlib
+import tomllib
+from packaging import version as _version
import sphinx_rtd_theme
-from enumchoicefield.version import version as module_version
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -52,19 +53,24 @@
# General information about the project.
project = 'Django EnumChoiceField'
-copyright = '2016, Tim Heap'
+copyright = '2026, Tim Heap'
author = 'Tim Heap'
-version = '.'.join(module_version.split('.')[:2])
+with open(pathlib.Path(__file__).parent.parent / 'pyproject.toml', 'rb') as f:
+ _pyproject = tomllib.load(f)
+
# The full version, including alpha/beta/rc tags.
-release = module_version
+release = _pyproject['project']['version']
+# The major.minor version
+_v = _version.parse(release)
+version = f'{_v.major}.{_v.minor}'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -118,9 +124,6 @@
# a list of builtin themes.
html_theme = "sphinx_rtd_theme"
-# Add any paths that contain custom themes here, relative to this directory.
-html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
-
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
diff --git a/docs/requirements.txt b/docs/requirements.txt
deleted file mode 100644
index 579bb10..0000000
--- a/docs/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Sphinx==2.3.1
-sphinx_rtd_theme
diff --git a/enumchoicefield/__init__.py b/enumchoicefield/__init__.py
index 400964a..ab1194d 100644
--- a/enumchoicefield/__init__.py
+++ b/enumchoicefield/__init__.py
@@ -1,7 +1,8 @@
-from __future__ import absolute_import, unicode_literals
+import importlib.metadata
from .enum import ChoiceEnum
from .fields import EnumChoiceField
-from .version import version
__all__ = ['ChoiceEnum', 'EnumChoiceField', 'version']
+
+version = importlib.metadata.version('django-enumchoicefield')
diff --git a/enumchoicefield/enum.py b/enumchoicefield/enum.py
index 1554a85..95b02bc 100644
--- a/enumchoicefield/enum.py
+++ b/enumchoicefield/enum.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
import enum
diff --git a/enumchoicefield/fields.py b/enumchoicefield/fields.py
index f09e888..4040554 100644
--- a/enumchoicefield/fields.py
+++ b/enumchoicefield/fields.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
from django.db.models.fields import Field
from .forms import EnumField
diff --git a/enumchoicefield/tests/test_field.py b/enumchoicefield/tests/test_field.py
index d9b724f..d9ddf1f 100644
--- a/enumchoicefield/tests/test_field.py
+++ b/enumchoicefield/tests/test_field.py
@@ -132,6 +132,6 @@ def test_exact(self):
ChoiceModel.objects.get(choice=MyEnum.foo))
def test_in(self):
- self.assertQuerysetEqual(
+ self.assertQuerySetEqual(
ChoiceModel.objects.filter(choice__in=[MyEnum.bar, MyEnum.baz]),
[self.bar, self.baz], transform=lambda x: x)
diff --git a/enumchoicefield/tests/test_forms.py b/enumchoicefield/tests/test_forms.py
index c12d4af..ae8252d 100644
--- a/enumchoicefield/tests/test_forms.py
+++ b/enumchoicefield/tests/test_forms.py
@@ -1,4 +1,3 @@
-import django
from django import forms
from django.core.exceptions import ValidationError
from django.forms.utils import flatatt
@@ -23,10 +22,18 @@ class TranslatedEnum(ChoiceEnum):
class SelectTestCase(SimpleTestCase):
- def assertSelectOptions(self, html, options, required=True, name='choice'):
- attrs = {'id': 'id_' + name, 'name': name}
- if django.VERSION >= (1, 10) and required:
- attrs['required'] = True
+ def assertSelectOptions(self, html, options, required=True, name='choice', error=False):
+ attrs = {
+ 'id': 'id_' + name,
+ 'name': name,
+ 'required': required,
+ }
+ if error:
+ attrs.update({
+ 'aria-describedby': 'id_' + name + '_error',
+ 'aria-invalid': 'true',
+ })
+
select = ''.format(
attrs=flatatt(attrs), options=''.join(options))
self.assertHTMLEqual(select, html)
@@ -143,7 +150,7 @@ def test_invalid_choice(self):
'',
'',
'',
- ])
+ ], error=True)
class TestLimitedMembers(SelectTestCase):
diff --git a/enumchoicefield/utils.py b/enumchoicefield/utils.py
index 6ea1396..03c4b74 100644
--- a/enumchoicefield/utils.py
+++ b/enumchoicefield/utils.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
from django.db.models import Case, IntegerField, When
diff --git a/enumchoicefield/version.py b/enumchoicefield/version.py
deleted file mode 100644
index 5918abd..0000000
--- a/enumchoicefield/version.py
+++ /dev/null
@@ -1 +0,0 @@
-version = '3.0.0'
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..f44c352
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,54 @@
+[build-system]
+requires = [
+ "setuptools >= 35.0.2",
+]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "django-enumchoicefield"
+version = "3.0.1"
+description = "A choice field for Django using native Python Enums"
+authors = [
+ {name = "Tim Heap", email = "tim@timheap.me"},
+]
+license = "BSD-2-Clause"
+license-files = ["LICENSE"]
+requires-python = ">=3.10"
+dependencies = [
+ "Django>=5.2"
+]
+
+dynamic = ["readme"]
+
+classifiers = [
+ "Environment :: Web Environment",
+ "Intended Audience :: Developers",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Framework :: Django",
+]
+
+[project.optional-dependencies]
+testing = [
+ "dj-database-url~=3.1.2",
+ "ruff",
+]
+
+docs = [
+ "Sphinx~=9.1.0",
+ "sphinx_rtd_theme",
+]
+
+[project.urls]
+Documentation = "https://django-enumchoicefield.readthedocs.io/"
+Source = "https://github.com/mx-moth/django-enumchoicefield"
+
+[tool.setuptools.dynamic]
+readme = {file = ["README.rst"], content-type = "text/x-rst"}
+
+[tool.isort]
+line_length = 79
+multi_line_output = 4
+known_first_party = "enumchoicefield,tests"
+skip = "migrations"
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index a7e32c0..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,2 +0,0 @@
--r requirements-test.txt
-tox==2.0.1
diff --git a/requirements-test.txt b/requirements-test.txt
deleted file mode 100644
index 7b13ade..0000000
--- a/requirements-test.txt
+++ /dev/null
@@ -1 +0,0 @@
-dj-database-url~=0.4.1
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index db24bc0..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,13 +0,0 @@
-[metadata]
-description_file = README.rst
-
-[bdist_wheel]
-universal = 1
-
-[flake8]
-exclude = migrations
-
-[isort]
-line_length = 79
-multi_line_output = 4
-skip = migrations
diff --git a/setup.py b/setup.py
deleted file mode 100755
index a4e5854..0000000
--- a/setup.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python
-"""
-Install django-enumchoicefield using setuptools
-"""
-from setuptools import find_packages, setup
-
-with open('README.rst', 'r') as f:
- readme = f.read()
-
-with open('enumchoicefield/version.py') as v:
- version = None
- exec(v.read()) # Get version
-
-
-setup(
- name='django-enumchoicefield',
- version=version,
- description='A choice field for Django using native Python Enums',
- long_description=readme,
- author='Tim Heap',
- author_email='tim@timheap.me',
- url='https://github.com/timheap/django-enumchoicefield',
-
- install_requires=['Django>=2.0'],
- zip_safe=False,
- license='BSD License',
-
- packages=find_packages(),
-
- include_package_data=True,
- package_data={},
-
- classifiers=[
- 'Environment :: Web Environment',
- 'Intended Audience :: Developers',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
- 'Framework :: Django',
- 'License :: OSI Approved :: BSD License',
- ],
-)
diff --git a/tox.ini b/tox.ini
index fe454da..40576e8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,40 +2,34 @@
skip_missing_interpreters = True
envlist =
- py37-dj{2x,3x}
- py38-dj{2x,3x,4x}
- py39-dj{2x,3x,4x}
- py310-dj{3x,4x}
- flake8,isort,docs
+ py310-dj{52}
+ py311-dj{52}
+ py312-dj{52,60}
+ py313-dj{52,60}
+ py314-dj{52,60}
+ ruff,docs
[testenv]
commands = python runtests.py {posargs}
-usedevelop = True
+package = editable
+extras = testing
pip_pre = True
setenv = PYTHONDONTWRITEBYTECODE=1
-passenv = DJANGO_SETTINGS_MODULE DATABASE_URL
+passenv = "DJANGO_SETTINGS_MODULE,DATABASE_URL"
deps =
- -rrequirements-test.txt
- dj2x: Django~=2.2.17
- dj3x: Django~=3.2.9
- dj4x: Django~=4.0.0
+ dj52: Django~=5.2.0
+ dj60: Django~=6.0.0
postgres: psycopg2
mysql: mysqlclient
-[testenv:flake8]
+[testenv:ruff]
basepython = python3
-deps = flake8
-commands = flake8 enumchoicefield/
-
-[testenv:isort]
-basepython = python3
-deps = isort
-commands = isort --check-only --diff enumchoicefield/
+commands = ruff check enumchoicefield/
[testenv:docs]
basepython = python3
-deps = -r{toxinidir}/docs/requirements.txt
+extras = testing,docs
changedir = docs
-whitelist_externals = make
+allowlist_externals = make
commands = make SPHINXOPTS=-nW clean html