Skip to content

Commit

Permalink
Merge branch 'adamchainz-drop_old_pythons'
Browse files Browse the repository at this point in the history
  • Loading branch information
pfouque committed Oct 28, 2023
2 parents a1eb871 + 1e981d7 commit 5e24a14
Show file tree
Hide file tree
Showing 19 changed files with 73 additions and 154 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ sudo: false
cache: pip

python:
- 2.7
- 3.6
- 3.7
- 3.8
- 3.9
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
django-fsm unreleased
~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Drop support for Python < 3.7.
- add support for django 4.2
- add support for python 3.11

Expand All @@ -12,7 +13,6 @@ django-fsm 2.8.1 2022-08-15

- Improve fix for get_available_FIELD_transition


django-fsm 2.8.0 2021-11-05
~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 0 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ Or, for the latest git version
$ pip install -e git://github.com/kmmbvnr/django-fsm.git#egg=django-fsm
The library has full Python 3 support

Usage
-----

Expand Down
107 changes: 39 additions & 68 deletions django_fsm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
"""
State tracking functionality for django models
"""
import inspect
import sys
from functools import wraps
from functools import partialmethod, wraps

import django
from django.db import models
Expand All @@ -13,12 +11,6 @@
from django.db.models.signals import class_prepared
from django_fsm.signals import pre_transition, post_transition

try:
from functools import partialmethod
except ImportError:
# python 2.7, so we are on django<=1.11
from django.utils.functional import curry as partialmethod

try:
from django.apps import apps as django_apps

Expand Down Expand Up @@ -46,25 +38,6 @@ def get_model(app_label, model_name):
"RETURN_VALUE",
]

if sys.version_info[:2] == (2, 6):
# Backport of Python 2.7 inspect.getmembers,
# since Python 2.6 ships buggy implementation
def __getmembers(object, predicate=None):
"""Return all members of an object as (name, value) pairs sorted by name.
Optionally, only return members that satisfy a given predicate."""
results = []
for key in dir(object):
try:
value = getattr(object, key)
except AttributeError:
continue
if not predicate or predicate(value):
results.append((key, value))
results.sort()
return results

inspect.getmembers = __getmembers

# South support; see http://south.aeracode.org/docs/tutorial/part4.html#simple-inheritance
try:
from south.modelsinspector import add_introspection_rules
Expand All @@ -82,7 +55,7 @@ class TransitionNotAllowed(Exception):
def __init__(self, *args, **kwargs):
self.object = kwargs.pop("object", None)
self.method = kwargs.pop("method", None)
super(TransitionNotAllowed, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)


class InvalidResultState(Exception):
Expand All @@ -97,7 +70,7 @@ class ConcurrentTransition(Exception):
"""


class Transition(object):
class Transition:
def __init__(self, method, source, target, on_error, conditions, permission, custom):
self.method = method
self.source = source
Expand Down Expand Up @@ -155,7 +128,7 @@ def get_available_user_FIELD_transitions(instance, user, field):
yield transition


class FSMMeta(object):
class FSMMeta:
"""
Models methods transitions meta information
"""
Expand All @@ -174,7 +147,7 @@ def get_transition(self, source):

def add_transition(self, method, source, target, on_error=None, conditions=[], permission=None, custom={}):
if source in self.transitions:
raise AssertionError("Duplicate transition for {0} state".format(source))
raise AssertionError(f"Duplicate transition for {source} state")

self.transitions[source] = Transition(
method=method,
Expand Down Expand Up @@ -226,20 +199,20 @@ def next_state(self, current_state):
transition = self.get_transition(current_state)

if transition is None:
raise TransitionNotAllowed("No transition from {0}".format(current_state))
raise TransitionNotAllowed(f"No transition from {current_state}")

return transition.target

def exception_state(self, current_state):
transition = self.get_transition(current_state)

if transition is None:
raise TransitionNotAllowed("No transition from {0}".format(current_state))
raise TransitionNotAllowed(f"No transition from {current_state}")

return transition.on_error


class FSMFieldDescriptor(object):
class FSMFieldDescriptor:
def __init__(self, field):
self.field = field

Expand All @@ -250,14 +223,14 @@ def __get__(self, instance, type=None):

def __set__(self, instance, value):
if self.field.protected and self.field.name in instance.__dict__:
raise AttributeError("Direct {0} modification is not allowed".format(self.field.name))
raise AttributeError(f"Direct {self.field.name} modification is not allowed")

# Update state
self.field.set_proxy(instance, value)
self.field.set_state(instance, value)


class FSMFieldMixin(object):
class FSMFieldMixin:
descriptor_class = FSMFieldDescriptor

def __init__(self, *args, **kwargs):
Expand All @@ -277,10 +250,10 @@ def __init__(self, *args, **kwargs):
self.state_proxy[state] = proxy_cls_ref
kwargs["choices"] = choices

super(FSMFieldMixin, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def deconstruct(self):
name, path, args, kwargs = super(FSMFieldMixin, self).deconstruct()
name, path, args, kwargs = super().deconstruct()
if self.protected:
kwargs["protected"] = self.protected
return name, path, args, kwargs
Expand Down Expand Up @@ -326,7 +299,7 @@ def set_proxy(self, instance, state):

model = get_model(app_label, model_name)
if model is None:
raise ValueError("No model found {0}".format(state_proxy))
raise ValueError(f"No model found {state_proxy}")

instance.__class__ = model

Expand All @@ -337,13 +310,13 @@ def change_state(self, instance, method, *args, **kwargs):

if not meta.has_transition(current_state):
raise TransitionNotAllowed(
"Can't switch from state '{0}' using method '{1}'".format(current_state, method_name),
f"Can't switch from state '{current_state}' using method '{method_name}'",
object=instance,
method=method,
)
if not meta.conditions_met(instance, current_state):
raise TransitionNotAllowed(
"Transition conditions have not been met for method '{0}'".format(method_name), object=instance, method=method
f"Transition conditions have not been met for method '{method_name}'", object=instance, method=method
)

next_state = meta.next_state(current_state)
Expand Down Expand Up @@ -398,15 +371,15 @@ def get_all_transitions(self, instance_cls):
def contribute_to_class(self, cls, name, **kwargs):
self.base_cls = cls

super(FSMFieldMixin, self).contribute_to_class(cls, name, **kwargs)
super().contribute_to_class(cls, name, **kwargs)
setattr(cls, self.name, self.descriptor_class(self))
setattr(cls, "get_all_{0}_transitions".format(self.name), partialmethod(get_all_FIELD_transitions, field=self))
setattr(cls, f"get_all_{self.name}_transitions", partialmethod(get_all_FIELD_transitions, field=self))
setattr(
cls, "get_available_{0}_transitions".format(self.name), partialmethod(get_available_FIELD_transitions, field=self)
cls, f"get_available_{self.name}_transitions", partialmethod(get_available_FIELD_transitions, field=self)
)
setattr(
cls,
"get_available_user_{0}_transitions".format(self.name),
f"get_available_user_{self.name}_transitions",
partialmethod(get_available_user_FIELD_transitions, field=self),
)

Expand Down Expand Up @@ -448,7 +421,7 @@ class FSMField(FSMFieldMixin, models.CharField):

def __init__(self, *args, **kwargs):
kwargs.setdefault("max_length", 50)
super(FSMField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)


class FSMIntegerField(FSMFieldMixin, models.IntegerField):
Expand All @@ -471,7 +444,7 @@ def set_state(self, instance, state):
instance.__dict__[self.attname] = self.to_python(state)


class ConcurrentTransitionMixin(object):
class ConcurrentTransitionMixin:
"""
Protects a Model from undesirable effects caused by concurrently executed transitions,
e.g. running the same transition multiple times at the same time, or running different
Expand All @@ -498,7 +471,7 @@ class ConcurrentTransitionMixin(object):
"""

def __init__(self, *args, **kwargs):
super(ConcurrentTransitionMixin, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self._update_initial_state()

@property
Expand All @@ -513,9 +486,9 @@ def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_updat
filter_on = filter(lambda field: field.model == base_qs.model, self.state_fields)

# state filter will be used to narrow down the standard filter checking only PK
state_filter = dict((field.attname, self.__initial_states[field.attname]) for field in filter_on)
state_filter = {field.attname: self.__initial_states[field.attname] for field in filter_on}

updated = super(ConcurrentTransitionMixin, self)._do_update(
updated = super()._do_update(
base_qs=base_qs.filter(**state_filter),
using=using,
pk_val=pk_val,
Expand All @@ -536,14 +509,14 @@ def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_updat
return updated

def _update_initial_state(self):
self.__initial_states = dict((field.attname, field.value_from_object(self)) for field in self.state_fields)
self.__initial_states = {field.attname: field.value_from_object(self) for field in self.state_fields}

def refresh_from_db(self, *args, **kwargs):
super(ConcurrentTransitionMixin, self).refresh_from_db(*args, **kwargs)
super().refresh_from_db(*args, **kwargs)
self._update_initial_state()

def save(self, *args, **kwargs):
super(ConcurrentTransitionMixin, self).save(*args, **kwargs)
super().save(*args, **kwargs)
self._update_initial_state()


Expand Down Expand Up @@ -588,36 +561,34 @@ def can_proceed(bound_method, check_conditions=True):
conditions.
"""
if not hasattr(bound_method, "_django_fsm"):
im_func = getattr(bound_method, "im_func", getattr(bound_method, "__func__"))
raise TypeError("%s method is not transition" % im_func.__name__)
raise TypeError("%s method is not transition" % bound_method.__func__.__name__)

meta = bound_method._django_fsm
im_self = getattr(bound_method, "im_self", getattr(bound_method, "__self__"))
current_state = meta.field.get_state(im_self)
self = bound_method.__self__
current_state = meta.field.get_state(self)

return meta.has_transition(current_state) and (not check_conditions or meta.conditions_met(im_self, current_state))
return meta.has_transition(current_state) and (not check_conditions or meta.conditions_met(self, current_state))


def has_transition_perm(bound_method, user):
"""
Returns True if model in state allows to call bound_method and user have rights on it
"""
if not hasattr(bound_method, "_django_fsm"):
im_func = getattr(bound_method, "im_func", getattr(bound_method, "__func__"))
raise TypeError("%s method is not transition" % im_func.__name__)
raise TypeError("%s method is not transition" % bound_method.__func__.__name__)

meta = bound_method._django_fsm
im_self = getattr(bound_method, "im_self", getattr(bound_method, "__self__"))
current_state = meta.field.get_state(im_self)
self = bound_method.__self__
current_state = meta.field.get_state(self)

return (
meta.has_transition(current_state)
and meta.conditions_met(im_self, current_state)
and meta.has_transition_perm(im_self, current_state, user)
and meta.conditions_met(self, current_state)
and meta.has_transition_perm(self, current_state, user)
)


class State(object):
class State:
def get_state(self, model, transition, result, args=[], kwargs={}):
raise NotImplementedError

Expand All @@ -629,7 +600,7 @@ def __init__(self, *allowed_states):
def get_state(self, model, transition, result, args=[], kwargs={}):
if self.allowed_states is not None:
if result not in self.allowed_states:
raise InvalidResultState("{} is not in list of allowed states\n{}".format(result, self.allowed_states))
raise InvalidResultState(f"{result} is not in list of allowed states\n{self.allowed_states}")
return result


Expand All @@ -642,5 +613,5 @@ def get_state(self, model, transition, result, args=[], kwargs={}):
result_state = self.func(model, *args, **kwargs)
if self.allowed_states is not None:
if result_state not in self.allowed_states:
raise InvalidResultState("{} is not in list of allowed states\n{}".format(result, self.allowed_states))
raise InvalidResultState(f"{result} is not in list of allowed states\n{self.allowed_states}")
return result_state
11 changes: 5 additions & 6 deletions django_fsm/management/commands/graph_transitions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8; mode: django -*-
import graphviz
from optparse import make_option
from itertools import chain
Expand Down Expand Up @@ -37,7 +36,7 @@ def all_fsm_fields_data(model):

def node_name(field, state):
opts = field.model._meta
return "%s.%s.%s.%s" % (opts.app_label, opts.verbose_name.replace(" ", "_"), field.name, state)
return "{}.{}.{}.{}".format(opts.app_label, opts.verbose_name.replace(" ", "_"), field.name, state)


def node_label(field, state):
Expand Down Expand Up @@ -79,7 +78,7 @@ def generate_dot(fields_data):
add_transition(source, target, transition.name, source_name, field, sources, targets, edges)

targets.update(
set((node_name(field, target), node_label(field, target)) for target, _ in chain(any_targets, any_except_targets))
{(node_name(field, target), node_label(field, target)) for target, _ in chain(any_targets, any_except_targets)}
)
for target, name in any_targets:
target_name = node_name(field, target)
Expand All @@ -91,16 +90,16 @@ def generate_dot(fields_data):
for target, name in any_except_targets:
target_name = node_name(field, target)
all_nodes = sources | targets
all_nodes.remove(((target_name, node_label(field, target))))
all_nodes.remove((target_name, node_label(field, target)))
for source_name, label in all_nodes:
sources.add((source_name, label))
edges.add((source_name, target_name, (("label", name),)))

# construct subgraph
opts = field.model._meta
subgraph = graphviz.Digraph(
name="cluster_%s_%s_%s" % (opts.app_label, opts.object_name, field.name),
graph_attr={"label": "%s.%s.%s" % (opts.app_label, opts.object_name, field.name)},
name=f"cluster_{opts.app_label}_{opts.object_name}_{field.name}",
graph_attr={"label": f"{opts.app_label}.{opts.object_name}.{field.name}"},
)

final_states = targets - sources
Expand Down
1 change: 0 additions & 1 deletion django_fsm/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Empty file to mark package as valid django application.
"""
1 change: 0 additions & 1 deletion django_fsm/signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from django.dispatch import Signal

pre_transition = Signal()
Expand Down
2 changes: 1 addition & 1 deletion django_fsm/tests/test_abstract_inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,5 @@ def test_field_available_transitions_works(self):
def test_field_all_transitions_works(self):
transitions = self.model.get_all_state_transitions()
self.assertEqual(
set([("new", "published"), ("published", "sticked")]), set((data.source, data.target) for data in transitions)
{("new", "published"), ("published", "sticked")}, {(data.source, data.target) for data in transitions}
)
Loading

0 comments on commit 5e24a14

Please sign in to comment.