qq_lib.core.field_coupling
Utilities for defining and enforcing coupled fields in dataclasses.
This module provides FieldCoupling for specifying dominance-ordered
relationships among multiple fields, and the @coupled_fields decorator for
automatically enforcing these rules in a dataclass's __post_init__.
1# Released under MIT License. 2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab 3 4""" 5Utilities for defining and enforcing coupled fields in dataclasses. 6 7This module provides `FieldCoupling` for specifying dominance-ordered 8relationships among multiple fields, and the `@coupled_fields` decorator for 9automatically enforcing these rules in a dataclass's `__post_init__`. 10""" 11 12from typing import Any, Protocol 13 14 15class FieldCoupling: 16 """ 17 Represents a coupling among multiple fields, ordered by dominance. 18 19 The earlier a field appears in `fields`, the more dominant it is. 20 If multiple fields have values in an instance, only the most dominant 21 one is preserved; all others are set to None. 22 """ 23 24 def __init__(self, *fields: str): 25 if len(fields) < 2: 26 raise ValueError("FieldCoupling requires at least two fields") 27 self.fields = list(fields) 28 29 def contains(self, field_name: str) -> bool: 30 """Return True if the field participates in this coupling.""" 31 return field_name in self.fields 32 33 def get_fields(self) -> tuple[str, ...]: 34 """Return all coupled fields as a tuple.""" 35 return tuple(self.fields) 36 37 def has_value(self, instance: Any) -> bool: 38 """Return True if any of the coupled fields has a non-None value.""" 39 return any(getattr(instance, field) is not None for field in self.fields) 40 41 def get_most_dominant_set_field(self, instance: Any) -> str | None: 42 """ 43 Return the name of the most dominant field that has a non-None value, 44 or None if none of them do. 45 """ 46 for field in self.fields: 47 if getattr(instance, field) is not None: 48 return field 49 50 return None 51 52 def enforce(self, instance: Any): 53 """ 54 Enforce dominance rules: only the most dominant field that is set 55 keeps its value; others are reset to None. 56 """ 57 dominant_set_field = self.get_most_dominant_set_field(instance) 58 if dominant_set_field is None: 59 return 60 61 for field in self.fields: 62 if field != dominant_set_field: 63 setattr(instance, field, None) 64 65 66def coupled_fields(*couplings: FieldCoupling): 67 """ 68 Class decorator that enforces multi-field coupling rules in __post_init__. 69 """ 70 71 def decorator(cls): 72 cls._field_couplings = couplings 73 original_post_init = getattr(cls, "__post_init__", None) 74 75 def __post_init__(self): 76 for coupling in self._field_couplings: 77 coupling.enforce(self) 78 79 if original_post_init: 80 original_post_init(self) 81 82 @staticmethod 83 def get_coupling_for_field(field_name: str) -> FieldCoupling | None: 84 for coupling in cls._field_couplings: 85 if coupling.contains(field_name): 86 return coupling 87 88 return None 89 90 cls.__post_init__ = __post_init__ 91 cls.get_coupling_for_field = get_coupling_for_field 92 return cls 93 94 return decorator 95 96 97class HasCouplingMethods(Protocol): 98 """Protocol for classes decorated with @coupled_fields.""" 99 100 _field_couplings: tuple[FieldCoupling, ...] 101 102 @staticmethod 103 def get_coupling_for_field(field_name: str) -> FieldCoupling | None: 104 """Return the FieldCoupling that contains the given field name, or None.""" 105 ...
16class FieldCoupling: 17 """ 18 Represents a coupling among multiple fields, ordered by dominance. 19 20 The earlier a field appears in `fields`, the more dominant it is. 21 If multiple fields have values in an instance, only the most dominant 22 one is preserved; all others are set to None. 23 """ 24 25 def __init__(self, *fields: str): 26 if len(fields) < 2: 27 raise ValueError("FieldCoupling requires at least two fields") 28 self.fields = list(fields) 29 30 def contains(self, field_name: str) -> bool: 31 """Return True if the field participates in this coupling.""" 32 return field_name in self.fields 33 34 def get_fields(self) -> tuple[str, ...]: 35 """Return all coupled fields as a tuple.""" 36 return tuple(self.fields) 37 38 def has_value(self, instance: Any) -> bool: 39 """Return True if any of the coupled fields has a non-None value.""" 40 return any(getattr(instance, field) is not None for field in self.fields) 41 42 def get_most_dominant_set_field(self, instance: Any) -> str | None: 43 """ 44 Return the name of the most dominant field that has a non-None value, 45 or None if none of them do. 46 """ 47 for field in self.fields: 48 if getattr(instance, field) is not None: 49 return field 50 51 return None 52 53 def enforce(self, instance: Any): 54 """ 55 Enforce dominance rules: only the most dominant field that is set 56 keeps its value; others are reset to None. 57 """ 58 dominant_set_field = self.get_most_dominant_set_field(instance) 59 if dominant_set_field is None: 60 return 61 62 for field in self.fields: 63 if field != dominant_set_field: 64 setattr(instance, field, None)
Represents a coupling among multiple fields, ordered by dominance.
The earlier a field appears in fields, the more dominant it is.
If multiple fields have values in an instance, only the most dominant
one is preserved; all others are set to None.
30 def contains(self, field_name: str) -> bool: 31 """Return True if the field participates in this coupling.""" 32 return field_name in self.fields
Return True if the field participates in this coupling.
34 def get_fields(self) -> tuple[str, ...]: 35 """Return all coupled fields as a tuple.""" 36 return tuple(self.fields)
Return all coupled fields as a tuple.
38 def has_value(self, instance: Any) -> bool: 39 """Return True if any of the coupled fields has a non-None value.""" 40 return any(getattr(instance, field) is not None for field in self.fields)
Return True if any of the coupled fields has a non-None value.
42 def get_most_dominant_set_field(self, instance: Any) -> str | None: 43 """ 44 Return the name of the most dominant field that has a non-None value, 45 or None if none of them do. 46 """ 47 for field in self.fields: 48 if getattr(instance, field) is not None: 49 return field 50 51 return None
Return the name of the most dominant field that has a non-None value, or None if none of them do.
53 def enforce(self, instance: Any): 54 """ 55 Enforce dominance rules: only the most dominant field that is set 56 keeps its value; others are reset to None. 57 """ 58 dominant_set_field = self.get_most_dominant_set_field(instance) 59 if dominant_set_field is None: 60 return 61 62 for field in self.fields: 63 if field != dominant_set_field: 64 setattr(instance, field, None)
Enforce dominance rules: only the most dominant field that is set keeps its value; others are reset to None.
67def coupled_fields(*couplings: FieldCoupling): 68 """ 69 Class decorator that enforces multi-field coupling rules in __post_init__. 70 """ 71 72 def decorator(cls): 73 cls._field_couplings = couplings 74 original_post_init = getattr(cls, "__post_init__", None) 75 76 def __post_init__(self): 77 for coupling in self._field_couplings: 78 coupling.enforce(self) 79 80 if original_post_init: 81 original_post_init(self) 82 83 @staticmethod 84 def get_coupling_for_field(field_name: str) -> FieldCoupling | None: 85 for coupling in cls._field_couplings: 86 if coupling.contains(field_name): 87 return coupling 88 89 return None 90 91 cls.__post_init__ = __post_init__ 92 cls.get_coupling_for_field = get_coupling_for_field 93 return cls 94 95 return decorator
Class decorator that enforces multi-field coupling rules in __post_init__.
98class HasCouplingMethods(Protocol): 99 """Protocol for classes decorated with @coupled_fields.""" 100 101 _field_couplings: tuple[FieldCoupling, ...] 102 103 @staticmethod 104 def get_coupling_for_field(field_name: str) -> FieldCoupling | None: 105 """Return the FieldCoupling that contains the given field name, or None.""" 106 ...
Protocol for classes decorated with @coupled_fields.
1945def _no_init_or_replace_init(self, *args, **kwargs): 1946 cls = type(self) 1947 1948 if cls._is_protocol: 1949 raise TypeError('Protocols cannot be instantiated') 1950 1951 # Already using a custom `__init__`. No need to calculate correct 1952 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1953 if cls.__init__ is not _no_init_or_replace_init: 1954 return 1955 1956 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1957 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1958 # searches for a proper new `__init__` in the MRO. The new `__init__` 1959 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1960 # instantiation of the protocol subclass will thus use the new 1961 # `__init__` and no longer call `_no_init_or_replace_init`. 1962 for base in cls.__mro__: 1963 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1964 if init is not _no_init_or_replace_init: 1965 cls.__init__ = init 1966 break 1967 else: 1968 # should not happen 1969 cls.__init__ = object.__init__ 1970 1971 cls.__init__(self, *args, **kwargs)