Coverage for src/configuraptor/helpers.py: 100%
67 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-07 15:36 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2023-11-07 15:36 +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
18from .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}
62def check_type(value: typing.Any, expected_type: T_typelike) -> bool:
63 """
64 Given a variable, check if it matches 'expected_type' (which can be a Union, parameterized generic etc.).
66 Based on typeguard but this returns a boolean instead of returning the value or throwing a TypeCheckError
67 """
68 try:
69 _check_type(value, expected_type)
70 return True
71 except TypeCheckError:
72 return False
75def is_builtin_type(_type: Type) -> bool:
76 """
77 Returns whether _type is one of the builtin types.
78 """
79 return _type.__module__ in ("__builtin__", "builtins")
82# def is_builtin_class_instance(obj: typing.Any) -> bool:
83# return is_builtin_type(obj.__class__)
86def is_from_types_or_typing(_type: Type) -> bool:
87 """
88 Returns whether _type is one of the stlib typing/types types.
90 e.g. types.UnionType or typing.Union
91 """
92 return _type.__module__ in ("types", "typing")
95def is_from_other_toml_supported_module(_type: Type) -> bool:
96 """
97 Besides builtins, toml also supports 'datetime' and 'math' types, \
98 so this returns whether _type is a type from these stdlib modules.
99 """
100 return _type.__module__ in ("datetime", "math")
103def is_parameterized(_type: Type) -> bool:
104 """
105 Returns whether _type is a parameterized type.
107 Examples:
108 list[str] -> True
109 str -> False
110 """
111 return typing.get_origin(_type) is not None
114def is_custom_class(_type: Type) -> bool:
115 """
116 Tries to guess if _type is a builtin or a custom (user-defined) class.
118 Other logic in this module depends on knowing that.
119 """
120 return (
121 type(_type) is type
122 and not is_builtin_type(_type)
123 and not is_from_other_toml_supported_module(_type)
124 and not is_from_types_or_typing(_type)
125 )
128def instance_of_custom_class(var: typing.Any) -> bool:
129 """
130 Calls `is_custom_class` on an instance of a (possibly custom) class.
131 """
132 return is_custom_class(var.__class__)
135def is_optional(_type: Type | typing.Any) -> bool:
136 """
137 Tries to guess if _type could be optional.
139 Examples:
140 None -> True
141 NoneType -> True
142 typing.Union[str, None] -> True
143 str | None -> True
144 list[str | None] -> False
145 list[str] -> False
146 """
147 if _type and (is_parameterized(_type) and typing.get_origin(_type) in (dict, list)) or (_type is math.nan):
148 # e.g. list[str]
149 # will crash issubclass to test it first here
150 return False
152 return (
153 _type is None
154 or types.NoneType in typing.get_args(_type) # union with Nonetype
155 or issubclass(types.NoneType, _type)
156 or issubclass(types.NoneType, type(_type)) # no type # Nonetype
157 )
160def dataclass_field(cls: Type, key: str) -> typing.Optional[dc.Field[typing.Any]]:
161 """
162 Get Field info for a dataclass cls.
163 """
164 fields = getattr(cls, "__dataclass_fields__", {})
165 return fields.get(key)
168@contextlib.contextmanager
169def uncloseable(fd: typing.BinaryIO) -> typing.Generator[typing.BinaryIO, typing.Any, None]:
170 """
171 Context manager which turns the fd's close operation to no-op for the duration of the context.
172 """
173 close = fd.close
174 fd.close = lambda: None # type: ignore
175 yield fd
176 fd.close = close # type: ignore
179def as_binaryio(file: str | Path | typing.BinaryIO | None, mode: typing.Literal["rb", "wb"] = "rb") -> typing.BinaryIO:
180 """
181 Convert a number of possible 'file' descriptions into a single BinaryIO interface.
182 """
183 if isinstance(file, str):
184 file = Path(file)
185 if isinstance(file, Path):
186 file = file.open(mode)
187 if file is None:
188 file = io.BytesIO()
189 if isinstance(file, io.BytesIO):
190 # so .read() works after .write():
191 file.seek(0)
192 # so the with-statement doesn't close the in-memory file:
193 file = uncloseable(file) # type: ignore
195 return file