Coverage for src/backend/InvenTree/InvenTree/helpers_mixin.py: 67%
57 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Provides helper mixins that are used throughout the InvenTree project."""
3import inspect
4from collections.abc import Callable
5from pathlib import Path
6from typing import Any
8from django.conf import settings
9from django.core.cache import cache
11from plugin import registry as plg_registry
14class ClassValidationMixin:
15 """Mixin to validate class attributes and overrides.
17 Class attributes:
18 required_attributes: List of class attributes that need to be defined
19 required_overrides: List of functions that need override, a nested list mean either one of them needs an override
21 Example:
22 ```py
23 class Parent(ClassValidationMixin):
24 NAME: str
25 def test(self):
26 pass
28 required_attributes = ["NAME"]
29 required_overrides = [test]
31 class MyClass(Parent):
32 pass
34 myClass = MyClass()
35 myClass.validate() # raises NotImplementedError
36 ```
37 """
39 required_attributes = []
40 required_overrides = []
42 @classmethod
43 def validate(cls):
44 """Validate the class against the required attributes/overrides."""
46 def attribute_missing(key):
47 """Check if attribute is missing."""
48 return not hasattr(cls, key) or getattr(cls, key) == ''
50 def override_missing(base_implementation):
51 """Check if override is missing."""
52 if isinstance(base_implementation, list):
53 return all(override_missing(x) for x in base_implementation)
55 return base_implementation == getattr(
56 cls, base_implementation.__name__, None
57 )
59 missing_attributes = list(filter(attribute_missing, cls.required_attributes))
60 missing_overrides = list(filter(override_missing, cls.required_overrides))
62 errors = []
64 if len(missing_attributes) > 0: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true
65 errors.append(
66 f'did not provide the following attributes: {", ".join(missing_attributes)}'
67 )
68 if len(missing_overrides) > 0: 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true
69 missing_overrides_list = []
70 for base_implementation in missing_overrides:
71 if isinstance(base_implementation, list):
72 missing_overrides_list.append(
73 'one of '
74 + ' or '.join(attr.__name__ for attr in base_implementation)
75 )
76 else:
77 missing_overrides_list.append(base_implementation.__name__)
78 errors.append(
79 f'did not override the required attributes: {", ".join(missing_overrides_list)}'
80 )
82 if len(errors) > 0: 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 raise NotImplementedError(f"'{cls}' " + ' and '.join(errors))
86class ClassProviderMixin:
87 """Mixin to get metadata about a class itself, e.g. the plugin that provided that class."""
89 @classmethod
90 def get_provider_file(cls):
91 """File that contains the Class definition."""
92 return inspect.getfile(cls)
94 @classmethod
95 def get_provider_plugin(cls):
96 """Plugin that contains the Class definition, otherwise None."""
97 for plg in plg_registry.plugins.values():
98 if plg.package_path == cls.__module__: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true
99 return plg
101 @classmethod
102 def get_is_builtin(cls):
103 """Is this Class build in the InvenTree source code?"""
104 try:
105 Path(cls.get_provider_file()).relative_to(settings.BASE_DIR)
106 return True
107 except ValueError:
108 # Path(...).relative_to throws an ValueError if its not relative to the InvenTree source base dir
109 return False
112def get_shared_class_instance_state_mixin(get_state_key: Callable[[type], str]):
113 """Get a mixin class that provides shared state for classes across the main application and worker.
115 Arguments:
116 get_state_key: A function that returns the key for the shared state when given a class instance.
117 """
119 class SharedClassStateMixinClass:
120 """Mixin to provide shared state for classes across the main application and worker."""
122 def set_shared_state(self, key: str, value: Any):
123 """Set a shared state value for this machine.
125 Arguments:
126 key: The key for the shared state
127 value: The value to set
128 """
129 cache.set(self._get_key(key), value, timeout=None)
131 def get_shared_state(self, key: str, default=None):
132 """Get a shared state value for this machine.
134 Arguments:
135 key: The key for the shared state
136 default: The default value to return if the key does not exist
137 """
138 return cache.get(self._get_key(key)) or default
140 def _get_key(self, key: str):
141 """Get the key for this class instance."""
142 return f'{get_state_key(self)}:{key}'
144 return SharedClassStateMixinClass