From 34ea373d88c06a798d7a042c670fa93ba83768c3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 13 Oct 2026 20:45:44 +1100 Subject: [PATCH] Improve status code comparison (#13006) Fixes bug caused by override of BaseEnum.__new__ which broke comparison of status code values --- .../InvenTree/generic/states/states.py | 34 ++++++++++++++---- src/backend/InvenTree/generic/states/tests.py | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/backend/InvenTree/generic/states/states.py b/src/backend/InvenTree/generic/states/states.py index 72b531da9e..4b940f24ba 100644 --- a/src/backend/InvenTree/generic/states/states.py +++ b/src/backend/InvenTree/generic/states/states.py @@ -9,6 +9,18 @@ from typing import Optional logger = logging.getLogger('inventree') +def _new_int(cls, value): + """Construct a new int-based enum member with the provided value as its int payload. + + Non-integer values (which are not valid status codes, and are ignored by + StatusCode._is_element) fall back to a zero payload. + """ + if isinstance(value, int): + return int.__new__(cls, value) + + return int.__new__(cls) + + class BaseEnum(enum.IntEnum): """An `Enum` capable of having its members have docstrings. @@ -16,8 +28,13 @@ class BaseEnum(enum.IntEnum): """ def __new__(cls, *args): - """Assign values on creation.""" - obj = object.__new__(cls) + """Assign values on creation. + + Note: The underlying int payload must be set (via int.__new__) to match the value, + otherwise any operation which uses the raw int (e.g. '!=', '<', bool(), json.dumps) + would see 0 for every member. + """ + obj = _new_int(cls, args[0]) obj._value_ = args[0] return obj @@ -51,10 +68,13 @@ class BaseEnum(enum.IntEnum): return hash(self.value) def __ne__(self, obj): - """Override inequality operator to allow comparison with int.""" - if type(self) is type(obj): - return super().__ne__(obj) - return self.value != obj + """Override inequality operator to remain consistent with __eq__.""" + result = self.__eq__(obj) + + if result is NotImplemented: + return result + + return not result class StatusCode(BaseEnum): @@ -72,7 +92,7 @@ class StatusCode(BaseEnum): def __new__(cls, *args): """Define object out of args.""" - obj = int.__new__(cls) + obj = _new_int(cls, args[0]) obj._value_ = args[0] # Normal item definition diff --git a/src/backend/InvenTree/generic/states/tests.py b/src/backend/InvenTree/generic/states/tests.py index 1bb2d2585c..729f4b9bed 100644 --- a/src/backend/InvenTree/generic/states/tests.py +++ b/src/backend/InvenTree/generic/states/tests.py @@ -123,6 +123,41 @@ class GeneralStateTest(InvenTreeTestCase): # label self.assertEqual(GeneralStatus.label(10), 'Pending') + def test_int_payload(self): + """The underlying int payload of each member matches its value. + + Regression test: members were previously constructed with a zero int payload, + so any operation using the raw int ('!=' between members of the same class, + ordering, bool(), arithmetic, json serialization) was silently wrong. + """ + import json + + from build.status_codes import BuildStatus + + for cls in [GeneralStatus, BuildStatus]: + for member in cls.values(): + self.assertEqual(int.__repr__(member), str(member.value)) + + pending, complete = GeneralStatus.PENDING, GeneralStatus.COMPLETE + + # Comparison between members of the same class + self.assertNotEqual(pending, complete) + self.assertEqual(pending, GeneralStatus.PENDING) + self.assertNotEqual(BuildStatus.PENDING, BuildStatus.COMPLETE) + self.assertLess(pending, complete) + self.assertEqual(sorted([complete, pending]), [pending, complete]) + + # Comparison against raw integers + self.assertEqual(pending, 10) + self.assertNotEqual(pending, 20) + self.assertEqual(10, pending) + self.assertNotEqual(20, pending) + + self.assertTrue(bool(pending)) + self.assertEqual(pending + 1, 11) + self.assertEqual(f'{complete:d}', '30') + self.assertEqual(json.dumps({'status': complete}), '{"status": 30}') + def test_color(self): """Test that the color enum validation works.""" with self.assertRaises(ValueError) as e: