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

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 

13 

14import black.files 

15from typeguard import TypeCheckError 

16from typeguard import check_type as _check_type 

17 

18# from .abs import T_typelike 

19 

20 

21def camel_to_snake(s: str) -> str: 

22 """ 

23 Convert CamelCase to snake_case. 

24 

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("_") 

29 

30 

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(),)) 

36 

37 

38Type = typing.Type[typing.Any] 

39 

40 

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__)) 

47 

48 

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. 

52 

53 It also flattens the ChainMap to a regular dict. 

54 """ 

55 if _except is None: 

56 _except = set() 

57 

58 _all = _all_annotations(cls) 

59 return {k: v for k, v in _all.items() if k not in _except} 

60 

61 

62T = typing.TypeVar("T") 

63 

64 

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.). 

68 

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 

76 

77 

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") 

83 

84 

85# def is_builtin_class_instance(obj: typing.Any) -> bool: 

86# return is_builtin_type(obj.__class__) 

87 

88 

89def is_from_types_or_typing(_type: Type) -> bool: 

90 """ 

91 Returns whether _type is one of the stlib typing/types types. 

92 

93 e.g. types.UnionType or typing.Union 

94 """ 

95 return _type.__module__ in ("types", "typing") 

96 

97 

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") 

104 

105 

106def is_parameterized(_type: Type) -> bool: 

107 """ 

108 Returns whether _type is a parameterized type. 

109 

110 Examples: 

111 list[str] -> True 

112 str -> False 

113 """ 

114 return typing.get_origin(_type) is not None 

115 

116 

117def is_custom_class(_type: Type) -> bool: 

118 """ 

119 Tries to guess if _type is a builtin or a custom (user-defined) class. 

120 

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 ) 

129 

130 

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__) 

136 

137 

138def is_optional(_type: Type | typing.Any) -> bool: 

139 """ 

140 Tries to guess if _type could be optional. 

141 

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 

154 

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 ) 

161 

162 

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) 

169 

170 

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 

180 

181 

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 

197 

198 return file