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

1"""Provides helper mixins that are used throughout the InvenTree project.""" 

2 

3import inspect 

4from collections.abc import Callable 

5from pathlib import Path 

6from typing import Any 

7 

8from django.conf import settings 

9from django.core.cache import cache 

10 

11from plugin import registry as plg_registry 

12 

13 

14class ClassValidationMixin: 

15 """Mixin to validate class attributes and overrides. 

16 

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 

20 

21 Example: 

22 ```py 

23 class Parent(ClassValidationMixin): 

24 NAME: str 

25 def test(self): 

26 pass 

27 

28 required_attributes = ["NAME"] 

29 required_overrides = [test] 

30 

31 class MyClass(Parent): 

32 pass 

33 

34 myClass = MyClass() 

35 myClass.validate() # raises NotImplementedError 

36 ``` 

37 """ 

38 

39 required_attributes = [] 

40 required_overrides = [] 

41 

42 @classmethod 

43 def validate(cls): 

44 """Validate the class against the required attributes/overrides.""" 

45 

46 def attribute_missing(key): 

47 """Check if attribute is missing.""" 

48 return not hasattr(cls, key) or getattr(cls, key) == '' 

49 

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) 

54 

55 return base_implementation == getattr( 

56 cls, base_implementation.__name__, None 

57 ) 

58 

59 missing_attributes = list(filter(attribute_missing, cls.required_attributes)) 

60 missing_overrides = list(filter(override_missing, cls.required_overrides)) 

61 

62 errors = [] 

63 

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 ) 

81 

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)) 

84 

85 

86class ClassProviderMixin: 

87 """Mixin to get metadata about a class itself, e.g. the plugin that provided that class.""" 

88 

89 @classmethod 

90 def get_provider_file(cls): 

91 """File that contains the Class definition.""" 

92 return inspect.getfile(cls) 

93 

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 

100 

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 

110 

111 

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. 

114 

115 Arguments: 

116 get_state_key: A function that returns the key for the shared state when given a class instance. 

117 """ 

118 

119 class SharedClassStateMixinClass: 

120 """Mixin to provide shared state for classes across the main application and worker.""" 

121 

122 def set_shared_state(self, key: str, value: Any): 

123 """Set a shared state value for this machine. 

124 

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) 

130 

131 def get_shared_state(self, key: str, default=None): 

132 """Get a shared state value for this machine. 

133 

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 

139 

140 def _get_key(self, key: str): 

141 """Get the key for this class instance.""" 

142 return f'{get_state_key(self)}:{key}' 

143 

144 return SharedClassStateMixinClass