Coverage for tdom / format.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-03 21:23 +0000

1import typing as t 

2from collections.abc import Callable, Sequence 

3from string.templatelib import Interpolation, Template 

4 

5 

6@t.overload 

7def convert[T](value: T, conversion: None) -> T: ... 

8 

9 

10@t.overload 

11def convert(value: object, conversion: t.Literal["a", "r", "s"]) -> str: ... 

12 

13 

14def convert[T](value: T, conversion: t.Literal["a", "r", "s"] | None) -> T | str: 

15 """ 

16 Convert a value according to the given conversion specifier. 

17 

18 In the future, something like this should probably ship with Python itself. 

19 """ 

20 if conversion == "a": 

21 return ascii(value) 

22 elif conversion == "r": 

23 return repr(value) 

24 elif conversion == "s": 

25 return str(value) 

26 else: 

27 return value 

28 

29 

30type FormatMatcher = Callable[[str], bool] 

31"""A predicate function that returns True if a given format specifier matches its criteria.""" 

32 

33 

34V = t.TypeVar("V", bound=object) 

35 

36 

37type CustomFormatter[V] = Callable[[V, str], object] 

38"""A function that takes a value and a format specifier and returns a formatted string.""" 

39 

40type MatcherAndFormatter = tuple[str | FormatMatcher, CustomFormatter] 

41""" 

42A pair of a matcher and its corresponding formatter. 

43 

44The matcher is used to determine if the formatter should be applied to a given  

45format specifier. If the matcher is a string, it must exactly match the format 

46specifier. If it is a FormatMatcher, it is called with the format specifier and 

47should return True if the formatter should be used. 

48""" 

49 

50 

51def _matcher_matches(matcher: str | FormatMatcher, format_spec: str) -> bool: 

52 """Check if a matcher matches a given format specifier.""" 

53 return matcher == format_spec if isinstance(matcher, str) else matcher(format_spec) 

54 

55 

56def _format_interpolation( 

57 value: object, 

58 format_spec: str, 

59 conversion: t.Literal["a", "r", "s"] | None, 

60 *, 

61 formatters: Sequence[MatcherAndFormatter], 

62) -> object: 

63 converted = convert(value, conversion) 

64 if format_spec: 

65 for matcher, formatter in formatters: 

66 if _matcher_matches(matcher, format_spec): 

67 return formatter(converted, format_spec) 

68 return format(converted, format_spec) 

69 return converted 

70 

71 

72def format_interpolation( 

73 interpolation: Interpolation, 

74 *, 

75 formatters: Sequence[MatcherAndFormatter] = (), 

76) -> object: 

77 """ 

78 Format an Interpolation's value according to its format spec and conversion. 

79 

80 PEP 750 allows t-string processing code to decide whether, and how, to 

81 interpret format specifiers. This function takes an optional sequence of 

82 (matcher, formatter) pairs. If a matcher returns True for the given format 

83 spec, the corresponding formatter is used to format the value. If no 

84 matchers match, the default formatting behavior is used. 

85 

86 Conversions are always applied before formatting. 

87 """ 

88 return _format_interpolation( 

89 interpolation.value, 

90 interpolation.format_spec, 

91 interpolation.conversion, 

92 formatters=formatters, 

93 ) 

94 

95 

96def format_template(template: Template) -> str: 

97 """Fully render a template by formatting its interpolations.""" 

98 parts: list[str] = [] 

99 for part in template: 

100 if isinstance(part, str): 

101 parts.append(part) 

102 else: 

103 parts.append(str(format_interpolation(part))) 

104 return "".join(parts)