docs for muutils v0.6.15
View Source on GitHub

muutils.misc

miscellaneous 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    "hashing",
53    "numerical",
54    "sequence",
55    "string",
56    # imports
57    "stable_hash",
58    "WhenMissing",
59    "empty_sequence_if_attr_false",
60    "flatten",
61    "list_split",
62    "list_join",
63    "apply_mapping",
64    "apply_mapping_chain",
65    "sanitize_name",
66    "sanitize_fname",
67    "sanitize_identifier",
68    "dict_to_filename",
69    "dynamic_docstring",
70    "shorten_numerical_to_str",
71    "str_to_numeric",
72    "_SHORTEN_MAP",
73    "FrozenDict",
74    "FrozenList",
75    "freeze",
76    "is_abstract",
77    "get_all_subclasses",
78    "isinstance_by_type_name",
79    "IsDataclass",
80    "get_hashable_eq_attrs",
81    "dataclass_set_equals",
82]

def stable_hash(s: str | bytes) -> int:
 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

WhenMissing = typing.Literal['except', 'skip', 'include']
def empty_sequence_if_attr_false(itr: Iterable[Any], attr_owner: Any, attr_name: str) -> Iterable[Any]:
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 is True.
  • attr_owner: Any The object to check for the attribute.
  • attr_name: str The name of the attribute to check.

Returns:

  • itr: Iterable if attr_owner has the attribute attr_name and it boolean casts to True, otherwise an empty sequence.
  • () an empty sequence if the attribute is False or not present.
def flatten(it: Iterable[Any], levels_to_flatten: int | None = None) -> Generator:
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. If None, performs full flattening.
def list_split(lst: list, val: Any) -> list[list]:
 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)
[[]]
def list_join(lst: list, factory: Callable) -> list:
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]
def apply_mapping( mapping: Mapping[~_AM_K, ~_AM_V], iter: Iterable[~_AM_K], when_missing: Literal['except', 'skip', 'include'] = 'skip') -> list[typing.Union[~_AM_K, ~_AM_V]]:
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 to
  • when_missing : WhenMissing what to do when a key is missing from the mapping -- this is what distinguishes this function from map you can choose from "skip", "include" (without converting), and "except" (defaults to "skip")

Returns:

return type is one of:

  • list[_AM_V] if when_missing is "skip" or "except"
  • list[Union[_AM_K, _AM_V]] if when_missing is "include"

Raises:

  • KeyError : if the item is missing from the mapping and when_missing is "except"
  • ValueError : if when_missing is invalid
def apply_mapping_chain( mapping: Mapping[~_AM_K, Iterable[~_AM_V]], iter: Iterable[~_AM_K], when_missing: Literal['except', 'skip', 'include'] = 'skip') -> list[typing.Union[~_AM_K, ~_AM_V]]:
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 returns Iterable[_AM_V]
  • iter : Iterable[_AM_K] the iterable to apply the mapping to
  • when_missing : WhenMissing what to do when a key is missing from the mapping -- this is what distinguishes this function from map you can choose from "skip", "include" (without converting), and "except" (defaults to "skip")

Returns:

return type is one of:

  • list[_AM_V] if when_missing is "skip" or "except"
  • list[Union[_AM_K, _AM_V]] if when_missing is "include"

Raises:

  • KeyError : if the item is missing from the mapping and when_missing is "except"
  • ValueError : if when_missing is invalid
def sanitize_name( name: str | None, additional_allowed_chars: str = '', replace_invalid: str = '', when_none: str | None = '_None_', leading_digit_prefix: str = '') -> str:
 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 string
  • additional_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 if name is None. if None, 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
def sanitize_fname(fname: str | None, **kwargs) -> str:
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)
def sanitize_identifier(fname: str | None, **kwargs) -> str:
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
def dict_to_filename( data: dict, format_str: str = '{key}_{val}', separator: str = '.', max_length: int = 255):
 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)}"
def dynamic_docstring(**doc_params):
103def dynamic_docstring(**doc_params):
104    def decorator(func):
105        if func.__doc__:
106            func.__doc__ = func.__doc__.format(**doc_params)
107        return func
108
109    return decorator
def shorten_numerical_to_str( num: int | float, small_as_decimal: bool = True, precision: int = 1) -> 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

def str_to_numeric( quantity: str, mapping: None | bool | dict[str, int | float] = True) -> int | float:
 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
_SHORTEN_MAP = {1000.0: 'K', 1000000.0: 'M', 1000000000.0: 'B', 1000000000000.0: 't', 1000000000000000.0: 'q', 1e+18: 'Q'}
class FrozenDict(builtins.dict):
 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
class FrozenList(builtins.list):
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.

def append(self, value):
20    def append(self, value):
21        raise AttributeError("list is frozen")

Append object to the end of the list.

def extend(self, iterable):
23    def extend(self, iterable):
24        raise AttributeError("list is frozen")

Extend list by appending elements from the iterable.

def insert(self, index, value):
26    def insert(self, index, value):
27        raise AttributeError("list is frozen")

Insert object before index.

def remove(self, value):
29    def remove(self, value):
30        raise AttributeError("list is frozen")

Remove first occurrence of value.

Raises ValueError if the value is not present.

def pop(self, index=-1):
32    def pop(self, index=-1):
33        raise AttributeError("list is frozen")

Remove and return item at index (default last).

Raises IndexError if list is empty or index is out of range.

def clear(self):
35    def clear(self):
36        raise AttributeError("list is frozen")

Remove all items from list.

Inherited Members
builtins.list
list
copy
index
count
reverse
sort
def freeze(instance: object) -> object:
 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

def is_abstract(cls: type) -> bool:
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.

def get_all_subclasses(class_: type, include_self=False) -> set[type]:
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 include class_ itself in the returned set
  • class_: 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.

def isinstance_by_type_name(o: object, type_name: str):
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__.

@runtime_checkable
class IsDataclass(typing.Protocol):
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:
        ...
IsDataclass(*args, **kwargs)
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)
def get_hashable_eq_attrs(dc: IsDataclass) -> tuple[typing.Any]:
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.

def dataclass_set_equals( coll1: Iterable[IsDataclass], coll2: Iterable[IsDataclass]) -> bool:
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.