Coverage for src/configuraptor/helpers.py: 100%
47 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-09-18 12:33 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-09-18 12:33 +0200
1"""
2Contains stand-alone helper functions.
3"""
4import dataclasses as dc
5import math
6import os
7import types
8import typing
9from collections import ChainMap
11import black.files
12from typeguard import TypeCheckError
13from typeguard import check_type as _check_type
15from .abs import T_typelike
18def camel_to_snake(s: str) -> str:
19 """
20 Convert CamelCase to snake_case.
22 Source:
23 https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
24 """
25 return "".join([f"_{c.lower()}" if c.isupper() else c for c in s]).lstrip("_")
28def find_pyproject_toml() -> typing.Optional[str]:
29 """
30 Find the project's config toml, looks up until it finds the project root (black's logic).
31 """
32 return black.files.find_pyproject_toml((os.getcwd(),))
35Type = typing.Type[typing.Any]
38def _all_annotations(cls: Type) -> ChainMap[str, Type]:
39 """
40 Returns a dictionary-like ChainMap that includes annotations for all \
41 attributes defined in cls or inherited from superclasses.
42 """
43 return ChainMap(*(c.__annotations__ for c in getattr(cls, "__mro__", []) if "__annotations__" in c.__dict__))
46def all_annotations(cls: Type, _except: typing.Iterable[str] = None) -> dict[str, type[object]]:
47 """
48 Wrapper around `_all_annotations` that filters away any keys in _except.
50 It also flattens the ChainMap to a regular dict.
51 """
52 if _except is None:
53 _except = set()
55 _all = _all_annotations(cls)
56 return {k: v for k, v in _all.items() if k not in _except}
59def check_type(value: typing.Any, expected_type: T_typelike) -> bool:
60 """
61 Given a variable, check if it matches 'expected_type' (which can be a Union, parameterized generic etc.).
63 Based on typeguard but this returns a boolean instead of returning the value or throwing a TypeCheckError
64 """
65 try:
66 _check_type(value, expected_type)
67 return True
68 except TypeCheckError:
69 return False
72def is_builtin_type(_type: Type) -> bool:
73 """
74 Returns whether _type is one of the builtin types.
75 """
76 return _type.__module__ in ("__builtin__", "builtins")
79# def is_builtin_class_instance(obj: typing.Any) -> bool:
80# return is_builtin_type(obj.__class__)
83def is_from_types_or_typing(_type: Type) -> bool:
84 """
85 Returns whether _type is one of the stlib typing/types types.
87 e.g. types.UnionType or typing.Union
88 """
89 return _type.__module__ in ("types", "typing")
92def is_from_other_toml_supported_module(_type: Type) -> bool:
93 """
94 Besides builtins, toml also supports 'datetime' and 'math' types, \
95 so this returns whether _type is a type from these stdlib modules.
96 """
97 return _type.__module__ in ("datetime", "math")
100def is_parameterized(_type: Type) -> bool:
101 """
102 Returns whether _type is a parameterized type.
104 Examples:
105 list[str] -> True
106 str -> False
107 """
108 return typing.get_origin(_type) is not None
111def is_custom_class(_type: Type) -> bool:
112 """
113 Tries to guess if _type is a builtin or a custom (user-defined) class.
115 Other logic in this module depends on knowing that.
116 """
117 return (
118 type(_type) is type
119 and not is_builtin_type(_type)
120 and not is_from_other_toml_supported_module(_type)
121 and not is_from_types_or_typing(_type)
122 )
125def instance_of_custom_class(var: typing.Any) -> bool:
126 """
127 Calls `is_custom_class` on an instance of a (possibly custom) class.
128 """
129 return is_custom_class(var.__class__)
132def is_optional(_type: Type | typing.Any) -> bool:
133 """
134 Tries to guess if _type could be optional.
136 Examples:
137 None -> True
138 NoneType -> True
139 typing.Union[str, None] -> True
140 str | None -> True
141 list[str | None] -> False
142 list[str] -> False
143 """
144 if _type and (is_parameterized(_type) and typing.get_origin(_type) in (dict, list)) or (_type is math.nan):
145 # e.g. list[str]
146 # will crash issubclass to test it first here
147 return False
149 return (
150 _type is None
151 or types.NoneType in typing.get_args(_type) # union with Nonetype
152 or issubclass(types.NoneType, _type)
153 or issubclass(types.NoneType, type(_type)) # no type # Nonetype
154 )
157def dataclass_field(cls: Type, key: str) -> typing.Optional[dc.Field[typing.Any]]:
158 """
159 Get Field info for a dataclass cls.
160 """
161 fields = getattr(cls, "__dataclass_fields__", {})
162 return fields.get(key)