Coverage for src/griffe/_internal/agents/inspector.py: 90.39%

265 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 13:44 +0200

1# This module contains our dynamic analysis agent, 

2# capable of inspecting modules and objects in memory, at runtime. 

3 

4from __future__ import annotations 

5 

6import ast 

7import functools 

8import sys 

9import types 

10import typing 

11from inspect import Parameter as SignatureParameter 

12from inspect import Signature, cleandoc, getsourcelines, unwrap 

13from inspect import signature as getsignature 

14from typing import TYPE_CHECKING, Any 

15 

16from griffe._internal.agents.nodes.runtime import ObjectNode 

17from griffe._internal.collections import LinesCollection, ModulesCollection 

18from griffe._internal.enumerations import Kind, ParameterKind, TypeParameterKind 

19from griffe._internal.expressions import Expr, ExprBinOp, ExprSubscript, ExprTuple, safe_get_annotation 

20from griffe._internal.extensions.base import Extensions, load_extensions 

21from griffe._internal.importer import dynamic_import 

22from griffe._internal.logger import logger 

23from griffe._internal.models import ( 

24 Alias, 

25 Attribute, 

26 Class, 

27 Docstring, 

28 Function, 

29 Module, 

30 Parameter, 

31 Parameters, 

32 TypeAlias, 

33 TypeParameter, 

34 TypeParameters, 

35) 

36 

37if TYPE_CHECKING: 

38 from collections.abc import Sequence 

39 from pathlib import Path 

40 

41 from griffe._internal.docstrings.parsers import DocstringStyle 

42 from griffe._internal.enumerations import Parser 

43 

44_TYPING_MODULES: tuple[types.ModuleType, ...] 

45try: 

46 import typing_extensions 

47except ImportError: 

48 _TYPING_MODULES = (typing,) 

49else: 

50 _TYPING_MODULES = (typing, typing_extensions) 

51 

52_empty = Signature.empty 

53 

54 

55def inspect( 

56 module_name: str, 

57 *, 

58 filepath: Path | None = None, 

59 import_paths: Sequence[str | Path] | None = None, 

60 extensions: Extensions | None = None, 

61 parent: Module | None = None, 

62 docstring_parser: DocstringStyle | Parser | None = None, 

63 docstring_options: dict[str, Any] | None = None, 

64 lines_collection: LinesCollection | None = None, 

65 modules_collection: ModulesCollection | None = None, 

66) -> Module: 

67 """Inspect a module. 

68 

69 Sometimes we cannot get the source code of a module or an object, 

70 typically built-in modules like `itertools`. 

71 The only way to know what they are made of is to actually import them and inspect their contents. 

72 

73 Sometimes, even if the source code is available, 

74 loading the object is desired because it was created or modified dynamically, 

75 and our static agent is not powerful enough to infer all these dynamic modifications. 

76 In this case, we load the module using introspection. 

77 

78 Griffe therefore provides this function for dynamic analysis. 

79 It uses a [`NodeVisitor`][ast.NodeVisitor]-like class, the [`Inspector`][griffe.Inspector], 

80 to inspect the module with [`inspect.getmembers()`][inspect.getmembers]. 

81 

82 The inspection agent works similarly to the regular [`Visitor`][griffe.Visitor] agent, 

83 in that it maintains a state with the current object being handled, and recursively handle its members. 

84 

85 Important: 

86 This function is generally not used directly. 

87 In most cases, users can rely on the [`GriffeLoader`][griffe.GriffeLoader] 

88 and its accompanying [`load`][griffe.load] shortcut and their respective options 

89 to load modules using dynamic analysis. 

90 

91 Parameters: 

92 module_name: The module name (as when importing [from] it). 

93 filepath: The module file path. 

94 import_paths: Paths to import the module from. 

95 extensions: The extensions to use when inspecting the module. 

96 parent: The optional parent of this module. 

97 docstring_parser: The docstring parser to use. By default, no parsing is done. 

98 docstring_options: Additional docstring parsing options. 

99 lines_collection: A collection of source code lines. 

100 modules_collection: A collection of modules. 

101 

102 Returns: 

103 The module, with its members populated. 

104 """ 

105 return Inspector( 

106 module_name, 

107 filepath, 

108 extensions or load_extensions(), 

109 parent, 

110 docstring_parser=docstring_parser, 

111 docstring_options=docstring_options, 

112 lines_collection=lines_collection, 

113 modules_collection=modules_collection, 

114 ).get_module(import_paths) 

115 

116 

117class Inspector: 

118 """This class is used to instantiate an inspector. 

119 

120 Inspectors iterate on objects members to extract data from them. 

121 """ 

122 

123 def __init__( 

124 self, 

125 module_name: str, 

126 filepath: Path | None, 

127 extensions: Extensions, 

128 parent: Module | None = None, 

129 docstring_parser: DocstringStyle | Parser | None = None, 

130 docstring_options: dict[str, Any] | None = None, 

131 lines_collection: LinesCollection | None = None, 

132 modules_collection: ModulesCollection | None = None, 

133 ) -> None: 

134 """Initialize the inspector. 

135 

136 Parameters: 

137 module_name: The module name. 

138 filepath: The optional filepath. 

139 extensions: Extensions to use when inspecting. 

140 parent: The module parent. 

141 docstring_parser: The docstring parser to use. 

142 docstring_options: The docstring parsing options. 

143 lines_collection: A collection of source code lines. 

144 modules_collection: A collection of modules. 

145 """ 

146 super().__init__() 

147 

148 self.module_name: str = module_name 

149 """The module name.""" 

150 

151 self.filepath: Path | None = filepath 

152 """The module file path.""" 

153 

154 self.extensions: Extensions = extensions 

155 """The extensions to use when inspecting.""" 

156 

157 self.parent: Module | None = parent 

158 """An optional parent for the final module object.""" 

159 

160 self.current: Module | Class = None # type: ignore[assignment] 

161 """The current object being inspected.""" 

162 

163 self.docstring_parser: DocstringStyle | Parser | None = docstring_parser 

164 """The docstring parser to use.""" 

165 

166 self.docstring_options: dict[str, Any] = docstring_options or {} 

167 """The docstring parsing options.""" 

168 

169 self.lines_collection: LinesCollection = lines_collection or LinesCollection() 

170 """A collection of source code lines.""" 

171 

172 self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() 

173 """A collection of modules.""" 

174 

175 def _get_docstring(self, node: ObjectNode) -> Docstring | None: 

176 try: 

177 # Access `__doc__` directly to avoid taking the `__doc__` attribute from a parent class. 

178 value = getattr(node.obj, "__doc__", None) 

179 except Exception: # noqa: BLE001 

180 return None 

181 if value is None: 

182 return None 

183 try: 

184 # We avoid `inspect.getdoc` to avoid getting 

185 # the `__doc__` attribute from a parent class, 

186 # but we still want to clean the doc. 

187 cleaned = cleandoc(value) 

188 except AttributeError: 

189 # Triggered on method descriptors. 

190 return None 

191 return Docstring( 

192 cleaned, 

193 parser=self.docstring_parser, 

194 parser_options=self.docstring_options, 

195 ) 

196 

197 def _get_linenos(self, node: ObjectNode) -> tuple[int, int] | tuple[None, None]: 

198 # Line numbers won't be useful if we don't have the source code. 

199 if not self.filepath or self.filepath not in self.lines_collection: 

200 return None, None 

201 try: 

202 lines, lineno = getsourcelines(node.obj) 

203 except (OSError, TypeError): 

204 return None, None 

205 return lineno, lineno + "".join(lines).rstrip().count("\n") 

206 

207 def get_module(self, import_paths: Sequence[str | Path] | None = None) -> Module: 

208 """Build and return the object representing the module attached to this inspector. 

209 

210 This method triggers a complete inspection of the module members. 

211 

212 Parameters: 

213 import_paths: Paths replacing `sys.path` to import the module. 

214 

215 Returns: 

216 A module instance. 

217 """ 

218 import_path = self.module_name 

219 if self.parent is not None: 

220 import_path = f"{self.parent.path}.{import_path}" 

221 

222 # Make sure `import_paths` is a list, in case we want to `insert` into it. 

223 import_paths = list(import_paths or ()) 

224 

225 # If the thing we want to import has a filepath, 

226 # we make sure to insert the right parent directory 

227 # at the front of our list of import paths. 

228 # We do this by counting the number of dots `.` in the import path, 

229 # corresponding to slashes `/` in the filesystem, 

230 # and go up in the file tree the same number of times. 

231 if self.filepath: 

232 parent_path = self.filepath.parent 

233 for _ in range(import_path.count(".")): 

234 parent_path = parent_path.parent 

235 # Climb up one more time for `__init__` modules. 

236 if self.filepath.stem == "__init__": 

237 parent_path = parent_path.parent 

238 if parent_path not in import_paths: 

239 import_paths.insert(0, parent_path) 

240 

241 value = dynamic_import(import_path, import_paths) 

242 

243 # We successfully imported the given object, 

244 # and we now create the object tree with all the necessary nodes, 

245 # from the root of the package to this leaf object. 

246 parent_node = None 

247 if self.parent is not None: 

248 for part in self.parent.path.split("."): 

249 parent_node = ObjectNode(None, name=part, parent=parent_node) 

250 module_node = ObjectNode(value, self.module_name, parent=parent_node) 

251 

252 self.inspect(module_node) 

253 return self.current.module 

254 

255 def inspect(self, node: ObjectNode) -> None: 

256 """Extend the base inspection with extensions. 

257 

258 Parameters: 

259 node: The node to inspect. 

260 """ 

261 getattr(self, f"inspect_{node.kind}", self.generic_inspect)(node) 

262 

263 def generic_inspect(self, node: ObjectNode) -> None: 

264 """Extend the base generic inspection with extensions. 

265 

266 Parameters: 

267 node: The node to inspect. 

268 """ 

269 for child in node.children: 

270 if target_path := child.alias_target_path: 

271 # If the child is an actual submodule of the current module, 

272 # and has no `__file__` set, we won't find it on the disk so we must inspect it now. 

273 # For that we instantiate a new inspector and use it to inspect the submodule, 

274 # then assign the submodule as member of the current module. 

275 # If the submodule has a `__file__` set, the loader should find it on the disk, 

276 # so we skip it here (no member, no alias, just skip it). 

277 if child.is_module and target_path == f"{self.current.path}.{child.name}": 

278 if not hasattr(child.obj, "__file__"): 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true

279 logger.debug("Module %s is not discoverable on disk, inspecting right now", target_path) 

280 inspector = Inspector( 

281 child.name, 

282 filepath=None, 

283 parent=self.current.module, 

284 extensions=self.extensions, 

285 docstring_parser=self.docstring_parser, 

286 docstring_options=self.docstring_options, 

287 lines_collection=self.lines_collection, 

288 modules_collection=self.modules_collection, 

289 ) 

290 inspector.inspect_module(child) 

291 self.current.set_member(child.name, inspector.current.module) 

292 # Otherwise, alias the object. 

293 else: 

294 alias = Alias(child.name, target_path) 

295 self.current.set_member(child.name, alias) 

296 self.extensions.call("on_alias", alias=alias, node=node, agent=self) 

297 else: 

298 self.inspect(child) 

299 

300 def inspect_module(self, node: ObjectNode) -> None: 

301 """Inspect a module. 

302 

303 Parameters: 

304 node: The node to inspect. 

305 """ 

306 self.extensions.call("on_node", node=node, agent=self) 

307 self.extensions.call("on_module_node", node=node, agent=self) 

308 self.current = module = Module( 

309 name=self.module_name, 

310 filepath=self.filepath, 

311 parent=self.parent, 

312 docstring=self._get_docstring(node), 

313 lines_collection=self.lines_collection, 

314 modules_collection=self.modules_collection, 

315 ) 

316 self.extensions.call("on_instance", node=node, obj=module, agent=self) 

317 self.extensions.call("on_module_instance", node=node, mod=module, agent=self) 

318 self.generic_inspect(node) 

319 self.extensions.call("on_members", node=node, obj=module, agent=self) 

320 self.extensions.call("on_module_members", node=node, mod=module, agent=self) 

321 

322 def inspect_class(self, node: ObjectNode) -> None: 

323 """Inspect a class. 

324 

325 Parameters: 

326 node: The node to inspect. 

327 """ 

328 self.extensions.call("on_node", node=node, agent=self) 

329 self.extensions.call("on_class_node", node=node, agent=self) 

330 

331 bases = [] 

332 for base in node.obj.__bases__: 

333 if base is object: 

334 continue 

335 bases.append(f"{base.__module__}.{base.__qualname__}") 

336 

337 lineno, endlineno = self._get_linenos(node) 

338 class_ = Class( 

339 name=node.name, 

340 docstring=self._get_docstring(node), 

341 bases=bases, 

342 type_parameters=TypeParameters(*_convert_type_parameters(node.obj, parent=self.current, member=node.name)), 

343 lineno=lineno, 

344 endlineno=endlineno, 

345 ) 

346 self.current.set_member(node.name, class_) 

347 self.current = class_ 

348 self.extensions.call("on_instance", node=node, obj=class_, agent=self) 

349 self.extensions.call("on_class_instance", node=node, cls=class_, agent=self) 

350 self.generic_inspect(node) 

351 self.extensions.call("on_members", node=node, obj=class_, agent=self) 

352 self.extensions.call("on_class_members", node=node, cls=class_, agent=self) 

353 self.current = self.current.parent # type: ignore[assignment] 

354 

355 def inspect_staticmethod(self, node: ObjectNode) -> None: 

356 """Inspect a static method. 

357 

358 Parameters: 

359 node: The node to inspect. 

360 """ 

361 self.handle_function(node, {"staticmethod"}) 

362 

363 def inspect_classmethod(self, node: ObjectNode) -> None: 

364 """Inspect a class method. 

365 

366 Parameters: 

367 node: The node to inspect. 

368 """ 

369 self.handle_function(node, {"classmethod"}) 

370 

371 def inspect_method_descriptor(self, node: ObjectNode) -> None: 

372 """Inspect a method descriptor. 

373 

374 Parameters: 

375 node: The node to inspect. 

376 """ 

377 self.handle_function(node, {"method descriptor"}) 

378 

379 def inspect_builtin_method(self, node: ObjectNode) -> None: 

380 """Inspect a builtin method. 

381 

382 Parameters: 

383 node: The node to inspect. 

384 """ 

385 self.handle_function(node, {"builtin"}) 

386 

387 def inspect_method(self, node: ObjectNode) -> None: 

388 """Inspect a method. 

389 

390 Parameters: 

391 node: The node to inspect. 

392 """ 

393 self.handle_function(node) 

394 

395 def inspect_coroutine(self, node: ObjectNode) -> None: 

396 """Inspect a coroutine. 

397 

398 Parameters: 

399 node: The node to inspect. 

400 """ 

401 self.handle_function(node, {"async"}) 

402 

403 def inspect_builtin_function(self, node: ObjectNode) -> None: 

404 """Inspect a builtin function. 

405 

406 Parameters: 

407 node: The node to inspect. 

408 """ 

409 self.handle_function(node, {"builtin"}) 

410 

411 def inspect_function(self, node: ObjectNode) -> None: 

412 """Inspect a function. 

413 

414 Parameters: 

415 node: The node to inspect. 

416 """ 

417 self.handle_function(node) 

418 

419 def inspect_cached_property(self, node: ObjectNode) -> None: 

420 """Inspect a cached property. 

421 

422 Parameters: 

423 node: The node to inspect. 

424 """ 

425 self.handle_function(node, {"cached", "property"}) 

426 

427 def inspect_property(self, node: ObjectNode) -> None: 

428 """Inspect a property. 

429 

430 Parameters: 

431 node: The node to inspect. 

432 """ 

433 self.handle_function(node, {"property"}) 

434 

435 def inspect_getset_descriptor(self, node: ObjectNode) -> None: 

436 """Inspect a get/set descriptor. 

437 

438 Parameters: 

439 node: The node to inspect. 

440 """ 

441 self.handle_function(node, {"property"}) 

442 

443 def handle_function(self, node: ObjectNode, labels: set | None = None) -> None: 

444 """Handle a function. 

445 

446 Parameters: 

447 node: The node to inspect. 

448 labels: Labels to add to the data object. 

449 """ 

450 self.extensions.call("on_node", node=node, agent=self) 

451 self.extensions.call("on_function_node", node=node, agent=self) 

452 

453 try: 

454 signature = getsignature(node.obj) 

455 except Exception: # noqa: BLE001 

456 # So many exceptions can be raised here: 

457 # AttributeError, NameError, RuntimeError, ValueError, TokenError, TypeError... 

458 parameters = None 

459 returns = None 

460 else: 

461 parameters = Parameters( 

462 *[ 

463 _convert_parameter(parameter, parent=self.current, member=node.name) 

464 for parameter in signature.parameters.values() 

465 ], 

466 ) 

467 return_annotation = signature.return_annotation 

468 returns = ( 

469 None 

470 if return_annotation is _empty 

471 else _convert_object_to_annotation(return_annotation, parent=self.current, member=node.name) 

472 ) 

473 

474 lineno, endlineno = self._get_linenos(node) 

475 

476 obj: Attribute | Function 

477 labels = labels or set() 

478 if "property" in labels: 

479 obj = Attribute( 

480 name=node.name, 

481 value=None, 

482 annotation=returns, 

483 docstring=self._get_docstring(node), 

484 lineno=lineno, 

485 endlineno=endlineno, 

486 ) 

487 else: 

488 obj = Function( 

489 name=node.name, 

490 parameters=parameters, 

491 returns=returns, 

492 type_parameters=TypeParameters( 

493 *_convert_type_parameters(node.obj, parent=self.current, member=node.name), 

494 ), 

495 docstring=self._get_docstring(node), 

496 lineno=lineno, 

497 endlineno=endlineno, 

498 ) 

499 obj.labels |= labels 

500 self.current.set_member(node.name, obj) 

501 self.extensions.call("on_instance", node=node, obj=obj, agent=self) 

502 if obj.is_attribute: 

503 self.extensions.call("on_attribute_instance", node=node, attr=obj, agent=self) 

504 else: 

505 self.extensions.call("on_function_instance", node=node, func=obj, agent=self) 

506 

507 def inspect_type_alias(self, node: ObjectNode) -> None: 

508 """Inspect a type alias. 

509 

510 Parameters: 

511 node: The node to inspect. 

512 """ 

513 self.extensions.call("on_node", node=node, agent=self) 

514 self.extensions.call("on_type_alias_node", node=node, agent=self) 

515 

516 lineno, endlineno = self._get_linenos(node) 

517 

518 type_alias = TypeAlias( 

519 name=node.name, 

520 value=_convert_type_to_annotation(node.obj.__value__, parent=self.current, member=node.name), 

521 lineno=lineno, 

522 endlineno=endlineno, 

523 type_parameters=TypeParameters(*_convert_type_parameters(node.obj, parent=self.current, member=node.name)), 

524 docstring=self._get_docstring(node), 

525 parent=self.current, 

526 ) 

527 self.current.set_member(node.name, type_alias) 

528 self.extensions.call("on_instance", node=node, obj=type_alias, agent=self) 

529 self.extensions.call("on_type_alias_instance", node=node, type_alias=type_alias, agent=self) 

530 

531 def inspect_attribute(self, node: ObjectNode) -> None: 

532 """Inspect an attribute. 

533 

534 Parameters: 

535 node: The node to inspect. 

536 """ 

537 self.handle_attribute(node) 

538 

539 def handle_attribute(self, node: ObjectNode, annotation: str | Expr | None = None) -> None: 

540 """Handle an attribute. 

541 

542 Parameters: 

543 node: The node to inspect. 

544 annotation: A potential annotation. 

545 """ 

546 self.extensions.call("on_node", node=node, agent=self) 

547 self.extensions.call("on_attribute_node", node=node, agent=self) 

548 

549 # TODO: To improve. 

550 parent = self.current 

551 labels: set[str] = set() 

552 

553 if parent.kind is Kind.MODULE: 

554 labels.add("module-attribute") 

555 elif parent.kind is Kind.CLASS: 555 ↛ 557line 555 didn't jump to line 557 because the condition on line 555 was always true

556 labels.add("class-attribute") 

557 elif parent.kind is Kind.FUNCTION: 

558 if parent.name != "__init__": 

559 return 

560 parent = parent.parent # type: ignore[assignment] 

561 labels.add("instance-attribute") 

562 

563 try: 

564 value = repr(node.obj) 

565 except Exception: # noqa: BLE001 

566 value = None 

567 try: 

568 docstring = self._get_docstring(node) 

569 except Exception: # noqa: BLE001 

570 docstring = None 

571 

572 attribute = Attribute( 

573 name=node.name, 

574 value=value, 

575 annotation=annotation, 

576 docstring=docstring, 

577 ) 

578 attribute.labels |= labels 

579 parent.set_member(node.name, attribute) 

580 

581 if node.name == "__all__": 

582 parent.exports = list(node.obj) 

583 self.extensions.call("on_instance", node=node, obj=attribute, agent=self) 

584 self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self) 

585 

586 

587_parameter_kind_map = { 

588 SignatureParameter.POSITIONAL_ONLY: ParameterKind.positional_only, 

589 SignatureParameter.POSITIONAL_OR_KEYWORD: ParameterKind.positional_or_keyword, 

590 SignatureParameter.VAR_POSITIONAL: ParameterKind.var_positional, 

591 SignatureParameter.KEYWORD_ONLY: ParameterKind.keyword_only, 

592 SignatureParameter.VAR_KEYWORD: ParameterKind.var_keyword, 

593} 

594 

595 

596def _convert_parameter( 

597 parameter: SignatureParameter, 

598 *, 

599 parent: Module | Class, 

600 member: str | None = None, 

601) -> Parameter: 

602 name = parameter.name 

603 annotation = ( 

604 None 

605 if parameter.annotation is _empty 

606 else _convert_object_to_annotation(parameter.annotation, parent=parent, member=member) 

607 ) 

608 kind = _parameter_kind_map[parameter.kind] 

609 if parameter.default is _empty: 

610 default = None 

611 elif hasattr(parameter.default, "__name__"): 

612 # Avoid `repr` containing chevrons and memory addresses. 

613 default = parameter.default.__name__ 

614 else: 

615 default = repr(parameter.default) 

616 return Parameter(name, annotation=annotation, kind=kind, default=default) 

617 

618 

619def _convert_object_to_annotation(obj: Any, *, parent: Module | Class, member: str | None = None) -> str | Expr | None: 

620 # Even when *we* import future annotations, 

621 # the object from which we get a signature 

622 # can come from modules which did *not* import them, 

623 # so `inspect.signature` returns actual Python objects 

624 # that we must deal with. 

625 if isinstance(obj, str): 

626 annotation = obj 

627 else: 

628 # Always give precedence to the object's representation... 

629 obj_repr = repr(obj) 

630 if hasattr(obj, "__name__"): # noqa: SIM108 

631 # ...unless it contains chevrons (which likely means it's a class), 

632 # in which case we use the object's name. 

633 annotation = obj.__name__ if "<" in obj_repr else obj_repr 

634 else: 

635 annotation = obj_repr 

636 try: 

637 annotation_node = compile(annotation, mode="eval", filename="<>", flags=ast.PyCF_ONLY_AST, optimize=2) 

638 except SyntaxError: 

639 return obj 

640 return safe_get_annotation(annotation_node.body, parent, member=member) # type: ignore[attr-defined] 

641 

642 

643_type_parameter_kind_map = { 

644 getattr(module, attr): value 

645 for attr, value in { 

646 "TypeVar": TypeParameterKind.type_var, 

647 "TypeVarTuple": TypeParameterKind.type_var_tuple, 

648 "ParamSpec": TypeParameterKind.param_spec, 

649 }.items() 

650 for module in _TYPING_MODULES 

651 if hasattr(module, attr) 

652} 

653 

654 

655def _convert_type_parameters( 

656 obj: Any, 

657 *, 

658 parent: Module | Class, 

659 member: str | None = None, 

660) -> list[TypeParameter]: 

661 obj = unwrap(obj) 

662 

663 if not hasattr(obj, "__type_params__"): 

664 return [] 

665 

666 type_parameters = [] 

667 for type_parameter in obj.__type_params__: 

668 bound = getattr(type_parameter, "__bound__", None) 

669 if bound is not None: 

670 bound = _convert_type_to_annotation(bound, parent=parent, member=member) 

671 constraints: list[str | Expr] = [ 

672 _convert_type_to_annotation(constraint, parent=parent, member=member) # type: ignore[misc] 

673 for constraint in getattr(type_parameter, "__constraints__", ()) 

674 ] 

675 

676 if getattr(type_parameter, "has_default", lambda: False)(): 

677 default = _convert_type_to_annotation( 

678 type_parameter.__default__, 

679 parent=parent, 

680 member=member, 

681 ) 

682 else: 

683 default = None 

684 

685 type_parameters.append( 

686 TypeParameter( 

687 type_parameter.__name__, 

688 kind=_type_parameter_kind_map[type(type_parameter)], 

689 bound=bound, 

690 constraints=constraints or None, 

691 default=default, 

692 ), 

693 ) 

694 

695 return type_parameters 

696 

697 

698def _convert_type_to_annotation(obj: Any, *, parent: Module | Class, member: str | None = None) -> str | Expr | None: 

699 origin = typing.get_origin(obj) 

700 

701 if origin is None: 

702 return _convert_object_to_annotation(obj, parent=parent, member=member) 

703 

704 args: Sequence[str | Expr | None] = [ 

705 _convert_type_to_annotation(arg, parent=parent, member=member) for arg in typing.get_args(obj) 

706 ] 

707 

708 # YORE: EOL 3.9: Replace block with lines 2-3. 

709 if sys.version_info >= (3, 10): 709 ↛ 713line 709 didn't jump to line 713 because the condition on line 709 was always true

710 if origin is types.UnionType: 710 ↛ 711line 710 didn't jump to line 711 because the condition on line 710 was never true

711 return functools.reduce(lambda left, right: ExprBinOp(left, "|", right), args) # type: ignore[arg-type] 

712 

713 origin = _convert_type_to_annotation(origin, parent=parent, member=member) 

714 if origin is None: 714 ↛ 715line 714 didn't jump to line 715 because the condition on line 714 was never true

715 return None 

716 

717 return ExprSubscript(origin, ExprTuple(args, implicit=True)) # type: ignore[arg-type]