Coverage for src/pystencilssfg/context.py: 97%

100 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-04 07:16 +0000

1from __future__ import annotations 

2from typing import Sequence, Any, Generator 

3from contextlib import contextmanager 

4 

5from .config import CodeStyle, ClangFormatOptions 

6from .ir import ( 

7 SfgSourceFile, 

8 SfgNamespace, 

9 SfgNamespaceBlock, 

10 SfgCodeEntity, 

11 SfgGlobalNamespace, 

12) 

13from .ir.syntax import SfgNamespaceElement 

14from .exceptions import SfgException 

15 

16 

17class SfgContext: 

18 """Manages context information during the execution of a generator script.""" 

19 

20 def __init__( 

21 self, 

22 header_file: SfgSourceFile, 

23 impl_file: SfgSourceFile | None, 

24 namespace: str | None = None, 

25 codestyle: CodeStyle | None = None, 

26 clang_format_opts: ClangFormatOptions | None = None, 

27 argv: Sequence[str] | None = None, 

28 project_info: Any = None, 

29 ): 

30 self._argv = argv 

31 self._project_info = project_info 

32 

33 self._outer_namespace = namespace 

34 self._inner_namespace: str | None = None 

35 

36 self._codestyle = codestyle if codestyle is not None else CodeStyle() 

37 self._clang_format: ClangFormatOptions = ( 

38 clang_format_opts if clang_format_opts is not None else ClangFormatOptions() 

39 ) 

40 

41 self._header_file = header_file 

42 self._impl_file = impl_file 

43 

44 self._global_namespace = SfgGlobalNamespace() 

45 

46 current_namespace: SfgNamespace 

47 if namespace is not None: 

48 current_namespace = self._global_namespace.get_child_namespace(namespace) 

49 else: 

50 current_namespace = self._global_namespace 

51 

52 self._cursor = SfgCursor(self, current_namespace) 

53 

54 @property 

55 def argv(self) -> Sequence[str]: 

56 """If this context was created by a `pystencilssfg.SourceFileGenerator`, provides the command 

57 line arguments given to the generator script, with configuration arguments for the code generator 

58 stripped away. 

59 

60 Otherwise, throws an exception.""" 

61 if self._argv is None: 

62 raise SfgException("This context provides no command-line arguments.") 

63 return self._argv 

64 

65 @property 

66 def project_info(self) -> Any: 

67 """Project-specific information provided by a build system.""" 

68 return self._project_info 

69 

70 @property 

71 def outer_namespace(self) -> str | None: 

72 """Outer code namespace. Set by constructor argument `outer_namespace`.""" 

73 return self._outer_namespace 

74 

75 @property 

76 def codestyle(self) -> CodeStyle: 

77 """The code style object for this generation context.""" 

78 return self._codestyle 

79 

80 @property 

81 def clang_format(self) -> ClangFormatOptions: 

82 return self._clang_format 

83 

84 @property 

85 def header_file(self) -> SfgSourceFile: 

86 return self._header_file 

87 

88 @property 

89 def impl_file(self) -> SfgSourceFile | None: 

90 return self._impl_file 

91 

92 @property 

93 def cursor(self) -> SfgCursor: 

94 return self._cursor 

95 

96 @property 

97 def files(self) -> Generator[SfgSourceFile, None, None]: 

98 yield self._header_file 

99 if self._impl_file is not None: 

100 yield self._impl_file 

101 

102 @property 

103 def global_namespace(self) -> SfgNamespace: 

104 return self._global_namespace 

105 

106 

107class SfgCursor: 

108 """Cursor that tracks the current location in the source file(s) during execution of the generator script.""" 

109 

110 def __init__(self, ctx: SfgContext, namespace: SfgNamespace) -> None: 

111 self._ctx = ctx 

112 

113 self._cur_namespace: SfgNamespace = namespace 

114 

115 self._loc: dict[SfgSourceFile, list[SfgNamespaceElement]] = dict() 

116 for f in self._ctx.files: 

117 if not isinstance(namespace, SfgGlobalNamespace): 

118 block = SfgNamespaceBlock( 

119 self._cur_namespace, self._cur_namespace.fqname 

120 ) 

121 f.elements.append(block) 

122 self._loc[f] = block.elements 

123 else: 

124 self._loc[f] = f.elements 

125 

126 @property 

127 def context(self) -> SfgContext: 

128 return self._ctx 

129 

130 @property 

131 def current_namespace(self) -> SfgNamespace: 

132 return self._cur_namespace 

133 

134 def get_entity(self, name: str) -> SfgCodeEntity | None: 

135 return self._cur_namespace.get_entity(name) 

136 

137 def add_entity(self, entity: SfgCodeEntity): 

138 self._cur_namespace.add_entity(entity) 

139 

140 def write_header(self, elem: SfgNamespaceElement) -> None: 

141 self._loc[self._ctx.header_file].append(elem) 

142 

143 def write_impl(self, elem: SfgNamespaceElement) -> None: 

144 impl_file = self._ctx.impl_file 

145 if impl_file is None: 

146 raise SfgException( 

147 f"Cannot write element {elem} to implemenation file since no implementation file is being generated." 

148 ) 

149 self._loc[impl_file].append(elem) 

150 

151 def enter_namespace(self, qual_name: str): 

152 namespace = self._cur_namespace.get_child_namespace(qual_name) 

153 

154 outer_locs = self._loc.copy() 

155 

156 for f in self._ctx.files: 

157 block = SfgNamespaceBlock(namespace, qual_name) 

158 self._loc[f].append(block) 

159 self._loc[f] = block.elements 

160 

161 outer_namespace = self._cur_namespace 

162 self._cur_namespace = namespace 

163 

164 @contextmanager 

165 def ctxmgr(): 

166 try: 

167 yield None 

168 finally: 

169 # Have the cursor step back out of the nested namespace blocks 

170 self._loc = outer_locs 

171 self._cur_namespace = outer_namespace 

172 

173 return ctxmgr()