Coverage for tdom/placeholders.py: 100%

51 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-19 21:37 +0000

1import random 

2import re 

3import string 

4from dataclasses import dataclass, field 

5 

6from .template_utils import TemplateRef 

7 

8 

9def make_placeholder_config() -> PlaceholderConfig: 

10 prefix = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-" 

11 suffix = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t" 

12 return PlaceholderConfig( 

13 prefix=prefix, 

14 suffix=suffix, 

15 pattern=re.compile(re.escape(prefix) + r"(\d+)" + re.escape(suffix)), 

16 ) 

17 

18 

19@dataclass(frozen=True) 

20class PlaceholderConfig: 

21 """String operations for working with a placeholder pattern.""" 

22 

23 prefix: str 

24 suffix: str 

25 pattern: re.Pattern 

26 

27 def make_placeholder(self, i: int) -> str: 

28 """Generate a placeholder for the i-th interpolation.""" 

29 return f"{self.prefix}{i}{self.suffix}" 

30 

31 def match_placeholders(self, s: str) -> list[re.Match[str]]: 

32 """Find all placeholders in a string.""" 

33 return list(self.pattern.finditer(s)) 

34 

35 def find_placeholders(self, s: str) -> TemplateRef: 

36 """ 

37 Find all placeholders in a string and return a TemplateRef. 

38 

39 If no placeholders are found, returns a literal TemplateRef. 

40 """ 

41 matches = self.match_placeholders(s) 

42 if not matches: 

43 return TemplateRef.literal(s) 

44 

45 strings: list[str] = [] 

46 indexes: list[int] = [] 

47 last_index = 0 

48 for match in matches: 

49 start, end = match.span() 

50 strings.append(s[last_index:start]) 

51 indexes.append(int(match[1])) 

52 last_index = end 

53 strings.append(s[last_index:]) 

54 

55 i_start = indexes[0] 

56 if indexes != list(range(i_start, i_start + len(indexes))): 

57 raise ValueError("TemplateRef interpolation indexes must be contiguous.") 

58 return TemplateRef(tuple(strings), i_start) 

59 

60 

61@dataclass 

62class PlaceholderState: 

63 known: set[int] = field(default_factory=set) 

64 config: PlaceholderConfig = field(default_factory=make_placeholder_config) 

65 """Collection of currently 'known and active' placeholder indexes.""" 

66 

67 @property 

68 def is_empty(self) -> bool: 

69 return len(self.known) == 0 

70 

71 def add_placeholder(self, index: int) -> str: 

72 placeholder = self.config.make_placeholder(index) 

73 self.known.add(index) 

74 return placeholder 

75 

76 def remove_placeholders(self, text: str) -> TemplateRef: 

77 """ 

78 Find all known placeholders in the text and return their indices. 

79 

80 If unknown placeholders are found, raises ValueError. 

81 

82 If no placeholders are found, returns a literal TemplateRef. 

83 """ 

84 pt = self.config.find_placeholders(text) 

85 for index in range(pt.i_start, pt.i_stop): 

86 if index not in self.known: 

87 raise ValueError(f"Unknown placeholder index {index} found in text.") 

88 self.known.remove(index) 

89 return pt