Coverage for bookbuilderpy/types.py: 91%

46 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-17 23:15 +0000

1"""Some basic type handling routines.""" 

2from typing import Any, Final, Iterable 

3 

4 

5def type_name(tpe: type) -> str: 

6 """ 

7 Convert a type to a string. 

8 

9 :param tpe: the type 

10 :returns: the string 

11 

12 >>> type_name(type(None)) 

13 'None' 

14 >>> type_name(int) 

15 'int' 

16 >>> from bookbuilderpy.path import Path 

17 >>> type_name(Path) 

18 'bookbuilderpy.path.Path' 

19 """ 

20 c1: str = str(tpe) 

21 if c1.startswith("<class '"): 

22 c1 = c1[8:-2] 

23 if c1 == "NoneType": 

24 return "None" 

25 

26 if hasattr(tpe, "__qualname__"): 

27 c2: str = tpe.__qualname__ 

28 if hasattr(tpe, "__module__"): 

29 module = tpe.__module__ 

30 if (module is not None) and (module != "builtins"): 

31 c2 = f"{module}.{c2}" 

32 if len(c2) > len(c1): 

33 return c2 

34 return c1 

35 

36 

37def type_name_of(obj) -> str: 

38 """ 

39 Get the fully-qualified class name of an object. 

40 

41 :param obj: the object 

42 :returns: the fully-qualified class name of the object 

43 

44 >>> from bookbuilderpy.path import Path 

45 >>> type_name_of(Path.path("/tmp")) 

46 'bookbuilderpy.path.Path' 

47 """ 

48 if obj is None: 

49 return "None" 

50 c1: Final[str] = type_name(type(obj)) 

51 if hasattr(obj, "__class__"): 

52 cls: Final[type] = obj.__class__ 

53 c2: str = type_name(cls) 

54 

55 if hasattr(cls, "__qualname__"): 

56 c3: str = cls.__qualname__ 

57 if hasattr(obj, "__module__"): 

58 module = obj.__module__ 

59 if (module is not None) and (module != "builtins"): 

60 c3 = f"{module}.{c3}" 

61 if len(c3) > len(c2): 

62 c2 = c3 

63 

64 if len(c2) > len(c1): 

65 return c2 

66 return c1 

67 

68 

69def type_error(obj: Any, 

70 name: str, 

71 expected: None | type | Iterable[type] = None, 

72 call: bool = False) -> ValueError | TypeError: 

73 """ 

74 Create an error to raise if a type did not fit. 

75 

76 :param obj: the object that is of the wrong type 

77 :param name: the name of the object 

78 :param expected: the expected types (or `None`) 

79 :param call: the object should have been callable? 

80 :returns: a :class:`TypeError` with a descriptive information 

81 

82 >>> type_error(1.3, "var", int) 

83 TypeError("var should be an instance of int but is float, namely '1.3'.") 

84 >>> type_error("x", "z", (int, float)).args[0] 

85 "z should be an instance of any in {float, int} but is str, namely 'x'." 

86 >>> type_error("f", "q", call=True).args[0] 

87 "q should be a callable but is str, namely 'f'." 

88 >>> type_error("1", "2", bool, call=True).args[0] 

89 "2 should be an instance of bool or a callable but is str, namely '1'." 

90 >>> type_error(None, "x", str) 

91 TypeError('x should be an instance of str but is None.') 

92 """ 

93 exp: str = "" 

94 if isinstance(expected, Iterable): 

95 exp = ", ".join(sorted([type_name(e) for e in expected])) 

96 exp = f"an instance of any in {{{exp}}}" 

97 elif expected is not None: 

98 exp = f"an instance of {type_name(expected)}" 

99 if call: 

100 exp = f"{exp} or a callable" if exp else "a callable" 

101 

102 if obj is None: 

103 return TypeError(f"{name} should be {exp} but is None.") 

104 return TypeError(f"{name} should be {exp} but is " 

105 f"{type_name_of(obj)}, namely '{obj}'.")