Coverage for src/configuraptor/helpers.py: 100%
67 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-20 11:43 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-20 11:43 +0100
1"""
2Contains stand-alone helper functions.
3"""
4import contextlib
5import dataclasses as dc
6import io
7import math
8import os
9import types
10import typing
11from collections import ChainMap
12from pathlib import Path
14import black.files
15from typeguard import TypeCheckError
16from typeguard import check_type as _check_type
18# from .abs import T_typelike
21def camel_to_snake(s: str) -> str:
22 """
23 Convert CamelCase to snake_case.
25 Source:
26 https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
27 """
28 return "".join([f"_{c.lower()}" if c.isupper() else c for c in s]).lstrip("_")
31def find_pyproject_toml() -> typing.Optional[str]:
32 """
33 Find the project's config toml, looks up until it finds the project root (black's logic).
34 """
35 return black.files.find_pyproject_toml((os.getcwd(),))
38Type = typing.Type[typing.Any]
41def _all_annotations(cls: Type) -> ChainMap[str, Type]:
42 """
43 Returns a dictionary-like ChainMap that includes annotations for all \
44 attributes defined in cls or inherited from superclasses.
45 """
46 return ChainMap(*(c.__annotations__ for c in getattr(cls, "__mro__", []) if "__annotations__" in c.__dict__))
49def all_annotations(cls: Type, _except: typing.Iterable[str] = None) -> dict[str, type[object]]:
50 """
51 Wrapper around `_all_annotations` that filters away any keys in _except.
53 It also flattens the ChainMap to a regular dict.
54 """
55 if _except is None:
56 _except = set()
58 _all = _all_annotations(cls)
59 return {k: v for k, v in _all.items() if k not in _except}
62T = typing.TypeVar("T")
65def check_type(value: typing.Any, expected_type: typing.Type[T]) -> typing.TypeGuard[T]:
66 """
67 Given a variable, check if it matches 'expected_type' (which can be a Union, parameterized generic etc.).
69 Based on typeguard but this returns a boolean instead of returning the value or throwing a TypeCheckError
70 """
71 try:
72 _check_type(value, expected_type)
73 return True
74 except TypeCheckError:
75 return False
78def is_builtin_type(_type: Type) -> bool:
79 """
80 Returns whether _type is one of the builtin types.
81 """
82 return _type.__module__ in ("__builtin__", "builtins")
85# def is_builtin_class_instance(obj: typing.Any) -> bool:
86# return is_builtin_type(obj.__class__)
89def is_from_types_or_typing(_type: Type) -> bool:
90 """
91 Returns whether _type is one of the stlib typing/types types.
93 e.g. types.UnionType or typing.Union
94 """
95 return _type.__module__ in ("types", "typing")
98def is_from_other_toml_supported_module(_type: Type) -> bool:
99 """
100 Besides builtins, toml also supports 'datetime' and 'math' types, \
101 so this returns whether _type is a type from these stdlib modules.
102 """
103 return _type.__module__ in ("datetime", "math")
106def is_parameterized(_type: Type) -> bool:
107 """
108 Returns whether _type is a parameterized type.
110 Examples:
111 list[str] -> True
112 str -> False
113 """
114 return typing.get_origin(_type) is not None
117def is_custom_class(_type: Type) -> bool:
118 """
119 Tries to guess if _type is a builtin or a custom (user-defined) class.
121 Other logic in this module depends on knowing that.
122 """
123 return (
124 type(_type) is type
125 and not is_builtin_type(_type)
126 and not is_from_other_toml_supported_module(_type)
127 and not is_from_types_or_typing(_type)
128 )
131def instance_of_custom_class(var: typing.Any) -> bool:
132 """
133 Calls `is_custom_class` on an instance of a (possibly custom) class.
134 """
135 return is_custom_class(var.__class__)
138def is_optional(_type: Type | typing.Any) -> bool:
139 """
140 Tries to guess if _type could be optional.
142 Examples:
143 None -> True
144 NoneType -> True
145 typing.Union[str, None] -> True
146 str | None -> True
147 list[str | None] -> False
148 list[str] -> False
149 """
150 if _type and (is_parameterized(_type) and typing.get_origin(_type) in (dict, list)) or (_type is math.nan):
151 # e.g. list[str]
152 # will crash issubclass to test it first here
153 return False
155 return (
156 _type is None
157 or types.NoneType in typing.get_args(_type) # union with Nonetype
158 or issubclass(types.NoneType, _type)
159 or issubclass(types.NoneType, type(_type)) # no type # Nonetype
160 )
163def dataclass_field(cls: Type, key: str) -> typing.Optional[dc.Field[typing.Any]]:
164 """
165 Get Field info for a dataclass cls.
166 """
167 fields = getattr(cls, "__dataclass_fields__", {})
168 return fields.get(key)
171@contextlib.contextmanager
172def uncloseable(fd: typing.BinaryIO) -> typing.Generator[typing.BinaryIO, typing.Any, None]:
173 """
174 Context manager which turns the fd's close operation to no-op for the duration of the context.
175 """
176 close = fd.close
177 fd.close = lambda: None # type: ignore
178 yield fd
179 fd.close = close # type: ignore
182def as_binaryio(file: str | Path | typing.BinaryIO | None, mode: typing.Literal["rb", "wb"] = "rb") -> typing.BinaryIO:
183 """
184 Convert a number of possible 'file' descriptions into a single BinaryIO interface.
185 """
186 if isinstance(file, str):
187 file = Path(file)
188 if isinstance(file, Path):
189 file = file.open(mode)
190 if file is None:
191 file = io.BytesIO()
192 if isinstance(file, io.BytesIO):
193 # so .read() works after .write():
194 file.seek(0)
195 # so the with-statement doesn't close the in-memory file:
196 file = uncloseable(file) # type: ignore
198 return file