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

1""" 

2Contains stand-alone helper functions. 

3""" 

4import dataclasses as dc 

5import math 

6import os 

7import types 

8import typing 

9from collections import ChainMap 

10 

11import black.files 

12from typeguard import TypeCheckError 

13from typeguard import check_type as _check_type 

14 

15from .abs import T_typelike 

16 

17 

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

19 """ 

20 Convert CamelCase to snake_case. 

21 

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

26 

27 

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

33 

34 

35Type = typing.Type[typing.Any] 

36 

37 

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

44 

45 

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. 

49 

50 It also flattens the ChainMap to a regular dict. 

51 """ 

52 if _except is None: 

53 _except = set() 

54 

55 _all = _all_annotations(cls) 

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

57 

58 

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

62 

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 

70 

71 

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

77 

78 

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

80# return is_builtin_type(obj.__class__) 

81 

82 

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

84 """ 

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

86 

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

88 """ 

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

90 

91 

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

98 

99 

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

101 """ 

102 Returns whether _type is a parameterized type. 

103 

104 Examples: 

105 list[str] -> True 

106 str -> False 

107 """ 

108 return typing.get_origin(_type) is not None 

109 

110 

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

112 """ 

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

114 

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 ) 

123 

124 

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

130 

131 

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

133 """ 

134 Tries to guess if _type could be optional. 

135 

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 

148 

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 ) 

155 

156 

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)