From 8708f7df9ddb51724d2ab305a5388ce426eba575 Mon Sep 17 00:00:00 2001 From: Pascal F Date: Sun, 29 Oct 2023 05:38:53 +0100 Subject: [PATCH] Fix pre commit + cleanup old compatibility code (#10) Fix ruff settings --- .github/workflows/lint.yml | 17 ++ .github/workflows/tests.yml | 5 +- .pre-commit-config.yaml | 15 +- CHANGELOG.md | 2 + CHANGELOG.rst | 161 ------------------ django_fsm/__init__.py | 30 +--- .../management/commands/graph_transitions.py | 122 +++---------- django_fsm/tests/test_abstract_inheritance.py | 5 +- django_fsm/tests/test_basic_transitions.py | 30 ++-- django_fsm/tests/test_proxy_inheritance.py | 4 +- poetry.lock | 20 +-- pyproject.toml | 14 +- setup.py | 52 +++--- 13 files changed, 126 insertions(+), 351 deletions(-) create mode 100644 .github/workflows/lint.yml delete mode 100644 CHANGELOG.rst diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..636505e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +name: django-fsm linting + +on: + pull_request: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v2 + with: + python-version: '3.11' + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9bd7701..4595c4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,9 @@ name: django-fsm testing on: - - push - - pull_request + pull_request: + push: + branches: [main] jobs: build: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39f9761..783ad8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,13 +17,6 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 - hooks: - - id: pyupgrade - args: - - "--py38-plus" - - repo: https://github.com/adamchainz/django-upgrade rev: 1.15.0 hooks: @@ -32,14 +25,14 @@ repos: - repo: https://github.com/python-poetry/poetry - rev: 1.6.1 + rev: 1.6.0 hooks: - id: poetry-check - - id: poetry-lock - - id: poetry-export + # - id: poetry-lock + # - id: poetry-export - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.291 + rev: v0.1.3 hooks: - id: ruff-format - id: ruff diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc3e81..882cf31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## django-fsm unreleased + +- Remove South support...if exists - Add support for django 5.0 - Remove support for django < 3.2 - Add support for python 3.12 diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 57081a8..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,161 +0,0 @@ -Changelog -========= - -django-fsm unreleased -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Add support for django 5.0 -- Remove support for django < 3.2 -- Add support for python 3.12 -- Add support for python 3.11 -- Drop support for Python < 3.7. -- Enable Github actions for testing -- Add support for django 4.2 -- Add support for python 3.11 - -django-fsm 2.8.1 2022-08-15 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Improve fix for get_available_FIELD_transition - -django-fsm 2.8.0 2021-11-05 -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Fix get_available_FIELD_transition on django>=3.2 -- Fix refresh_from_db for ConcurrentTransitionMixin - - -django-fsm 2.7.1 2020-10-13 -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Fix warnings on Django 3.1+ - - -django-fsm 2.7.0 2019-12-03 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Django 3.0 support -- Test on Python 3.8 - - -django-fsm 2.6.1 2019-04-19 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Update pypi classifiers to latest django/python supported versions -- Several fixes for graph_transition command - - -django-fsm 2.6.0 2017-06-08 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Fix django 1.11 compatibility -- Fix TypeError in `graph_transitions` command when using django's lazy translations - - -django-fsm 2.5.0 2017-03-04 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- graph_transition command fix for django 1.10 -- graph_transition command supports GET_STATE targets -- signal data extended with method args/kwargs and field -- sets allowed to be passed to the transition decorator - - -django-fsm 2.4.0 2016-05-14 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- graph_transition commnad now works with multiple FSM's per model -- Add ability to set target state from transition return value or callable - - -django-fsm 2.3.0 2015-10-15 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Add source state shortcut '+' to specify transitions from all states except the target -- Add object-level permission checks -- Fix translated labels for graph of FSMIntegerField -- Fix multiple signals for several transition decorators - - -django-fsm 2.2.1 2015-04-27 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Improved exception message for unmet transition conditions. -- Don't send post transition signal in case of no state changes on - exception -- Allow empty string as correct state value -- Improved graphviz fsm visualisation -- Clean django 1.8 warnings - -django-fsm 2.2.0 2014-09-03 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Support for `class - substitution `__ - to proxy classes depending on the state -- Added ConcurrentTransitionMixin with optimistic locking support -- Default db\_index=True for FSMIntegerField removed -- Graph transition code migrated to new graphviz library with python 3 - support -- Ability to change state on transition exception - -django-fsm 2.1.0 2014-05-15 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Support for attaching permission checks on model transitions - -django-fsm 2.0.0 2014-03-15 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Backward incompatible release -- All public code import moved directly to django\_fsm package -- Correct support for several @transitions decorator with different - source states and conditions on same method -- save parameter from transition decorator removed -- get\_available\_FIELD\_transitions return Transition data object - instead of tuple -- Models got get\_available\_FIELD\_transitions, even if field - specified as string reference -- New get\_all\_FIELD\_transitions method contributed to class - -django-fsm 1.6.0 2014-03-15 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- FSMIntegerField and FSMKeyField support - -django-fsm 1.5.1 2014-01-04 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Ad-hoc support for state fields from proxy and inherited models - -django-fsm 1.5.0 2013-09-17 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Python 3 compatibility - -django-fsm 1.4.0 2011-12-21 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Add graph\_transition command for drawing state transition picture - -django-fsm 1.3.0 2011-07-28 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Add direct field modification protection - -django-fsm 1.2.0 2011-03-23 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Add pre\_transition and post\_transition signals - -django-fsm 1.1.0 2011-02-22 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Add support for transition conditions -- Allow multiple FSMField in one model -- Contribute get\_available\_FIELD\_transitions for model class - -django-fsm 1.0.0 2010-10-12 -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Initial public release diff --git a/django_fsm/__init__.py b/django_fsm/__init__.py index a432bb0..4822227 100644 --- a/django_fsm/__init__.py +++ b/django_fsm/__init__.py @@ -5,22 +5,13 @@ from functools import partialmethod, wraps import django +from django.apps import apps as django_apps from django.db import models from django.db.models import Field from django.db.models.query_utils import DeferredAttribute from django.db.models.signals import class_prepared -from django_fsm.signals import pre_transition, post_transition - -try: - from django.apps import apps as django_apps - - def get_model(app_label, model_name): - app = django_apps.get_app_config(app_label) - return app.get_model(model_name) - -except ImportError: - from django.db.models.loading import get_model +from django_fsm.signals import pre_transition, post_transition __all__ = [ @@ -38,16 +29,6 @@ def get_model(app_label, model_name): "RETURN_VALUE", ] -# South support; see http://south.aeracode.org/docs/tutorial/part4.html#simple-inheritance -try: - from south.modelsinspector import add_introspection_rules -except ImportError: - pass -else: - add_introspection_rules([], [r"^django_fsm\.FSMField"]) - add_introspection_rules([], [r"^django_fsm\.FSMIntegerField"]) - add_introspection_rules([], [r"^django_fsm\.FSMKeyField"]) - class TransitionNotAllowed(Exception): """Raised when a transition is not allowed""" @@ -297,7 +278,8 @@ def set_proxy(self, instance, state): app_label = instance._meta.app_label model_name = state_proxy - model = get_model(app_label, model_name) + model = django_apps.get_app_config(app_label).get_model(model_name) + if model is None: raise ValueError(f"No model found {state_proxy}") @@ -374,9 +356,7 @@ def contribute_to_class(self, cls, name, **kwargs): super().contribute_to_class(cls, name, **kwargs) setattr(cls, self.name, self.descriptor_class(self)) setattr(cls, f"get_all_{self.name}_transitions", partialmethod(get_all_FIELD_transitions, field=self)) - setattr( - cls, f"get_available_{self.name}_transitions", partialmethod(get_available_FIELD_transitions, field=self) - ) + setattr(cls, f"get_available_{self.name}_transitions", partialmethod(get_available_FIELD_transitions, field=self)) setattr( cls, f"get_available_user_{self.name}_transitions", diff --git a/django_fsm/management/commands/graph_transitions.py b/django_fsm/management/commands/graph_transitions.py index c66b14a..18ba9ad 100644 --- a/django_fsm/management/commands/graph_transitions.py +++ b/django_fsm/management/commands/graph_transitions.py @@ -1,37 +1,15 @@ import graphviz -from optparse import make_option from itertools import chain +from django.apps import apps from django.core.management.base import BaseCommand -try: - from django.utils.encoding import force_str - _requires_system_checks = True -except ImportError: # Django >= 4.0 - from django.utils.encoding import force_str as force_text - from django.core.management.base import ALL_CHECKS - _requires_system_checks = ALL_CHECKS +from django.utils.encoding import force_str from django_fsm import FSMFieldMixin, GET_STATE, RETURN_VALUE -try: - from django.db.models import get_apps, get_app, get_models, get_model - - NEW_META_API = False -except ImportError: - from django.apps import apps - - NEW_META_API = True - -from django import VERSION - -HAS_ARGPARSE = VERSION >= (1, 10) - def all_fsm_fields_data(model): - if NEW_META_API: - return [(field, model) for field in model._meta.get_fields() if isinstance(field, FSMFieldMixin)] - else: - return [(field, model) for field in model._meta.fields if isinstance(field, FSMFieldMixin)] + return [(field, model) for field in model._meta.get_fields() if isinstance(field, FSMFieldMixin)] def node_name(field, state): @@ -40,7 +18,7 @@ def node_name(field, state): def node_label(field, state): - if type(state) == int or (type(state) == bool and hasattr(field, "choices")): + if isinstance(state, int) or (isinstance(state, bool) and hasattr(field, "choices")): return force_str(dict(field.choices).get(state)) else: return state @@ -137,54 +115,26 @@ def get_graphviz_layouts(): class Command(BaseCommand): - requires_system_checks = _requires_system_checks - - if not HAS_ARGPARSE: - option_list = BaseCommand.option_list + ( - make_option( - "--output", - "-o", - action="store", - dest="outputfile", - help=( - "Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image." - ), - ), - # NOQA - make_option( - "--layout", - "-l", - action="store", - dest="layout", - default="dot", - help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())), - ), - ) - args = "[appname[.model[.field]]]" - else: - - def add_arguments(self, parser): - parser.add_argument( - "--output", - "-o", - action="store", - dest="outputfile", - help=( - "Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image." - ), - ) - parser.add_argument( - "--layout", - "-l", - action="store", - dest="layout", - default="dot", - help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())), - ) - parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]")) - help = "Creates a GraphViz dot file with transitions for selected fields" + def add_arguments(self, parser): + parser.add_argument( + "--output", + "-o", + action="store", + dest="outputfile", + help=("Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image."), + ) + parser.add_argument( + "--layout", + "-l", + action="store", + dest="layout", + default="dot", + help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())), + ) + parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]")) + def render_output(self, graph, **options): filename, format = options["outputfile"].rsplit(".", 1) @@ -199,35 +149,19 @@ def handle(self, *args, **options): field_spec = arg.split(".") if len(field_spec) == 1: - if NEW_META_API: - app = apps.get_app(field_spec[0]) - models = apps.get_models(app) - else: - app = get_app(field_spec[0]) - models = get_models(app) + app = apps.get_app(field_spec[0]) + models = apps.get_models(app) for model in models: fields_data += all_fsm_fields_data(model) elif len(field_spec) == 2: - if NEW_META_API: - model = apps.get_model(field_spec[0], field_spec[1]) - else: - model = get_model(field_spec[0], field_spec[1]) + model = apps.get_model(field_spec[0], field_spec[1]) fields_data += all_fsm_fields_data(model) elif len(field_spec) == 3: - if NEW_META_API: - model = apps.get_model(field_spec[0], field_spec[1]) - else: - model = get_model(field_spec[0], field_spec[1]) + model = apps.get_model(field_spec[0], field_spec[1]) fields_data += all_fsm_fields_data(model) else: - if NEW_META_API: - for model in apps.get_models(): - fields_data += all_fsm_fields_data(model) - else: - for app in get_apps(): - for model in get_models(app): - fields_data += all_fsm_fields_data(model) - + for model in apps.get_models(): + fields_data += all_fsm_fields_data(model) dotdata = generate_dot(fields_data) if options["outputfile"]: diff --git a/django_fsm/tests/test_abstract_inheritance.py b/django_fsm/tests/test_abstract_inheritance.py index 12643e7..8daf864 100644 --- a/django_fsm/tests/test_abstract_inheritance.py +++ b/django_fsm/tests/test_abstract_inheritance.py @@ -21,6 +21,7 @@ class AnotherFromAbstractModel(BaseAbstractModel): inherit from a shared abstract class (example: BaseAbstractModel). Don't try to remove it. """ + @transition(field="state", source="published", target="sticked") def stick(self): pass @@ -53,6 +54,4 @@ def test_field_available_transitions_works(self): def test_field_all_transitions_works(self): transitions = self.model.get_all_state_transitions() - self.assertEqual( - {("new", "published"), ("published", "sticked")}, {(data.source, data.target) for data in transitions} - ) + self.assertEqual({("new", "published"), ("published", "sticked")}, {(data.source, data.target) for data in transitions}) diff --git a/django_fsm/tests/test_basic_transitions.py b/django_fsm/tests/test_basic_transitions.py index 763ea9e..9536421 100644 --- a/django_fsm/tests/test_basic_transitions.py +++ b/django_fsm/tests/test_basic_transitions.py @@ -154,12 +154,12 @@ def test_available_conditions_from_published(self): transitions = self.model.get_available_state_transitions() actual = {(transition.source, transition.target) for transition in transitions} expected = { - ("*", "moderated"), - ("published", None), - ("published", "hidden"), - ("published", "stolen"), - ("*", ""), - ("+", "blocked"), + ("*", "moderated"), + ("published", None), + ("published", "hidden"), + ("published", "stolen"), + ("*", ""), + ("+", "blocked"), } self.assertEqual(actual, expected) @@ -198,14 +198,14 @@ def test_all_conditions(self): actual = {(transition.source, transition.target) for transition in transitions} expected = { - ("*", "moderated"), - ("new", "published"), - ("new", "removed"), - ("published", None), - ("published", "hidden"), - ("published", "stolen"), - ("hidden", "stolen"), - ("*", ""), - ("+", "blocked"), + ("*", "moderated"), + ("new", "published"), + ("new", "removed"), + ("published", None), + ("published", "hidden"), + ("published", "stolen"), + ("hidden", "stolen"), + ("*", ""), + ("+", "blocked"), } self.assertEqual(actual, expected) diff --git a/django_fsm/tests/test_proxy_inheritance.py b/django_fsm/tests/test_proxy_inheritance.py index 1910f92..0772e44 100644 --- a/django_fsm/tests/test_proxy_inheritance.py +++ b/django_fsm/tests/test_proxy_inheritance.py @@ -46,6 +46,4 @@ def test_field_all_transitions_base_model(self): def test_field_all_transitions_works(self): transitions = self.model.get_all_state_transitions() - self.assertEqual( - {("new", "published"), ("published", "sticked")}, {(data.source, data.target) for data in transitions} - ) + self.assertEqual({("new", "published"), ("published", "sticked")}, {(data.source, data.target) for data in transitions}) diff --git a/poetry.lock b/poetry.lock index 8113b66..4018902 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.0 and should not be changed by hand. [[package]] name = "asgiref" @@ -168,19 +168,19 @@ Django = ">=2.2" [[package]] name = "filelock" -version = "3.12.4" +version = "3.13.0" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, - {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, + {file = "filelock-3.13.0-py3-none-any.whl", hash = "sha256:a552f4fde758f4eab33191e9548f671970f8b06d436d31388c9aa1e5861a710f"}, + {file = "filelock-3.13.0.tar.gz", hash = "sha256:63c6052c82a1a24c873a549fbd39a26982e8f35a3016da231ead11a5be9dad44"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] -typing = ["typing-extensions (>=4.7.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "graphviz" @@ -200,13 +200,13 @@ test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>= [[package]] name = "identify" -version = "2.5.30" +version = "2.5.31" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, - {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, + {file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"}, + {file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"}, ] [package.extras] diff --git a/pyproject.toml b/pyproject.toml index b087299..521cd8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,15 @@ +[tool.ruff] +line-length = 130 +target-version = "py38" + +[tool.ruff.lint] +extend-select = [ + "F", # Pyflakes + "E", # pycodestyle + "W", # pycodestyle + "UP", # pyupgrade +] + [tool.poetry] name = "django-fsm" version = "2.8.1" @@ -20,12 +32,12 @@ classifiers = [ "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", 'Programming Language :: Python', + 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', - 'Framework :: Django', 'Topic :: Software Development :: Libraries :: Python Modules', ] packages = [{ include = "django_fsm" }] diff --git a/setup.py b/setup.py index 574deac..143f698 100644 --- a/setup.py +++ b/setup.py @@ -1,42 +1,42 @@ from setuptools import setup try: - long_description = open('README.rst').read() + long_description = open("README.rst").read() except OSError: - long_description = '' + long_description = "" setup( - name='django-fsm', - version='2.8.1', - description='Django friendly finite state machine support.', - author='Mikhail Podgurskiy', - author_email='kmmbvnr@gmail.com', - url='http://github.com/kmmbvnr/django-fsm', + name="django-fsm", + version="2.8.1", + description="Django friendly finite state machine support.", + author="Mikhail Podgurskiy", + author_email="kmmbvnr@gmail.com", + url="http://github.com/kmmbvnr/django-fsm", keywords="django", - packages=['django_fsm', 'django_fsm.management', 'django_fsm.management.commands'], + packages=["django_fsm", "django_fsm.management", "django_fsm.management.commands"], include_package_data=True, zip_safe=False, - license='MIT License', - platforms=['any'], - python_requires='>=3.7', + license="MIT License", + platforms=["any"], + python_requires=">=3.7", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Software Development :: Libraries :: Python Modules', - ] + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + ], )