Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / codegen / source / composition.py: 90%

107 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Compose nested AutoModel sources into one generated modeling unit.""" 

16 

17from __future__ import annotations 

18 

19import ast 

20import importlib.util 

21import keyword 

22import os 

23import re 

24from typing import Any 

25 

26 

27def compose_nested_model_sources(source_text: str, meta: Any) -> str: 

28 """Inline source modules that own boundaries below a nested model root. 

29 

30 The native model remains unchanged. This is an artifact-only composition: 

31 it replaces a nested ``AutoModel.from_config`` construction with the 

32 observed concrete class and appends that class's original source module to 

33 the same generated file before normal TP/EP lowering runs. 

34 

35 Args: 

36 source_text: Primary model source. 

37 meta: Frozen codegen metadata carrying observed nested components. 

38 

39 Returns: 

40 One source string containing the primary and nested model definitions. 

41 """ 

42 components = getattr(meta, "nested_components", None) or () 

43 if not components: 

44 return source_text 

45 rendered = source_text 

46 included_modules: set[str] = set() 

47 for component in components: 

48 identity = component.get("type") 

49 if not isinstance(identity, str) or ":" not in identity: 

50 raise ValueError(f"invalid nested component identity: {identity!r}") 

51 module_name, class_name = identity.split(":", 1) 

52 class_name = class_name.rsplit(".", 1)[-1] 

53 replaced = _replace_auto_model_construction( 

54 rendered, 

55 component_fqn=str(component.get("fqn") or ""), 

56 class_name=class_name, 

57 ) 

58 if replaced is None: 

59 continue 

60 rendered = replaced 

61 if module_name not in included_modules: 

62 nested_source = _read_module_source(module_name) 

63 rendered = _append_source_module(rendered, nested_source, module_name) 

64 included_modules.add(module_name) 

65 return rendered 

66 

67 

68def _read_module_source(module_name: str) -> str: 

69 """Read an installed module without importing its runtime dependencies.""" 

70 spec = importlib.util.find_spec(module_name) 

71 path = None if spec is None else spec.origin 

72 if not path or not os.path.isfile(path): 

73 raise FileNotFoundError( 

74 f"codegen: nested model source {module_name!r} is not readable" 

75 ) 

76 with open(path, "r", encoding="utf-8", errors="replace") as handle: 

77 return handle.read() 

78 

79 

80def _append_source_module(primary: str, nested: str, module_name: str) -> str: 

81 """Append one nested module after removing module-only syntax.""" 

82 tree = ast.parse(nested, filename=module_name) 

83 package = module_name.rpartition(".")[0] 

84 tree = _AbsoluteImports(package).visit(tree) 

85 ast.fix_missing_locations(tree) 

86 primary_tree = ast.parse(primary) 

87 primary_bindings = _top_level_definitions(primary_tree) 

88 nested_bindings = _top_level_definitions(tree) 

89 collisions = { 

90 name for name in primary_bindings.keys() & nested_bindings.keys() 

91 if not isinstance(nested_bindings[name], ast.ClassDef) 

92 } 

93 if collisions: 

94 replacements = {name: f"_hp_nested_{name}" for name in collisions} 

95 for node in tree.body: 

96 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): 

97 node.name = replacements.get(node.name, node.name) 

98 tree = _RenameBindings(replacements).visit(tree) 

99 ast.fix_missing_locations(tree) 

100 nodes = [ 

101 node for node in tree.body 

102 if not isinstance(node, ast.Expr) or not _is_docstring(node) 

103 ] 

104 nodes = [ 

105 node for node in nodes 

106 if not isinstance(node, ast.ImportFrom) or node.module != "__future__" 

107 ] 

108 body = "\n\n".join(ast.unparse(node) for node in nodes) 

109 marker = ( 

110 "# [HYPER NESTED MODEL] source merged from " 

111 f"{module_name}; TP/EP lowering below targets these concrete classes." 

112 ) 

113 return f"{primary.rstrip()}\n\n\n{marker}\n{body}\n" 

114 

115 

116def _top_level_definitions(tree: ast.Module) -> dict[str, ast.AST]: 

117 """Return named functions, classes, and simple assignments.""" 

118 result: dict[str, ast.AST] = {} 

119 for node in tree.body: 

120 if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): 

121 result[node.name] = node 

122 elif isinstance(node, (ast.Assign, ast.AnnAssign)): 

123 targets = node.targets if isinstance(node, ast.Assign) else (node.target,) 

124 for target in targets: 

125 if isinstance(target, ast.Name): 

126 result[target.id] = node 

127 return result 

128 

129 

130def _is_docstring(node: ast.Expr) -> bool: 

131 return isinstance(node.value, ast.Constant) and isinstance(node.value.value, str) 

132 

133 

134class _RenameBindings(ast.NodeTransformer): 

135 """Rename colliding nested-module globals and their references.""" 

136 

137 def __init__(self, names: dict[str, str]) -> None: 

138 """Create a renamer for the supplied module-global bindings.""" 

139 self._names = names 

140 

141 def visit_Name(self, node: ast.Name) -> ast.AST: # pylint: disable=invalid-name 

142 """Rename a matching load or store while preserving its AST context.""" 

143 replacement = self._names.get(node.id) 

144 if replacement is None: 

145 return node 

146 return ast.copy_location(ast.Name(id=replacement, ctx=node.ctx), node) 

147 

148 

149class _AbsoluteImports(ast.NodeTransformer): 

150 """Anchor nested-module relative imports on their original package.""" 

151 

152 def __init__(self, package: str) -> None: 

153 """Create a transformer for one installed source package.""" 

154 self._package = package 

155 

156 def visit_ImportFrom( # pylint: disable=invalid-name 

157 self, 

158 node: ast.ImportFrom, 

159 ) -> ast.AST | list[ast.AST]: 

160 """Convert one relative import into its installed absolute module.""" 

161 if node.level == 0: 

162 return node 

163 relative = "." * node.level + (node.module or "") 

164 module = importlib.util.resolve_name(relative, self._package) 

165 if not all(part.isidentifier() and not keyword.iskeyword(part) for part in module.split(".")): 

166 return self._dynamic_imports(node, module) 

167 return ast.copy_location( 

168 ast.ImportFrom(module=module, names=node.names, level=0), 

169 node, 

170 ) 

171 

172 @staticmethod 

173 def _dynamic_imports(node: ast.ImportFrom, module: str) -> list[ast.AST]: 

174 """Render imports whose dynamic package contains non-Python identifiers.""" 

175 assignments = [] 

176 for alias in node.names: 

177 if alias.name == "*": 

178 raise ValueError( 

179 f"codegen cannot compose a wildcard import from dynamic module {module!r}" 

180 ) 

181 imported = ast.Call( 

182 func=ast.Name(id="__import__", ctx=ast.Load()), 

183 args=[ast.Constant(module)], 

184 keywords=[ast.keyword(arg="fromlist", value=ast.List( 

185 elts=[ast.Constant(alias.name)], 

186 ctx=ast.Load(), 

187 ))], 

188 ) 

189 assignment = ast.Assign( 

190 targets=[ast.Name(id=alias.asname or alias.name, ctx=ast.Store())], 

191 value=ast.Attribute(value=imported, attr=alias.name, ctx=ast.Load()), 

192 ) 

193 assignments.append(ast.copy_location(assignment, node)) 

194 return assignments 

195 

196 

197def _replace_auto_model_construction( 

198 source_text: str, 

199 *, 

200 component_fqn: str, 

201 class_name: str, 

202) -> str | None: 

203 """Replace an observed AutoModel construction, or reject the candidate.""" 

204 attribute = component_fqn.rsplit(".", 1)[-1] 

205 if not attribute: 

206 raise ValueError("nested component FQN must not be empty") 

207 pattern = re.compile( 

208 rf"(?m)^(?P<indent>\s*)self\.{re.escape(attribute)}\s*=\s*" 

209 r"AutoModel\.from_config\((?P<config>[^\n]+)\)\s*$" 

210 ) 

211 replacement = rf"\g<indent>self.{attribute} = {class_name}(\g<config>)" 

212 rendered, count = pattern.subn(replacement, source_text, count=1) 

213 return rendered if count == 1 else None 

214 

215 

216__all__ = ["compose_nested_model_sources"]