muutils.misc
miscellaneous utilities
stable_hash
for hashing that is stable across runsmuutils.misc.sequence
for sequence manipulation, applying mappings, and string-like operations on listsmuutils.misc.string
for sanitizing things for filenames, adjusting docstrings, and converting dicts to filenamesmuutils.misc.numerical
for turning numbers into nice strings and backmuutils.misc.freezing
for freezing thingsmuutils.misc.classes
for some weird class utilities
1"""miscellaneous utilities 2 3- `stable_hash` for hashing that is stable across runs 4- `muutils.misc.sequence` for sequence manipulation, applying mappings, and string-like operations on lists 5- `muutils.misc.string` for sanitizing things for filenames, adjusting docstrings, and converting dicts to filenames 6- `muutils.misc.numerical` for turning numbers into nice strings and back 7- `muutils.misc.freezing` for freezing things 8- `muutils.misc.classes` for some weird class utilities 9""" 10 11from muutils.misc.hashing import stable_hash 12from muutils.misc.sequence import ( 13 WhenMissing, 14 empty_sequence_if_attr_false, 15 flatten, 16 list_split, 17 list_join, 18 apply_mapping, 19 apply_mapping_chain, 20) 21from muutils.misc.string import ( 22 sanitize_name, 23 sanitize_fname, 24 sanitize_identifier, 25 dict_to_filename, 26 dynamic_docstring, 27) 28from muutils.misc.numerical import ( 29 shorten_numerical_to_str, 30 str_to_numeric, 31 _SHORTEN_MAP, 32) 33from muutils.misc.freezing import ( 34 FrozenDict, 35 FrozenList, 36 freeze, 37) 38from muutils.misc.classes import ( 39 is_abstract, 40 get_all_subclasses, 41 isinstance_by_type_name, 42 IsDataclass, 43 get_hashable_eq_attrs, 44 dataclass_set_equals, 45) 46 47 48__all__ = [ 49 # submodules 50 "classes", 51 "freezing", 52 "func", 53 "hashing", 54 "numerical", 55 "sequence", 56 "string", 57 # imports 58 "stable_hash", 59 "WhenMissing", 60 "empty_sequence_if_attr_false", 61 "flatten", 62 "list_split", 63 "list_join", 64 "apply_mapping", 65 "apply_mapping_chain", 66 "sanitize_name", 67 "sanitize_fname", 68 "sanitize_identifier", 69 "dict_to_filename", 70 "dynamic_docstring", 71 "shorten_numerical_to_str", 72 "str_to_numeric", 73 "_SHORTEN_MAP", 74 "FrozenDict", 75 "FrozenList", 76 "freeze", 77 "is_abstract", 78 "get_all_subclasses", 79 "isinstance_by_type_name", 80 "IsDataclass", 81 "get_hashable_eq_attrs", 82 "dataclass_set_equals", 83]
8def stable_hash(s: str | bytes) -> int: 9 """Returns a stable hash of the given string. not cryptographically secure, but stable between runs""" 10 # init hash object and update with string 11 s_bytes: bytes 12 if isinstance(s, str): 13 s_bytes = bytes(s, "UTF-8") 14 else: 15 s_bytes = s 16 hash_obj: hashlib._Hash = hashlib.sha256(s_bytes) 17 # get digest and convert to int 18 return int.from_bytes(hash_obj.digest(), "big")
Returns a stable hash of the given string. not cryptographically secure, but stable between runs
22def empty_sequence_if_attr_false( 23 itr: Iterable[Any], 24 attr_owner: Any, 25 attr_name: str, 26) -> Iterable[Any]: 27 """Returns `itr` if `attr_owner` has the attribute `attr_name` and it boolean casts to `True`. Returns an empty sequence otherwise. 28 29 Particularly useful for optionally inserting delimiters into a sequence depending on an `TokenizerElement` attribute. 30 31 # Parameters: 32 - `itr: Iterable[Any]` 33 The iterable to return if the attribute is `True`. 34 - `attr_owner: Any` 35 The object to check for the attribute. 36 - `attr_name: str` 37 The name of the attribute to check. 38 39 # Returns: 40 - `itr: Iterable` if `attr_owner` has the attribute `attr_name` and it boolean casts to `True`, otherwise an empty sequence. 41 - `()` an empty sequence if the attribute is `False` or not present. 42 """ 43 return itr if bool(getattr(attr_owner, attr_name, False)) else ()
Returns itr
if attr_owner
has the attribute attr_name
and it boolean casts to True
. Returns an empty sequence otherwise.
Particularly useful for optionally inserting delimiters into a sequence depending on an TokenizerElement
attribute.
Parameters:
itr: Iterable[Any]
The iterable to return if the attribute isTrue
.attr_owner: Any
The object to check for the attribute.attr_name: str
The name of the attribute to check.
Returns:
itr: Iterable
ifattr_owner
has the attributeattr_name
and it boolean casts toTrue
, otherwise an empty sequence.()
an empty sequence if the attribute isFalse
or not present.
46def flatten(it: Iterable[Any], levels_to_flatten: int | None = None) -> Generator: 47 """ 48 Flattens an arbitrarily nested iterable. 49 Flattens all iterable data types except for `str` and `bytes`. 50 51 # Returns 52 Generator over the flattened sequence. 53 54 # Parameters 55 - `it`: Any arbitrarily nested iterable. 56 - `levels_to_flatten`: Number of levels to flatten by, starting at the outermost layer. If `None`, performs full flattening. 57 """ 58 for x in it: 59 # TODO: swap type check with more general check for __iter__() or __next__() or whatever 60 if ( 61 hasattr(x, "__iter__") 62 and not isinstance(x, (str, bytes)) 63 and (levels_to_flatten is None or levels_to_flatten > 0) 64 ): 65 yield from flatten( 66 x, None if levels_to_flatten is None else levels_to_flatten - 1 67 ) 68 else: 69 yield x
Flattens an arbitrarily nested iterable.
Flattens all iterable data types except for str
and bytes
.
Returns
Generator over the flattened sequence.
Parameters
it
: Any arbitrarily nested iterable.levels_to_flatten
: Number of levels to flatten by, starting at the outermost layer. IfNone
, performs full flattening.
76def list_split(lst: list, val: Any) -> list[list]: 77 """split a list into sublists by `val`. similar to "a_b_c".split("_") 78 79 ```python 80 >>> list_split([1,2,3,0,4,5,0,6], 0) 81 [[1, 2, 3], [4, 5], [6]] 82 >>> list_split([0,1,2,3], 0) 83 [[], [1, 2, 3]] 84 >>> list_split([1,2,3], 0) 85 [[1, 2, 3]] 86 >>> list_split([], 0) 87 [[]] 88 ``` 89 90 """ 91 92 if len(lst) == 0: 93 return [[]] 94 95 output: list[list] = [ 96 [], 97 ] 98 99 for x in lst: 100 if x == val: 101 output.append([]) 102 else: 103 output[-1].append(x) 104 return output
split a list into sublists by val
. similar to "a_b_c".split("_")
>>> list_split([1,2,3,0,4,5,0,6], 0)
[[1, 2, 3], [4, 5], [6]]
>>> list_split([0,1,2,3], 0)
[[], [1, 2, 3]]
>>> list_split([1,2,3], 0)
[[1, 2, 3]]
>>> list_split([], 0)
[[]]
107def list_join(lst: list, factory: Callable) -> list: 108 """add a *new* instance of `factory()` between each element of `lst` 109 110 ```python 111 >>> list_join([1,2,3], lambda : 0) 112 [1,0,2,0,3] 113 >>> list_join([1,2,3], lambda: [time.sleep(0.1), time.time()][1]) 114 [1, 1600000000.0, 2, 1600000000.1, 3] 115 ``` 116 """ 117 118 if len(lst) == 0: 119 return [] 120 121 output: list = [ 122 lst[0], 123 ] 124 125 for x in lst[1:]: 126 output.append(factory()) 127 output.append(x) 128 129 return output
add a new instance of factory()
between each element of lst
>>> list_join([1,2,3], lambda : 0)
[1,0,2,0,3]
>>> list_join([1,2,3], lambda: [time.sleep(0.1), time.time()][1])
[1, 1600000000.0, 2, 1600000000.1, 3]
139def apply_mapping( 140 mapping: Mapping[_AM_K, _AM_V], 141 iter: Iterable[_AM_K], 142 when_missing: WhenMissing = "skip", 143) -> list[Union[_AM_K, _AM_V]]: 144 """Given an iterable and a mapping, apply the mapping to the iterable with certain options 145 146 Gotcha: if `when_missing` is invalid, this is totally fine until a missing key is actually encountered. 147 148 Note: you can use this with `muutils.kappa.Kappa` if you want to pass a function instead of a dict 149 150 # Parameters: 151 - `mapping : Mapping[_AM_K, _AM_V]` 152 must have `__contains__` and `__getitem__`, both of which take `_AM_K` and the latter returns `_AM_V` 153 - `iter : Iterable[_AM_K]` 154 the iterable to apply the mapping to 155 - `when_missing : WhenMissing` 156 what to do when a key is missing from the mapping -- this is what distinguishes this function from `map` 157 you can choose from `"skip"`, `"include"` (without converting), and `"except"` 158 (defaults to `"skip"`) 159 160 # Returns: 161 return type is one of: 162 - `list[_AM_V]` if `when_missing` is `"skip"` or `"except"` 163 - `list[Union[_AM_K, _AM_V]]` if `when_missing` is `"include"` 164 165 # Raises: 166 - `KeyError` : if the item is missing from the mapping and `when_missing` is `"except"` 167 - `ValueError` : if `when_missing` is invalid 168 """ 169 output: list[Union[_AM_K, _AM_V]] = list() 170 item: _AM_K 171 for item in iter: 172 if item in mapping: 173 output.append(mapping[item]) 174 continue 175 if when_missing == "skip": 176 continue 177 elif when_missing == "include": 178 output.append(item) 179 elif when_missing == "except": 180 raise KeyError(f"item {item} is missing from mapping {mapping}") 181 else: 182 raise ValueError( 183 f"invalid value for {when_missing = }\n{item = }\n{mapping = }" 184 ) 185 return output
Given an iterable and a mapping, apply the mapping to the iterable with certain options
Gotcha: if when_missing
is invalid, this is totally fine until a missing key is actually encountered.
Note: you can use this with muutils.kappa.Kappa
if you want to pass a function instead of a dict
Parameters:
mapping : Mapping[_AM_K, _AM_V]
must have__contains__
and__getitem__
, both of which take_AM_K
and the latter returns_AM_V
iter : Iterable[_AM_K]
the iterable to apply the mapping towhen_missing : WhenMissing
what to do when a key is missing from the mapping -- this is what distinguishes this function frommap
you can choose from"skip"
,"include"
(without converting), and"except"
(defaults to"skip"
)
Returns:
return type is one of:
list[_AM_V]
ifwhen_missing
is"skip"
or"except"
list[Union[_AM_K, _AM_V]]
ifwhen_missing
is"include"
Raises:
KeyError
: if the item is missing from the mapping andwhen_missing
is"except"
ValueError
: ifwhen_missing
is invalid
188def apply_mapping_chain( 189 mapping: Mapping[_AM_K, Iterable[_AM_V]], 190 iter: Iterable[_AM_K], 191 when_missing: WhenMissing = "skip", 192) -> list[Union[_AM_K, _AM_V]]: 193 """Given an iterable and a mapping, chain the mappings together 194 195 Gotcha: if `when_missing` is invalid, this is totally fine until a missing key is actually encountered. 196 197 Note: you can use this with `muutils.kappa.Kappa` if you want to pass a function instead of a dict 198 199 # Parameters: 200 - `mapping : Mapping[_AM_K, Iterable[_AM_V]]` 201 must have `__contains__` and `__getitem__`, both of which take `_AM_K` and the latter returns `Iterable[_AM_V]` 202 - `iter : Iterable[_AM_K]` 203 the iterable to apply the mapping to 204 - `when_missing : WhenMissing` 205 what to do when a key is missing from the mapping -- this is what distinguishes this function from `map` 206 you can choose from `"skip"`, `"include"` (without converting), and `"except"` 207 (defaults to `"skip"`) 208 209 # Returns: 210 return type is one of: 211 - `list[_AM_V]` if `when_missing` is `"skip"` or `"except"` 212 - `list[Union[_AM_K, _AM_V]]` if `when_missing` is `"include"` 213 214 # Raises: 215 - `KeyError` : if the item is missing from the mapping and `when_missing` is `"except"` 216 - `ValueError` : if `when_missing` is invalid 217 218 """ 219 output: list[Union[_AM_K, _AM_V]] = list() 220 item: _AM_K 221 for item in iter: 222 if item in mapping: 223 output.extend(mapping[item]) 224 continue 225 if when_missing == "skip": 226 continue 227 elif when_missing == "include": 228 output.append(item) 229 elif when_missing == "except": 230 raise KeyError(f"item {item} is missing from mapping {mapping}") 231 else: 232 raise ValueError( 233 f"invalid value for {when_missing = }\n{item = }\n{mapping = }" 234 ) 235 return output
Given an iterable and a mapping, chain the mappings together
Gotcha: if when_missing
is invalid, this is totally fine until a missing key is actually encountered.
Note: you can use this with muutils.kappa.Kappa
if you want to pass a function instead of a dict
Parameters:
mapping : Mapping[_AM_K, Iterable[_AM_V]]
must have__contains__
and__getitem__
, both of which take_AM_K
and the latter returnsIterable[_AM_V]
iter : Iterable[_AM_K]
the iterable to apply the mapping towhen_missing : WhenMissing
what to do when a key is missing from the mapping -- this is what distinguishes this function frommap
you can choose from"skip"
,"include"
(without converting), and"except"
(defaults to"skip"
)
Returns:
return type is one of:
list[_AM_V]
ifwhen_missing
is"skip"
or"except"
list[Union[_AM_K, _AM_V]]
ifwhen_missing
is"include"
Raises:
KeyError
: if the item is missing from the mapping andwhen_missing
is"except"
ValueError
: ifwhen_missing
is invalid
8def sanitize_name( 9 name: str | None, 10 additional_allowed_chars: str = "", 11 replace_invalid: str = "", 12 when_none: str | None = "_None_", 13 leading_digit_prefix: str = "", 14) -> str: 15 """sanitize a string, leaving only alphanumerics and `additional_allowed_chars` 16 17 # Parameters: 18 - `name : str | None` 19 input string 20 - `additional_allowed_chars : str` 21 additional characters to allow, none by default 22 (defaults to `""`) 23 - `replace_invalid : str` 24 character to replace invalid characters with 25 (defaults to `""`) 26 - `when_none : str | None` 27 string to return if `name` is `None`. if `None`, raises an exception 28 (defaults to `"_None_"`) 29 - `leading_digit_prefix : str` 30 character to prefix the string with if it starts with a digit 31 (defaults to `""`) 32 33 # Returns: 34 - `str` 35 sanitized string 36 """ 37 38 if name is None: 39 if when_none is None: 40 raise ValueError("name is None") 41 else: 42 return when_none 43 44 sanitized: str = "" 45 for char in name: 46 if char.isalnum(): 47 sanitized += char 48 elif char in additional_allowed_chars: 49 sanitized += char 50 else: 51 sanitized += replace_invalid 52 53 if sanitized[0].isdigit(): 54 sanitized = leading_digit_prefix + sanitized 55 56 return sanitized
sanitize a string, leaving only alphanumerics and additional_allowed_chars
Parameters:
name : str | None
input stringadditional_allowed_chars : str
additional characters to allow, none by default (defaults to""
)replace_invalid : str
character to replace invalid characters with (defaults to""
)when_none : str | None
string to return ifname
isNone
. ifNone
, raises an exception (defaults to"_None_"
)leading_digit_prefix : str
character to prefix the string with if it starts with a digit (defaults to""
)
Returns:
str
sanitized string
59def sanitize_fname(fname: str | None, **kwargs) -> str: 60 """sanitize a filename to posix standards 61 62 - leave only alphanumerics, `_` (underscore), '-' (dash) and `.` (period) 63 """ 64 return sanitize_name(fname, additional_allowed_chars="._-", **kwargs)
sanitize a filename to posix standards
- leave only alphanumerics,
_
(underscore), '-' (dash) and.
(period)
67def sanitize_identifier(fname: str | None, **kwargs) -> str: 68 """sanitize an identifier (variable or function name) 69 70 - leave only alphanumerics and `_` (underscore) 71 - prefix with `_` if it starts with a digit 72 """ 73 return sanitize_name( 74 fname, additional_allowed_chars="_", leading_digit_prefix="_", **kwargs 75 )
sanitize an identifier (variable or function name)
- leave only alphanumerics and
_
(underscore) - prefix with
_
if it starts with a digit
78def dict_to_filename( 79 data: dict, 80 format_str: str = "{key}_{val}", 81 separator: str = ".", 82 max_length: int = 255, 83): 84 # Convert the dictionary items to a list of strings using the format string 85 formatted_items: list[str] = [ 86 format_str.format(key=k, val=v) for k, v in data.items() 87 ] 88 89 # Join the formatted items using the separator 90 joined_str: str = separator.join(formatted_items) 91 92 # Remove special characters and spaces 93 sanitized_str: str = sanitize_fname(joined_str) 94 95 # Check if the length is within limits 96 if len(sanitized_str) <= max_length: 97 return sanitized_str 98 99 # If the string is too long, generate a hash 100 return f"h_{stable_hash(sanitized_str)}"
23def shorten_numerical_to_str( 24 num: int | float, 25 small_as_decimal: bool = True, 26 precision: int = 1, 27) -> str: 28 """shorten a large numerical value to a string 29 1234 -> 1K 30 31 precision guaranteed to 1 in 10, but can be higher. reverse of `str_to_numeric` 32 """ 33 34 # small values are returned as is 35 num_abs: float = abs(num) 36 if num_abs < 1e3: 37 return str(num) 38 39 # iterate over suffixes from largest to smallest 40 for i, (val, suffix) in enumerate(_SHORTEN_TUPLES): 41 if num_abs > val or i == len(_SHORTEN_TUPLES) - 1: 42 if (num_abs < val * 10) and small_as_decimal: 43 return f"{num / val:.{precision}f}{suffix}" 44 elif num_abs < val * 1e3: 45 return f"{int(round(num / val))}{suffix}" 46 47 return f"{num:.{precision}f}"
shorten a large numerical value to a string 1234 -> 1K
precision guaranteed to 1 in 10, but can be higher. reverse of str_to_numeric
50def str_to_numeric( 51 quantity: str, 52 mapping: None | bool | dict[str, int | float] = True, 53) -> int | float: 54 """Convert a string representing a quantity to a numeric value. 55 56 The string can represent an integer, python float, fraction, or shortened via `shorten_numerical_to_str`. 57 58 # Examples: 59 ``` 60 >>> str_to_numeric("5") 61 5 62 >>> str_to_numeric("0.1") 63 0.1 64 >>> str_to_numeric("1/5") 65 0.2 66 >>> str_to_numeric("-1K") 67 -1000.0 68 >>> str_to_numeric("1.5M") 69 1500000.0 70 >>> str_to_numeric("1.2e2") 71 120.0 72 ``` 73 74 """ 75 76 # check is string 77 if not isinstance(quantity, str): 78 raise TypeError( 79 f"quantity must be a string, got '{type(quantity) = }' '{quantity = }'" 80 ) 81 82 # basic int conversion 83 try: 84 quantity_int: int = int(quantity) 85 return quantity_int 86 except ValueError: 87 pass 88 89 # basic float conversion 90 try: 91 quantity_float: float = float(quantity) 92 return quantity_float 93 except ValueError: 94 pass 95 96 # mapping 97 _mapping: dict[str, int | float] 98 if mapping is True or mapping is None: 99 _mapping = _REVERSE_SHORTEN_MAP 100 else: 101 _mapping = mapping # type: ignore[assignment] 102 103 quantity_original: str = quantity 104 105 quantity = quantity.strip() 106 107 result: int | float 108 multiplier: int | float = 1 109 110 # detect if it has a suffix 111 suffixes_detected: list[bool] = [suffix in quantity for suffix in _mapping] 112 n_suffixes_detected: int = sum(suffixes_detected) 113 if n_suffixes_detected == 0: 114 # no suffix 115 pass 116 elif n_suffixes_detected == 1: 117 # find multiplier 118 for suffix, mult in _mapping.items(): 119 if quantity.endswith(suffix): 120 # remove suffix, store multiplier, and break 121 quantity = quantity[: -len(suffix)].strip() 122 multiplier = mult 123 break 124 else: 125 raise ValueError(f"Invalid suffix in {quantity_original}") 126 else: 127 # multiple suffixes 128 raise ValueError(f"Multiple suffixes detected in {quantity_original}") 129 130 # fractions 131 if "/" in quantity: 132 try: 133 assert quantity.count("/") == 1, "too many '/'" 134 # split and strip 135 num, den = quantity.split("/") 136 num = num.strip() 137 den = den.strip() 138 num_sign: int = 1 139 # negative numbers 140 if num.startswith("-"): 141 num_sign = -1 142 num = num[1:] 143 # assert that both are digits 144 assert ( 145 num.isdigit() and den.isdigit() 146 ), "numerator and denominator must be digits" 147 # return the fraction 148 result = num_sign * ( 149 int(num) / int(den) 150 ) # this allows for fractions with suffixes, which is weird, but whatever 151 except AssertionError as e: 152 raise ValueError(f"Invalid fraction {quantity_original}: {e}") from e 153 154 # decimals 155 else: 156 try: 157 result = int(quantity) 158 except ValueError: 159 try: 160 result = float(quantity) 161 except ValueError as e: 162 raise ValueError( 163 f"Invalid quantity {quantity_original} ({quantity})" 164 ) from e 165 166 return result * multiplier
Convert a string representing a quantity to a numeric value.
The string can represent an integer, python float, fraction, or shortened via shorten_numerical_to_str
.
Examples:
>>> str_to_numeric("5")
5
>>> str_to_numeric("0.1")
0.1
>>> str_to_numeric("1/5")
0.2
>>> str_to_numeric("-1K")
-1000.0
>>> str_to_numeric("1.5M")
1500000.0
>>> str_to_numeric("1.2e2")
120.0
5class FrozenDict(dict): 6 def __setitem__(self, key, value): 7 raise AttributeError("dict is frozen") 8 9 def __delitem__(self, key): 10 raise AttributeError("dict is frozen")
Inherited Members
- builtins.dict
- get
- setdefault
- pop
- popitem
- keys
- items
- values
- update
- fromkeys
- clear
- copy
13class FrozenList(list): 14 def __setitem__(self, index, value): 15 raise AttributeError("list is frozen") 16 17 def __delitem__(self, index): 18 raise AttributeError("list is frozen") 19 20 def append(self, value): 21 raise AttributeError("list is frozen") 22 23 def extend(self, iterable): 24 raise AttributeError("list is frozen") 25 26 def insert(self, index, value): 27 raise AttributeError("list is frozen") 28 29 def remove(self, value): 30 raise AttributeError("list is frozen") 31 32 def pop(self, index=-1): 33 raise AttributeError("list is frozen") 34 35 def clear(self): 36 raise AttributeError("list is frozen")
Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
Remove first occurrence of value.
Raises ValueError if the value is not present.
Remove and return item at index (default last).
Raises IndexError if list is empty or index is out of range.
Inherited Members
- builtins.list
- list
- copy
- index
- count
- reverse
- sort
39def freeze(instance: object) -> object: 40 """recursively freeze an object in-place so that its attributes and elements cannot be changed 41 42 messy in the sense that sometimes the object is modified in place, but you can't rely on that. always use the return value. 43 44 the [gelidum](https://github.com/diegojromerolopez/gelidum/) package is a more complete implementation of this idea 45 46 """ 47 48 # mark as frozen 49 if hasattr(instance, "_IS_FROZEN"): 50 if instance._IS_FROZEN: 51 return instance 52 53 # try to mark as frozen 54 try: 55 instance._IS_FROZEN = True # type: ignore[attr-defined] 56 except AttributeError: 57 pass 58 59 # skip basic types, weird things, or already frozen things 60 if isinstance(instance, (bool, int, float, str, bytes)): 61 pass 62 63 elif isinstance(instance, (type(None), type(Ellipsis))): 64 pass 65 66 elif isinstance(instance, (FrozenList, FrozenDict, frozenset)): 67 pass 68 69 # handle containers 70 elif isinstance(instance, list): 71 for i in range(len(instance)): 72 instance[i] = freeze(instance[i]) 73 instance = FrozenList(instance) 74 75 elif isinstance(instance, tuple): 76 instance = tuple(freeze(item) for item in instance) 77 78 elif isinstance(instance, set): 79 instance = frozenset({freeze(item) for item in instance}) 80 81 elif isinstance(instance, dict): 82 for key, value in instance.items(): 83 instance[key] = freeze(value) 84 instance = FrozenDict(instance) 85 86 # handle custom classes 87 else: 88 # set everything in the __dict__ to frozen 89 instance.__dict__ = freeze(instance.__dict__) # type: ignore[assignment] 90 91 # create a new class which inherits from the original class 92 class FrozenClass(instance.__class__): # type: ignore[name-defined] 93 def __setattr__(self, name, value): 94 raise AttributeError("class is frozen") 95 96 FrozenClass.__name__ = f"FrozenClass__{instance.__class__.__name__}" 97 FrozenClass.__module__ = instance.__class__.__module__ 98 FrozenClass.__doc__ = instance.__class__.__doc__ 99 100 # set the instance's class to the new class 101 try: 102 instance.__class__ = FrozenClass 103 except TypeError as e: 104 raise TypeError( 105 f"Cannot freeze:\n{instance = }\n{instance.__class__ = }\n{FrozenClass = }" 106 ) from e 107 108 return instance
recursively freeze an object in-place so that its attributes and elements cannot be changed
messy in the sense that sometimes the object is modified in place, but you can't rely on that. always use the return value.
the gelidum package is a more complete implementation of this idea
15def is_abstract(cls: type) -> bool: 16 """ 17 Returns if a class is abstract. 18 """ 19 if not hasattr(cls, "__abstractmethods__"): 20 return False # an ordinary class 21 elif len(cls.__abstractmethods__) == 0: 22 return False # a concrete implementation of an abstract class 23 else: 24 return True # an abstract class
Returns if a class is abstract.
27def get_all_subclasses(class_: type, include_self=False) -> set[type]: 28 """ 29 Returns a set containing all child classes in the subclass graph of `class_`. 30 I.e., includes subclasses of subclasses, etc. 31 32 # Parameters 33 - `include_self`: Whether to include `class_` itself in the returned set 34 - `class_`: Superclass 35 36 # Development 37 Since most class hierarchies are small, the inefficiencies of the existing recursive implementation aren't problematic. 38 It might be valuable to refactor with memoization if the need arises to use this function on a very large class hierarchy. 39 """ 40 subs: set[type] = set( 41 flatten( 42 get_all_subclasses(sub, include_self=True) 43 for sub in class_.__subclasses__() 44 if sub is not None 45 ) 46 ) 47 if include_self: 48 subs.add(class_) 49 return subs
Returns a set containing all child classes in the subclass graph of class_
.
I.e., includes subclasses of subclasses, etc.
Parameters
include_self
: Whether to includeclass_
itself in the returned setclass_
: Superclass
Development
Since most class hierarchies are small, the inefficiencies of the existing recursive implementation aren't problematic. It might be valuable to refactor with memoization if the need arises to use this function on a very large class hierarchy.
52def isinstance_by_type_name(o: object, type_name: str): 53 """Behaves like stdlib `isinstance` except it accepts a string representation of the type rather than the type itself. 54 This is a hacky function intended to circumvent the need to import a type into a module. 55 It is susceptible to type name collisions. 56 57 # Parameters 58 `o`: Object (not the type itself) whose type to interrogate 59 `type_name`: The string returned by `type_.__name__`. 60 Generic types are not supported, only types that would appear in `type_.__mro__`. 61 """ 62 return type_name in {s.__name__ for s in type(o).__mro__}
Behaves like stdlib isinstance
except it accepts a string representation of the type rather than the type itself.
This is a hacky function intended to circumvent the need to import a type into a module.
It is susceptible to type name collisions.
Parameters
o
: Object (not the type itself) whose type to interrogate
type_name
: The string returned by type_.__name__
.
Generic types are not supported, only types that would appear in type_.__mro__
.
69@runtime_checkable 70class IsDataclass(Protocol): 71 # Generic type for any dataclass instance 72 # https://stackoverflow.com/questions/54668000/type-hint-for-an-instance-of-a-non-specific-dataclass 73 __dataclass_fields__: ClassVar[dict[str, Any]]
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto[T](Protocol):
def meth(self) -> T:
...
1710def _no_init_or_replace_init(self, *args, **kwargs): 1711 cls = type(self) 1712 1713 if cls._is_protocol: 1714 raise TypeError('Protocols cannot be instantiated') 1715 1716 # Already using a custom `__init__`. No need to calculate correct 1717 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1718 if cls.__init__ is not _no_init_or_replace_init: 1719 return 1720 1721 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1722 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1723 # searches for a proper new `__init__` in the MRO. The new `__init__` 1724 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1725 # instantiation of the protocol subclass will thus use the new 1726 # `__init__` and no longer call `_no_init_or_replace_init`. 1727 for base in cls.__mro__: 1728 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1729 if init is not _no_init_or_replace_init: 1730 cls.__init__ = init 1731 break 1732 else: 1733 # should not happen 1734 cls.__init__ = object.__init__ 1735 1736 cls.__init__(self, *args, **kwargs)
76def get_hashable_eq_attrs(dc: IsDataclass) -> tuple[Any]: 77 """Returns a tuple of all fields used for equality comparison, including the type of the dataclass itself. 78 The type is included to preserve the unequal equality behavior of instances of different dataclasses whose fields are identical. 79 Essentially used to generate a hashable dataclass representation for equality comparison even if it's not frozen. 80 """ 81 return *( 82 getattr(dc, fld.name) 83 for fld in filter(lambda x: x.compare, dc.__dataclass_fields__.values()) 84 ), type(dc)
Returns a tuple of all fields used for equality comparison, including the type of the dataclass itself. The type is included to preserve the unequal equality behavior of instances of different dataclasses whose fields are identical. Essentially used to generate a hashable dataclass representation for equality comparison even if it's not frozen.
87def dataclass_set_equals( 88 coll1: Iterable[IsDataclass], coll2: Iterable[IsDataclass] 89) -> bool: 90 """Compares 2 collections of dataclass instances as if they were sets. 91 Duplicates are ignored in the same manner as a set. 92 Unfrozen dataclasses can't be placed in sets since they're not hashable. 93 Collections of them may be compared using this function. 94 """ 95 96 return {get_hashable_eq_attrs(x) for x in coll1} == { 97 get_hashable_eq_attrs(y) for y in coll2 98 }
Compares 2 collections of dataclass instances as if they were sets. Duplicates are ignored in the same manner as a set. Unfrozen dataclasses can't be placed in sets since they're not hashable. Collections of them may be compared using this function.