From 6aea58bf0a44756dc9a0d0f834c314388a2df589 Mon Sep 17 00:00:00 2001 From: Pascal F Date: Sat, 17 Jun 2023 06:58:36 +0200 Subject: [PATCH] Setup pre-commit --- .checkignore | 2 +- .pre-commit-config.yaml | 38 ++++++++++++ .pylintrc | 3 +- LICENSE | 4 +- README.rst | 4 +- django_fsm/__init__.py | 59 +++++++++---------- .../management/commands/graph_transitions.py | 15 +++-- django_fsm/models.py | 1 - django_fsm/signals.py | 1 - django_fsm/tests/test_abstract_inheritance.py | 2 +- django_fsm/tests/test_basic_transitions.py | 36 +++++------ django_fsm/tests/test_integer_field.py | 2 +- django_fsm/tests/test_proxy_inheritance.py | 4 +- setup.py | 2 +- tests/manage.py | 1 - tests/settings.py | 21 ++----- tests/testapp/tests/test_mixin_support.py | 2 +- .../testapp/tests/test_object_permissions.py | 2 +- tests/testapp/tests/test_permissions.py | 4 +- tests/testapp/tests/test_state_transitions.py | 2 +- 20 files changed, 112 insertions(+), 93 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.checkignore b/.checkignore index e73d656..e93d446 100644 --- a/.checkignore +++ b/.checkignore @@ -1,2 +1,2 @@ tests/* -django_fsm/tests/* \ No newline at end of file +django_fsm/tests/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0812619 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +default_language_version: + python: python3.8 + python_venv: python3.8 # Optional + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + args: ["--maxkb=700"] + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - 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: + - id: django-upgrade + args: [--target-version, "3.2"] + + # - repo: https://github.com/astral-sh/ruff-pre-commit + # rev: v0.0.291 + # hooks: + # - id: ruff-format + # - id: ruff diff --git a/.pylintrc b/.pylintrc index 6d0c5f9..2af6e9e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,7 +2,7 @@ persistent=yes [MESSAGES CONTROL] -# C0111 = Missing docstring +# C0111 = Missing docstring # I0011 = # Warning locally suppressed using disable-msg # I0012 = # Warning locally suppressed using disable-msg disable=I0011,I0012 @@ -60,4 +60,3 @@ max-parents=7 max-attributes=7 min-public-methods=0 max-public-methods=20 - diff --git a/LICENSE b/LICENSE index edca474..2e512e4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -copyright (c) 2010 Mikhail Podgurskiy +The MIT License (MIT) + +Copyright (c) 2010 Mikhail Podgurskiy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index d2f107b..f5b80fb 100644 --- a/README.rst +++ b/README.rst @@ -153,7 +153,7 @@ on a model instance with a protected FSMField will cause an exception. ``source`` parameter accepts a list of states, or an individual state or ``django_fsm.State`` implementation. -You can use ``*`` for ``source`` to allow switching to ``target`` from any state. +You can use ``*`` for ``source`` to allow switching to ``target`` from any state. You can use ``+`` for ``source`` to allow switching to ``target`` from any state excluding ``target`` state. @@ -163,7 +163,7 @@ You can use ``+`` for ``source`` to allow switching to ``target`` from any state ``target`` state parameter could point to a specific state or ``django_fsm.State`` implementation .. code:: python - + from django_fsm import FSMField, transition, RETURN_VALUE, GET_STATE @transition(field=state, source='*', diff --git a/django_fsm/__init__.py b/django_fsm/__init__.py index 340060f..f60b2ca 100644 --- a/django_fsm/__init__.py +++ b/django_fsm/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ State tracking functionality for django models """ @@ -82,7 +81,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): @@ -97,7 +96,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 @@ -155,7 +154,7 @@ def get_available_user_FIELD_transitions(instance, user, field): yield transition -class FSMMeta(object): +class FSMMeta: """ Models methods transitions meta information """ @@ -174,7 +173,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, @@ -226,7 +225,7 @@ 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 @@ -234,12 +233,12 @@ 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 @@ -250,14 +249,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): @@ -277,10 +276,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 @@ -326,7 +325,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 @@ -337,13 +336,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) @@ -398,15 +397,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), ) @@ -448,7 +447,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): @@ -471,7 +470,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 @@ -498,7 +497,7 @@ class ConcurrentTransitionMixin(object): """ def __init__(self, *args, **kwargs): - super(ConcurrentTransitionMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._update_initial_state() @property @@ -513,9 +512,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, @@ -536,14 +535,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() @@ -617,7 +616,7 @@ def has_transition_perm(bound_method, user): ) -class State(object): +class State: def get_state(self, model, transition, result, args=[], kwargs={}): raise NotImplementedError @@ -629,7 +628,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 @@ -642,5 +641,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 diff --git a/django_fsm/management/commands/graph_transitions.py b/django_fsm/management/commands/graph_transitions.py index e21c98a..c66b14a 100644 --- a/django_fsm/management/commands/graph_transitions.py +++ b/django_fsm/management/commands/graph_transitions.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8; mode: django -*- import graphviz from optparse import make_option from itertools import chain from django.core.management.base import BaseCommand try: - from django.utils.encoding import force_text + 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 @@ -37,12 +36,12 @@ 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): if type(state) == int or (type(state) == bool and hasattr(field, "choices")): - return force_text(dict(field.choices).get(state)) + return force_str(dict(field.choices).get(state)) else: return state @@ -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) @@ -91,7 +90,7 @@ 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),))) @@ -99,8 +98,8 @@ def generate_dot(fields_data): # 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 diff --git a/django_fsm/models.py b/django_fsm/models.py index 11afcc6..41eb870 100644 --- a/django_fsm/models.py +++ b/django_fsm/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Empty file to mark package as valid django application. """ diff --git a/django_fsm/signals.py b/django_fsm/signals.py index a6c8856..07fb641 100644 --- a/django_fsm/signals.py +++ b/django_fsm/signals.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.dispatch import Signal pre_transition = Signal() diff --git a/django_fsm/tests/test_abstract_inheritance.py b/django_fsm/tests/test_abstract_inheritance.py index c81fa3b..12643e7 100644 --- a/django_fsm/tests/test_abstract_inheritance.py +++ b/django_fsm/tests/test_abstract_inheritance.py @@ -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} ) diff --git a/django_fsm/tests/test_basic_transitions.py b/django_fsm/tests/test_basic_transitions.py index 84a4063..763ea9e 100644 --- a/django_fsm/tests/test_basic_transitions.py +++ b/django_fsm/tests/test_basic_transitions.py @@ -145,62 +145,59 @@ def setUp(self): def test_available_conditions_from_new(self): transitions = self.model.get_available_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set([("*", "moderated"), ("new", "published"), ("new", "removed"), ("*", ""), ("+", "blocked")]) + actual = {(transition.source, transition.target) for transition in transitions} + expected = {("*", "moderated"), ("new", "published"), ("new", "removed"), ("*", ""), ("+", "blocked")} self.assertEqual(actual, expected) def test_available_conditions_from_published(self): self.model.publish() transitions = self.model.get_available_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set( - [ + actual = {(transition.source, transition.target) for transition in transitions} + expected = { ("*", "moderated"), ("published", None), ("published", "hidden"), ("published", "stolen"), ("*", ""), ("+", "blocked"), - ] - ) + } self.assertEqual(actual, expected) def test_available_conditions_from_hidden(self): self.model.publish() self.model.hide() transitions = self.model.get_available_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set([("*", "moderated"), ("hidden", "stolen"), ("*", ""), ("+", "blocked")]) + actual = {(transition.source, transition.target) for transition in transitions} + expected = {("*", "moderated"), ("hidden", "stolen"), ("*", ""), ("+", "blocked")} self.assertEqual(actual, expected) def test_available_conditions_from_stolen(self): self.model.publish() self.model.steal() transitions = self.model.get_available_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set([("*", "moderated"), ("*", ""), ("+", "blocked")]) + actual = {(transition.source, transition.target) for transition in transitions} + expected = {("*", "moderated"), ("*", ""), ("+", "blocked")} self.assertEqual(actual, expected) def test_available_conditions_from_blocked(self): self.model.block() transitions = self.model.get_available_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set([("*", "moderated"), ("*", "")]) + actual = {(transition.source, transition.target) for transition in transitions} + expected = {("*", "moderated"), ("*", "")} self.assertEqual(actual, expected) def test_available_conditions_from_empty(self): self.model.empty() transitions = self.model.get_available_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set([("*", "moderated"), ("*", ""), ("+", "blocked")]) + actual = {(transition.source, transition.target) for transition in transitions} + expected = {("*", "moderated"), ("*", ""), ("+", "blocked")} self.assertEqual(actual, expected) def test_all_conditions(self): transitions = self.model.get_all_state_transitions() - actual = set((transition.source, transition.target) for transition in transitions) - expected = set( - [ + actual = {(transition.source, transition.target) for transition in transitions} + expected = { ("*", "moderated"), ("new", "published"), ("new", "removed"), @@ -210,6 +207,5 @@ def test_all_conditions(self): ("hidden", "stolen"), ("*", ""), ("+", "blocked"), - ] - ) + } self.assertEqual(actual, expected) diff --git a/django_fsm/tests/test_integer_field.py b/django_fsm/tests/test_integer_field.py index f2a30d4..438d545 100644 --- a/django_fsm/tests/test_integer_field.py +++ b/django_fsm/tests/test_integer_field.py @@ -3,7 +3,7 @@ from django_fsm import FSMIntegerField, TransitionNotAllowed, transition -class BlogPostStateEnum(object): +class BlogPostStateEnum: NEW = 10 PUBLISHED = 20 HIDDEN = 30 diff --git a/django_fsm/tests/test_proxy_inheritance.py b/django_fsm/tests/test_proxy_inheritance.py index f273f6a..1910f92 100644 --- a/django_fsm/tests/test_proxy_inheritance.py +++ b/django_fsm/tests/test_proxy_inheritance.py @@ -42,10 +42,10 @@ def test_field_available_transitions_works(self): def test_field_all_transitions_base_model(self): transitions = BaseModel().get_all_state_transitions() - self.assertEqual(set([("new", "published")]), set((data.source, data.target) for data in transitions)) + self.assertEqual({("new", "published")}, {(data.source, data.target) for data in transitions}) 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} ) diff --git a/setup.py b/setup.py index 39f778d..2fd7f0d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ try: long_description = open('README.rst').read() -except IOError: +except OSError: long_description = '' setup( diff --git a/tests/manage.py b/tests/manage.py index 2db24e4..034110c 100644 --- a/tests/manage.py +++ b/tests/manage.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import sys from django.core.management import execute_from_command_line diff --git a/tests/settings.py b/tests/settings.py index df5ef4b..d39399b 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,22 +25,11 @@ } } -if django.VERSION < (1, 9): - - class DisableMigrations(object): - def __contains__(self, item): - return True - - def __getitem__(self, item): - return "notmigrations" - - MIGRATION_MODULES = DisableMigrations() -else: - MIGRATION_MODULES = { - "auth": None, - "contenttypes": None, - "guardian": None, - } +MIGRATION_MODULES = { + "auth": None, + "contenttypes": None, + "guardian": None, +} ANONYMOUS_USER_ID = 0 diff --git a/tests/testapp/tests/test_mixin_support.py b/tests/testapp/tests/test_mixin_support.py index 691868e..f9a3385 100644 --- a/tests/testapp/tests/test_mixin_support.py +++ b/tests/testapp/tests/test_mixin_support.py @@ -3,7 +3,7 @@ from django_fsm import FSMField, transition -class WorkflowMixin(object): +class WorkflowMixin: @transition(field="state", source="*", target="draft") def draft(self): pass diff --git a/tests/testapp/tests/test_object_permissions.py b/tests/testapp/tests/test_object_permissions.py index a6ef291..58129c2 100644 --- a/tests/testapp/tests/test_object_permissions.py +++ b/tests/testapp/tests/test_object_permissions.py @@ -34,7 +34,7 @@ class Meta: ) class ObjectPermissionFSMFieldTest(TestCase): def setUp(self): - super(ObjectPermissionFSMFieldTest, self).setUp() + super().setUp() self.model = ObjectPermissionTestModel.objects.create() self.unprivileged = User.objects.create(username="unpriviledged") diff --git a/tests/testapp/tests/test_permissions.py b/tests/testapp/tests/test_permissions.py index 8a7e185..e373507 100644 --- a/tests/testapp/tests/test_permissions.py +++ b/tests/testapp/tests/test_permissions.py @@ -20,14 +20,14 @@ def test_proviledged_access_succed(self): self.assertTrue(has_transition_perm(self.model.remove, self.priviledged)) transitions = self.model.get_available_user_state_transitions(self.priviledged) - self.assertEqual(set(["publish", "remove", "moderate"]), set(transition.name for transition in transitions)) + self.assertEqual({"publish", "remove", "moderate"}, {transition.name for transition in transitions}) def test_unpriviledged_access_prohibited(self): self.assertFalse(has_transition_perm(self.model.publish, self.unpriviledged)) self.assertFalse(has_transition_perm(self.model.remove, self.unpriviledged)) transitions = self.model.get_available_user_state_transitions(self.unpriviledged) - self.assertEqual(set(["moderate"]), set(transition.name for transition in transitions)) + self.assertEqual({"moderate"}, {transition.name for transition in transitions}) def test_permission_instance_method(self): self.assertFalse(has_transition_perm(self.model.restore, self.unpriviledged)) diff --git a/tests/testapp/tests/test_state_transitions.py b/tests/testapp/tests/test_state_transitions.py index 6f3916f..6197cd4 100644 --- a/tests/testapp/tests/test_state_transitions.py +++ b/tests/testapp/tests/test_state_transitions.py @@ -63,4 +63,4 @@ def test_load_proxy_set(self): Insect.objects.create(state=Insect.STATE.BUTTERFLY) insects = Insect.objects.all() - self.assertEqual(set([Caterpillar, Butterfly]), set(insect.__class__ for insect in insects)) + self.assertEqual({Caterpillar, Butterfly}, {insect.__class__ for insect in insects})