Coverage for src / mafw / active.py: 92%

39 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-09 09:08 +0000

1# Copyright 2025 European Union 

2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu) 

3# SPDX-License-Identifier: EUPL-1.2 

4""" 

5Module implements active variable for classes. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import Generic, Type, TypeVar, cast 

11 

12ActiveType = TypeVar('ActiveType') 

13"""A type for templating the Active class.""" 

14 

15ActivableType = TypeVar('ActivableType') 

16"""A type for all classes that can include an Active.""" 

17 

18 

19class Active(Generic[ActiveType]): 

20 """ 

21 A descriptor class to make class variable **active**. 

22 

23 When assigned to a class variable, any change of this value will trigger a call back to a specific function. 

24 

25 Here is a clarifying example. 

26 

27 .. code-block:: python 

28 

29 class Person: 

30 age = Active() 

31 

32 def __init__(self, age): 

33 self.age = age 

34 

35 def on_age_change(self, old_value, new_value): 

36 # callback invoked every time the value of age is changed 

37 # do something with the old and the new age 

38 pass 

39 

40 def on_age_set(self, value): 

41 # callback invoked every time the value on age is set to the same 

42 # value as before. 

43 pass 

44 

45 def on_age_get(self, value): 

46 # callback invoked every time the value of age is asked. 

47 # not really useful, but... 

48 pass 

49 

50 

51 Once you have assigned an Active to a class member, you need to implement the callback in your class. 

52 If you do not implement them, the code will run without problems. 

53 

54 The three callbacks have the signature described in the example. 

55 

56 * on_[var_name]_change(self, old, new) 

57 * on_[var_name]_set(self, value) 

58 * on_[var_name]_get(self, value) 

59 """ 

60 

61 def __init__(self, default: ActiveType | None = None) -> None: 

62 """ 

63 Constructor parameter: 

64 

65 :param default: Initial value of the Active value. Defaults to None. 

66 :type default: ActiveType 

67 """ 

68 self.default = default 

69 

70 # the name of the three callbacks. 

71 # they will be assigned after we know the name of the variable. 

72 self._change_call_back_name: str 

73 self._set_callback_name: str 

74 self._get_callback_name: str 

75 

76 def __set_name__(self, obj: Type[ActivableType], name: str) -> None: 

77 # this is the public name of the class variable 

78 self.public_name = name 

79 # this is the name where we will be storing the value in the owner class 

80 self.private_name = 'active_' + name 

81 

82 # check if the owner does not have an attribute named after private name 

83 # if so create it and assign it the default value 

84 if not hasattr(obj, self.private_name): 84 ↛ 88line 84 didn't jump to line 88 because the condition on line 84 was always true

85 setattr(obj, self.private_name, self.default) 

86 

87 # now you can prepare the name for the callbacks 

88 self._init_callbacks() 

89 

90 def _init_callbacks(self) -> None: 

91 # now we know the name of the variable, so we can create the callback names as well 

92 self._change_callback_name = f'on_{self.public_name}_change' 

93 self._set_callback_name = f'on_{self.public_name}_set' 

94 self._get_callback_name = f'on_{self.public_name}_get' 

95 

96 def __get__(self, obj: ActivableType, obj_type: Type[ActivableType]) -> Active[ActiveType] | ActiveType: 

97 if obj is None: 

98 # obj is None means that we are invoking the descriptor via the class and not the instance 

99 # in this case we cannot return the private value and we return the descriptor itself. 

100 return self 

101 

102 # if the object does not have an attribute named with our private_name, it means this is the first use. 

103 # we need to initialize the descriptor 

104 if not hasattr(obj, self.private_name): 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true

105 setattr(obj, self.private_name, self.default) 

106 

107 # the value is stored in the private_name attribute. 

108 value = getattr(obj, self.private_name) 

109 

110 # check if there is a get callback and if yes call it 

111 if hasattr(obj, self._get_callback_name): 

112 getattr(obj, self._get_callback_name)(value) 

113 

114 return cast(ActiveType, value) 

115 

116 def __set__(self, obj: ActivableType, value: ActiveType) -> None: 

117 # this is the current value 

118 current_value = getattr(obj, self.private_name) 

119 # now set the new value 

120 setattr(obj, self.private_name, value) 

121 

122 if current_value != value: 

123 # the value really changed, call the change callback if it exists: 

124 if hasattr(obj, self._change_callback_name): 

125 getattr(obj, self._change_callback_name)(current_value, value) 

126 else: 

127 # the value did not change, but it was set anyhow. call the set callback if it exists 

128 if hasattr(obj, self._set_callback_name): 128 ↛ exitline 128 didn't return from function '__set__' because the condition on line 128 was always true

129 getattr(obj, self._set_callback_name)(value)