Coverage for src/typedal/helpers.py: 100%
80 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-02 16:17 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-02 16:17 +0200
1"""
2Helpers that work independently of core.
3"""
5import io
6import types
7import typing
8from collections import ChainMap
9from typing import Any
11from .types import AnyDict
13T = typing.TypeVar("T")
16def is_union(some_type: type | types.UnionType) -> bool:
17 """
18 Check if a type is some type of Union.
20 Args:
21 some_type: types.UnionType = type(int | str); typing.Union = typing.Union[int, str]
23 """
24 return typing.get_origin(some_type) in (types.UnionType, typing.Union)
27def _all_annotations(cls: type) -> ChainMap[str, type]:
28 """
29 Returns a dictionary-like ChainMap that includes annotations for all \
30 attributes defined in cls or inherited from superclasses.
31 """
32 return ChainMap(*(c.__annotations__ for c in getattr(cls, "__mro__", []) if "__annotations__" in c.__dict__))
35def all_dict(cls: type) -> AnyDict:
36 """
37 Get the internal data of a class and all it's parents.
38 """
39 return dict(ChainMap(*(c.__dict__ for c in getattr(cls, "__mro__", []))))
42def all_annotations(cls: type, _except: typing.Iterable[str] = None) -> dict[str, type]:
43 """
44 Wrapper around `_all_annotations` that filters away any keys in _except.
46 It also flattens the ChainMap to a regular dict.
47 """
48 if _except is None:
49 _except = set()
51 _all = _all_annotations(cls)
52 return {k: v for k, v in _all.items() if k not in _except}
55def instanciate(cls: typing.Type[T] | T, with_args: bool = False) -> T:
56 """
57 Create an instance of T (if it is a class).
59 If it already is an instance, return it.
60 If it is a generic (list[int)) create an instance of the 'origin' (-> list()).
62 If with_args: spread the generic args into the class creation
63 (needed for e.g. TypedField(str), but not for list[str])
64 """
65 if inner_cls := typing.get_origin(cls):
66 if not with_args:
67 return typing.cast(T, inner_cls())
69 args = typing.get_args(cls)
70 return typing.cast(T, inner_cls(*args))
72 if isinstance(cls, type):
73 return typing.cast(T, cls())
75 return cls
78def origin_is_subclass(obj: Any, _type: type) -> bool:
79 """
80 Check if the origin of a generic is a subclass of _type.
82 Example:
83 origin_is_subclass(list[str], list) -> True
84 """
85 return bool(
86 typing.get_origin(obj)
87 and isinstance(typing.get_origin(obj), type)
88 and issubclass(typing.get_origin(obj), _type)
89 )
92def mktable(
93 data: dict[Any, Any], header: typing.Optional[typing.Iterable[str] | range] = None, skip_first: bool = True
94) -> str:
95 """
96 Display a table for 'data'.
98 See Also:
99 https://stackoverflow.com/questions/70937491/python-flexible-way-to-format-string-output-into-a-table-without-using-a-non-st
100 """
101 # get max col width
102 col_widths: list[int] = list(map(max, zip(*(map(lambda x: len(str(x)), (k, *v)) for k, v in data.items()))))
104 # default numeric header if missing
105 if not header:
106 header = range(1, len(col_widths) + 1)
108 header_widths = map(lambda x: len(str(x)), header)
110 # correct column width if headers are longer
111 col_widths = [max(c, h) for c, h in zip(col_widths, header_widths)]
113 # create separator line
114 line = f"+{'+'.join('-' * (w + 2) for w in col_widths)}+"
116 # create formating string
117 fmt_str = "| %s |" % " | ".join(f"{{:<{i}}}" for i in col_widths)
119 output = io.StringIO()
120 # header
121 print()
122 print(line, file=output)
123 print(fmt_str.format(*header), file=output)
124 print(line, file=output)
126 # data
127 for k, v in data.items():
128 values = list(v.values())[1:] if skip_first else v.values()
129 print(fmt_str.format(k, *values), file=output)
131 # footer
132 print(line, file=output)
134 return output.getvalue()
137K = typing.TypeVar("K")
138V = typing.TypeVar("V")
141def looks_like(v: Any, _type: type[Any]) -> bool:
142 """
143 Returns true if v or v's class is of type _type, including if it is a generic.
145 Examples:
146 assert looks_like([], list)
147 assert looks_like(list, list)
148 assert looks_like(list[str], list)
149 """
150 return isinstance(v, _type) or (isinstance(v, type) and issubclass(v, _type)) or origin_is_subclass(v, _type)
153def filter_out(mut_dict: dict[K, V], _type: type[T]) -> dict[K, type[T]]:
154 """
155 Split a dictionary into things matching _type and the rest.
157 Modifies mut_dict and returns everything of type _type.
158 """
159 return {k: mut_dict.pop(k) for k, v in list(mut_dict.items()) if looks_like(v, _type)}
162def unwrap_type(_type: type) -> type:
163 """
164 Get the inner type of a generic.
166 Example:
167 list[list[str]] -> str
168 """
169 while args := typing.get_args(_type):
170 _type = args[0]
171 return _type
174@typing.overload
175def extract_type_optional(annotation: T) -> tuple[T, bool]:
176 """
177 T -> T is not exactly right because you'll get the inner type, but mypy seems happy with this.
178 """
181@typing.overload
182def extract_type_optional(annotation: None) -> tuple[None, bool]:
183 """
184 None leads to None, False.
185 """
188def extract_type_optional(annotation: T | None) -> tuple[T | None, bool]:
189 """
190 Given an annotation, extract the actual type and whether it is optional.
191 """
192 if annotation is None:
193 return None, False
195 if origin := typing.get_origin(annotation):
196 args = typing.get_args(annotation)
198 if origin in (typing.Union, types.UnionType, typing.Optional) and args:
199 # remove None:
200 return next(_ for _ in args if _ and _ != types.NoneType and not isinstance(_, types.NoneType)), True
202 return annotation, False
205def to_snake(camel: str) -> str:
206 """
207 Convert CamelCase to snake_case.
209 See Also:
210 https://stackoverflow.com/a/44969381
211 """
212 return "".join([f"_{c.lower()}" if c.isupper() else c for c in camel]).lstrip("_")
215class DummyQuery:
216 """
217 Placeholder to &= and |= actual query parts.
218 """
220 def __or__(self, other: T) -> T:
221 """
222 For 'or': DummyQuery | Other == Other.
223 """
224 return other
226 def __and__(self, other: T) -> T:
227 """
228 For 'and': DummyQuery & Other == Other.
229 """
230 return other
232 def __bool__(self) -> bool:
233 """
234 A dummy query is falsey, since it can't actually be used!
235 """
236 return False
239def as_lambda(value: T) -> typing.Callable[..., T]:
240 """
241 Wrap value in a callable.
242 """
243 return lambda *_, **__: value