Skip to content

Commit 225e84c

Browse files
authored
Merge pull request #6731 from Geowissenschaften/add-ParamterMixin
Add ParameterMixin for extending parameter functionality
2 parents 7eb16d0 + 178643c commit 225e84c

11 files changed

+2178
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from .parameter_mixin import ParameterMixin
2+
from .parameter_mixin_group_registry import GroupRegistryParameterMixin
3+
from .parameter_mixin_interdependent import InterdependentParameterMixin
4+
from .parameter_mixin_on_cache_change import OnCacheChangeParameterMixin
5+
from .parameter_mixin_set_cache_value_on_reset import SetCacheValueOnResetParameterMixin
6+
7+
__all__ = [
8+
"GroupRegistryParameterMixin",
9+
"InterdependentParameterMixin",
10+
"OnCacheChangeParameterMixin",
11+
"ParameterMixin",
12+
"SetCacheValueOnResetParameterMixin",
13+
]
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
"""
2+
Provides the `ParameterMixin` base class, which allows modular extensions for
3+
QCoDeS parameter classes in a structured way.
4+
5+
Key Features:
6+
-------------
7+
- Ensures naming consistency via enforced naming conventions (e.g., class names must end with "ParameterMixin").
8+
- Provides a framework for checking compatibility between mixins and parameter base classes.
9+
- Supports multiple mixin composition when explicitly marked as compatible.
10+
- Logs warnings or raises errors for invalid mixin combinations or unsupported base classes.
11+
12+
Intended Usage:
13+
---------------
14+
This module is intended to be subclassed to create mixins that encapsulate
15+
additional behavior for QCoDeS parameters.
16+
17+
See Also:
18+
---------
19+
- Other mixins in the `qcodes.extensions.parameters` module
20+
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import logging
26+
import warnings
27+
from typing import Any, ClassVar
28+
29+
from qcodes.parameters import ParameterBase
30+
31+
log = logging.getLogger(__name__)
32+
33+
34+
class ParameterMixin:
35+
"""
36+
A mixin for extending QCoDeS Parameters with additional functionalities.
37+
38+
This mixin enforces naming conventions and verifies compatibility with
39+
subclasses of `ParameterBase`. The class name must end with ``ParameterMixin``.
40+
41+
If multiple mixins are combined, set the class attribute
42+
``_PARAMETER_MIXIN_CLASSES_COMPATIBLE = True`` to indicate compatibility.
43+
44+
Each mixin should define:
45+
46+
- ``_COMPATIBLE_BASES``: A list of `ParameterBase` subclasses it supports.
47+
- ``_INCOMPATIBLE_BASES``: A list of `ParameterBase` subclasses it should not be used with.
48+
49+
If compatibility is not explicitly defined, warnings are issued.
50+
If an incompatibility is declared, an error is raised.
51+
52+
Attributes:
53+
_COMPATIBLE_BASES (List[type[ParameterBase]]): List of compatible ParameterBase subclasses.
54+
_INCOMPATIBLE_BASES (List[type[ParameterBase]]): List of explicitly incompatible base classes.
55+
_PARAMETER_MIXIN_CLASSES_COMPATIBLE (bool): Set to True if this mixin can be combined with others.
56+
57+
Examples:
58+
59+
.. code-block:: python
60+
61+
class NewFeatureParameterMixin(ParameterMixin):
62+
# Adds NewFeature-related functionality to Parameters.
63+
_COMPATIBLE_BASES = [Parameter]
64+
_INCOMPATIBLE_BASES = []
65+
66+
class ABParameterMixin(AParameterMixin, BParameterMixin):
67+
# Combines A and B ParameterMixins.
68+
_PARAMETER_MIXIN_CLASSES_COMPATIBLE = True
69+
_COMPATIBLE_BASES = [Parameter]
70+
_INCOMPATIBLE_BASES = []
71+
72+
"""
73+
74+
_COMPATIBLE_BASES: ClassVar[list[type[ParameterBase]]] = []
75+
_INCOMPATIBLE_BASES: ClassVar[list[type[ParameterBase]]] = []
76+
77+
def __init_subclass__(cls, **kwargs: Any) -> None:
78+
super().__init_subclass__(**kwargs)
79+
80+
if "_COMPATIBLE_BASES" not in cls.__dict__:
81+
cls._COMPATIBLE_BASES = []
82+
83+
if "_INCOMPATIBLE_BASES" not in cls.__dict__:
84+
cls._INCOMPATIBLE_BASES = []
85+
86+
applied_mixin_leaf_list = cls._get_leaf_classes(
87+
base_type=ParameterMixin, exclude_base_type=ParameterBase
88+
)
89+
90+
all_applied_mixins = cls._get_mixin_classes(
91+
base_type=ParameterMixin, exclude_base_type=ParameterBase
92+
)
93+
94+
apply_to_parameter_base = False
95+
parameter_base_leaf: type[ParameterBase] | None = None
96+
if issubclass(cls, ParameterBase):
97+
apply_to_parameter_base = True
98+
parameter_base_leaves = cls._get_leaf_classes(
99+
base_type=ParameterBase, exclude_base_type=ParameterMixin
100+
)
101+
102+
if len(parameter_base_leaves) != 1:
103+
raise TypeError(
104+
f"Expected exactly one ParameterBase leaf subclass, found "
105+
f"{len(parameter_base_leaves)}."
106+
)
107+
parameter_base_leaf = parameter_base_leaves[0]
108+
cls._check_compatibility(
109+
all_mixins=all_applied_mixins,
110+
mixin_leaves=applied_mixin_leaf_list,
111+
parameter_base_leaf=parameter_base_leaf,
112+
)
113+
114+
elif issubclass(cls, ParameterMixin):
115+
if not cls.__name__.endswith("ParameterMixin"):
116+
raise ValueError(
117+
f"Class name '{cls.__name__}' must end with 'ParameterMixin'."
118+
)
119+
120+
if hasattr(cls, "_COMPATIBLE_BASES") and not isinstance(
121+
cls._COMPATIBLE_BASES, list
122+
):
123+
raise TypeError(
124+
f"{cls.__name__} must define _COMPATIBLE_BASES as a list."
125+
)
126+
127+
if hasattr(cls, "_INCOMPATIBLE_BASES") and not isinstance(
128+
cls._INCOMPATIBLE_BASES, list
129+
):
130+
raise TypeError(
131+
f"{cls.__name__} must define _INCOMPATIBLE_BASES as a list."
132+
)
133+
134+
multiple_mixin_leaves = len(applied_mixin_leaf_list) > 1
135+
parameter_mixin_classes_compatible = getattr(
136+
cls, "_PARAMETER_MIXIN_CLASSES_COMPATIBLE", False
137+
)
138+
if multiple_mixin_leaves:
139+
if apply_to_parameter_base:
140+
raise TypeError(
141+
f"Multiple ParameterMixin are applied together with "
142+
f"{parameter_base_leaf.__name__ if parameter_base_leaf is not None else 'None'}."
143+
f"Combine them into a single ParameterMixin "
144+
f"class before combining with a ParameterBase class."
145+
)
146+
else:
147+
if not parameter_mixin_classes_compatible:
148+
message = "Multiple ParameterMixin are combined without a being declared compatible."
149+
log.warning(message)
150+
warnings.warn(message, UserWarning)
151+
152+
if cls._COMPATIBLE_BASES == []:
153+
all_compatible_bases_sets = [
154+
set(aml._COMPATIBLE_BASES)
155+
for aml in applied_mixin_leaf_list
156+
if hasattr(aml, "_COMPATIBLE_BASES")
157+
]
158+
159+
common_compatible_bases = list(
160+
set.intersection(*all_compatible_bases_sets)
161+
)
162+
163+
if not common_compatible_bases:
164+
raise TypeError(
165+
f"{cls.__name__} does not have any common compatible ParameterBase classes "
166+
f"(_COMPATIBLE_BASES) among its applied ParameterMixin classes."
167+
)
168+
else:
169+
cls._COMPATIBLE_BASES = list(common_compatible_bases)
170+
171+
if issubclass(cls, ParameterBase):
172+
cls._update_docstring(all_applied_mixins, parameter_base_leaf)
173+
174+
@classmethod
175+
def _check_compatibility(
176+
cls,
177+
all_mixins: list[type[ParameterMixin]],
178+
mixin_leaves: list[type[ParameterMixin]],
179+
parameter_base_leaf: type[ParameterBase],
180+
) -> None:
181+
"""
182+
Check compatibility between applied ParameterMixin classes and the ParameterBase subclass.
183+
184+
Only ParameterMixin-to-ParameterBase compatibility is considered:
185+
- Raise TypeError if any applied mixin or the parameter base class is explicitly incompatible with the other.
186+
- Issue warnings if no explicit compatibility declaration exists between a mixin leaf and the ParameterBase leaf.
187+
"""
188+
for mixin in all_mixins:
189+
mixin_incompatible = set(getattr(mixin, "_INCOMPATIBLE_BASES", []))
190+
if parameter_base_leaf in mixin_incompatible:
191+
message = (
192+
f"{mixin.__name__} is incompatible with "
193+
f"{parameter_base_leaf.__name__}."
194+
)
195+
raise TypeError(message)
196+
197+
for mixin_leaf in mixin_leaves:
198+
mixin_leaf_compatible = set(getattr(mixin_leaf, "_COMPATIBLE_BASES", []))
199+
if parameter_base_leaf not in mixin_leaf_compatible:
200+
message = (
201+
f"{mixin_leaf.__name__} is not explicitly compatible with "
202+
f"{parameter_base_leaf.__name__}. Compatibility is untested."
203+
)
204+
log.warning(message)
205+
warnings.warn(message, UserWarning)
206+
207+
@classmethod
208+
def _update_docstring(
209+
cls,
210+
all_applied_mixins: list[type[ParameterMixin]],
211+
parameter_base_leaf: type[ParameterBase] | None,
212+
) -> None:
213+
"""
214+
Update the class docstring with information about applied mixins
215+
and the base parameter class.
216+
217+
Args:
218+
all_applied_mixins: List of applied ParameterMixin classes.
219+
parameter_base_leaf: The ParameterBase subclass.
220+
221+
"""
222+
mixin_names = [m.__name__ for m in all_applied_mixins]
223+
mixin_docs = [m.__doc__ or "" for m in all_applied_mixins]
224+
base_doc = (
225+
parameter_base_leaf.__doc__
226+
if parameter_base_leaf
227+
else "No documentation available."
228+
)
229+
230+
mixin_docs_text = "\n\n".join(mixin_docs) if mixin_docs else "None"
231+
232+
additional_doc = (
233+
f"This Parameter has been extended by the following ParameterMixins: \n "
234+
f"{', '.join(mixin_names) if mixin_names else 'None'}\n"
235+
f"Base Class: "
236+
f"{parameter_base_leaf.__name__ if parameter_base_leaf else 'None'}\n"
237+
f"Base Class Docstring:\n"
238+
f"{base_doc}\n"
239+
f"Mixin Docstrings:\n"
240+
f"{mixin_docs_text}"
241+
)
242+
original_doc = cls.__doc__ or ""
243+
cls.__doc__ = (original_doc.strip() + "\n\n" + additional_doc).strip()
244+
245+
@classmethod
246+
def _get_leaf_classes(
247+
cls, base_type: type, exclude_base_type: type | None = None
248+
) -> list[type]:
249+
"""
250+
Retrieve all leaf classes in the MRO of cls that are subclasses of base_type,
251+
excluding first MRO entry and any classes that are subclasses of exclude_base_type.
252+
253+
A leaf class is subclass of base_type that does not have any subclasses within the MRO
254+
for the specified base type, and is not a subclass of exclude_base_type.
255+
256+
Args:
257+
base_type: The base type to filter classes by (e.g., ParameterMixin).
258+
exclude_base_type: An optional base type to exclude from the results.
259+
260+
Returns:
261+
A list of leaf classes that are subclasses of base_type,
262+
excluding those that are subclasses of exclude_base_type.
263+
264+
"""
265+
mro = cls.__mro__[1:]
266+
267+
mixin_classes = [
268+
base
269+
for base in mro
270+
if issubclass(base, base_type) and base is not base_type
271+
]
272+
273+
if exclude_base_type:
274+
mixin_classes = [
275+
base
276+
for base in mixin_classes
277+
if not issubclass(base, exclude_base_type)
278+
]
279+
280+
leaf_classes = []
281+
for mixin in mixin_classes:
282+
if not any(
283+
issubclass(other, mixin) and other is not mixin
284+
for other in mixin_classes
285+
):
286+
leaf_classes.append(mixin)
287+
return leaf_classes
288+
289+
@classmethod
290+
def _get_mixin_classes(
291+
cls, base_type: type, exclude_base_type: type | None = None
292+
) -> list[type]:
293+
"""
294+
Retrieve all classes in the MRO of cls that are subclasses of base_type,
295+
excluding any classes that are subclasses of exclude_base_type and the first MRO entry.
296+
297+
Args:
298+
base_type: The base type to filter classes by (e.g., ParameterMixin).
299+
exclude_base_type: An optional base type to exclude from the results.
300+
301+
Returns:
302+
A list of classes that are subclasses of base_type,
303+
excluding those that are subclasses of exclude_base_type.
304+
305+
"""
306+
mro = cls.__mro__[1:]
307+
308+
mixin_classes = [
309+
base
310+
for base in mro
311+
if issubclass(base, base_type) and base is not base_type
312+
]
313+
314+
if exclude_base_type:
315+
mixin_classes = [
316+
base
317+
for base in mixin_classes
318+
if not issubclass(base, exclude_base_type)
319+
]
320+
321+
return mixin_classes

0 commit comments

Comments
 (0)