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..36fe20b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,15 @@ ## 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 +- Add support for python 3.12 +- Drop support for django < 3.2 +- Add support for django 4.2 +- Add support for django 5.0 +- Enable Github actions for testing +- Remove South support...if exists ## django-fsm 2.8.1 2022-08-15 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c755f69..41a7188 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ django-fsm unreleased - Add support for django 4.2 - Add support for django 5.0 - Enable Github actions for testing +- Remove South support...if exists django-fsm 2.8.1 2022-08-15 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/django_fsm/__init__.py b/django_fsm/__init__.py index dfd54cc..3d6dec0 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""" @@ -308,7 +289,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}") @@ -385,9 +367,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 26356c2..0462caf 100644 --- a/django_fsm/tests/test_basic_transitions.py +++ b/django_fsm/tests/test_basic_transitions.py @@ -177,12 +177,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) @@ -221,14 +221,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", + ], )