Coverage for gws-app/gws/spec/generator/parser.py: 88%

434 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Parse Python source files into spec types.""" 

2 

3import ast 

4import re 

5 

6from typing import cast 

7 

8from . import base, util 

9 

10 

11def parse(gen: base.Generator, parse_all=False): 

12 """Parse the Python files of all chunks and add their types to the generator. 

13 

14 Modules containing ``# gws:nospec`` are skipped. 

15 

16 Args: 

17 gen: Generator state. 

18 parse_all: If True, parse all methods with all arguments, otherwise 

19 only command methods with their last argument. 

20 

21 Raises: 

22 ``GeneratorError``: If a file cannot be parsed. 

23 """ 

24 

25 _init_parser(gen) 

26 

27 for chunk in gen.chunks: 

28 for path in chunk.paths.get('python', []): 

29 _parse_path(gen, path, chunk.name, chunk.sourceDir, parse_all) 

30 

31 

32def _init_parser(gen: base.Generator): 

33 for b in base.v.BUILTINS: 

34 gen.add_type(c=base.c.ATOM, name=b) 

35 

36 

37def _parse_path(gen: base.Generator, path: str, base_name: str, base_dir: str, parse_all=True): 

38 pp = None 

39 

40 base.log.debug(f'parsing {path=}') 

41 

42 try: 

43 # <base_dir>/a/b/__init__.py => <base_name>.a.b 

44 # <base_dir>/a/b/c.py => <base_name>.a.b.c 

45 

46 if not path.startswith(base_dir): 

47 raise ValueError(f'invalid path {path!r}') 

48 p = path[len(base_dir) :].split('/') 

49 f = p.pop().split(DOT)[0] 

50 if f != '__init__': 

51 p.append(f) 

52 mod_name = base_name + DOT.join(p) 

53 

54 text = util.read_file(path) 

55 pp = _PythonParser(gen, mod_name, path, text, parse_all) 

56 pp.run() 

57 

58 except Exception as exc: 

59 lineno = '?' 

60 if pp and pp.context: 

61 lineno = pp.context[-1].lineno 

62 msg = str(exc.args[0]) if hasattr(exc, 'args') else repr(exc) 

63 raise base.GeneratorError(f'parse error: {msg} in {path}:{lineno}') 

64 

65 

66## 

67 

68 

69class _PythonParser: 

70 """Parser for a single Python module. 

71 

72 Adds types for the module, its type aliases and constants, its classes 

73 and enums with their properties, ``gws.ext`` declarations and decorated 

74 classes and command methods. Only names starting with an uppercase letter 

75 are considered types. 

76 """ 

77 

78 lines: list[str] 

79 """Not used.""" 

80 moduleNode: ast.Module 

81 """Root node of the module.""" 

82 moduleName: str 

83 """Qualified module name.""" 

84 docs: dict[int, str] 

85 """Not used.""" 

86 imports: dict[str, str] 

87 """Imported names mapped to qualified names.""" 

88 

89 def __init__(self, gen: base.Generator, module_name: str, path: str, text: str, parse_all: bool): 

90 self.gen = gen 

91 self.moduleName = module_name 

92 self.modulePath = path 

93 self.tModule = '' 

94 self.text = text 

95 self.source_lines = [''] + self.text.splitlines() 

96 self.is_init = path.endswith('__init__.py') 

97 self.context: list = [] 

98 self.parseAll = parse_all 

99 

100 def run(self): 

101 """Parse the module. 

102 

103 Raises: 

104 ``ValueError``: If the module contains an invalid declaration. 

105 """ 

106 

107 if any('# gws:nospec' in ln for ln in self.source_lines): 

108 return 

109 

110 tree = ast.parse(self.text) 

111 

112 for node in ast.walk(tree): 

113 if _cls(node) == 'Module': 

114 self.moduleNode = cast(ast.Module, node) 

115 break 

116 else: 

117 raise ValueError('module node not found') 

118 

119 typ = self.add( 

120 c=base.c.MODULE, 

121 name=self.moduleName, 

122 modPath=self.modulePath, 

123 doc=self.inner_doc(self.moduleNode), 

124 ) 

125 self.tModule = typ.uid 

126 

127 self.imports = self.prepare_imports() 

128 

129 for node in self.nodes(self.moduleNode.body): 

130 cc = _cls(node) 

131 if cc == 'Expr': 

132 self.parse_ext_declaration(node) 

133 elif cc == 'ClassDef': 

134 self.parse_class(node) 

135 elif cc in {'Assign', 'AnnAssign'}: 

136 self.parse_assign( 

137 node, 

138 self.outer_doc(node, self.moduleNode.body), 

139 annotated=(cc == 'AnnAssign'), 

140 ) 

141 

142 def prepare_imports(self): 

143 """Map imported names to qualified names and register aliases for imported types. 

144 

145 Returns: 

146 A dict of imported names and their qualified names. 

147 """ 

148 

149 # map import names to module names 

150 imp = {} 

151 

152 # "import a.b.c as foo" => {foo: a.b.c} 

153 for node in self.nodes(self.moduleNode.body, 'Import'): 

154 for nn in node.names: 

155 imp[nn.asname or nn.name] = nn.name 

156 

157 for node in self.nodes(self.moduleNode.body, 'ImportFrom'): 

158 # "from a.b.c import foo" => {foo: a.b.c.foo} 

159 if node.level == 0: 

160 for nn in node.names: 

161 imp[nn.asname or nn.name] = node.module + DOT + nn.name 

162 continue 

163 

164 # "from . import foo" => {foo: "<mod-name>.{ 

165 # "from .. import foo" => "<mod-name-before-dot>.foo" 

166 # "from ..abc import foo" => "<mod-name-before-dot>.abc.foo" 

167 

168 m = self.moduleName.split(DOT) 

169 level = node.level - self.is_init 

170 if level: 

171 m = m[:-level] 

172 m = DOT.join(m) 

173 if node.module: 

174 m += DOT + node.module 

175 for nn in node.names: 

176 imp[nn.asname or nn.name] = m + DOT + nn.name 

177 

178 # create aliases for imported types 

179 for alias, target in imp.items(): 

180 if _is_type_name(alias) and not _builtin_name(alias): 

181 self.gen.aliases[self.moduleName + DOT + alias] = target 

182 

183 return imp 

184 

185 def parse_ext_declaration(self, node): 

186 """Parse a ``gws.ext.new.<category>('<name>', ...)`` declaration. 

187 

188 Adds ``EXT`` types for the object, config and props, pointing to the 

189 given classes or to ``Object``, ``Config`` and ``Props`` of the module. 

190 

191 Args: 

192 node: Expression node. 

193 

194 Raises: 

195 ``ValueError``: If the declaration has no arguments. 

196 """ 

197 

198 if _cls(node.value) != 'Call': 

199 return 

200 call = cast(ast.Call, node.value) 

201 try: 

202 decl = _name(call.func) 

203 except ValueError: 

204 return 

205 if not decl.startswith(base.v.EXT_DECL_PREFIX): 

206 return 

207 if not call.args: 

208 raise ValueError('invalid gws.ext declaration') 

209 

210 args = list(call.args) 

211 tail = decl.split(DOT).pop() + DOT + _name(args.pop(0)) 

212 self.add(c=base.c.EXT, extName=base.v.EXT_OBJECT_PREFIX + tail, tTarget=self.qname(args.pop(0) if args else base.v.EXT_OBJECT_CLASS)) 

213 self.add(c=base.c.EXT, extName=base.v.EXT_CONFIG_PREFIX + tail, tTarget=self.qname(args.pop(0) if args else base.v.EXT_CONFIG_CLASS)) 

214 self.add(c=base.c.EXT, extName=base.v.EXT_PROPS_PREFIX + tail, tTarget=self.qname(args.pop(0) if args else base.v.EXT_PROPS_CLASS)) 

215 

216 def parse_assign(self, node, doc, annotated): 

217 """Parse a module level assignment, possibly a type alias or a constant. 

218 

219 Args: 

220 node: ``Assign`` or ``AnnAssign`` node. 

221 doc: Docstring following the assignment. 

222 annotated: True for an ``AnnAssign`` node. 

223 """ 

224 

225 if annotated: 

226 name_node = node.target 

227 else: 

228 if len(node.targets) > 1: 

229 return 

230 name_node = node.targets[0] 

231 

232 if _cls(name_node) != 'Name' or not _is_type_name(name_node.id): 

233 return 

234 

235 typ = None 

236 if hasattr(node, 'annotation'): 

237 typ = self.type_from_node(node.annotation) 

238 

239 if typ and typ.name == 'TypeAlias': 

240 # type alias 

241 target_type = self.type_from_node(node.value) 

242 self.add( 

243 c=base.c.TYPE, 

244 doc=doc, 

245 ident=name_node.id, 

246 name=self.qname(name_node), 

247 tTarget=target_type.uid, 

248 ) 

249 

250 return 

251 

252 # possibly, a constant 

253 

254 c, value = self.parse_const_value(node.value) 

255 if c == base.c.LITERAL: 

256 self.add( 

257 c=base.c.CONSTANT, 

258 doc=doc, 

259 ident=name_node.id, 

260 name=self.qname(name_node), 

261 constValue=value, 

262 ) 

263 

264 def parse_class(self, node): 

265 """Parse a class with its properties and command methods. 

266 

267 ``Enum`` subclasses are parsed by ``parse_enum``. A ``gws.ext`` 

268 class decorator adds an ``EXT`` type pointing to the class. 

269 

270 Args: 

271 node: ``ClassDef`` node. 

272 """ 

273 

274 if not _is_type_name(node.name): 

275 return 

276 

277 supers = [self.qname(b) for b in node.bases] 

278 if supers and _builtin_name(supers[0]) == 'Enum': 

279 return self.parse_enum(node) 

280 

281 typ = self.add( 

282 c=base.c.CLASS, 

283 doc=self.inner_doc(node), 

284 ident=node.name, 

285 name=self.qname(node), 

286 tSupers=[self.type_from_name(s).uid for s in supers if not _builtin_name(s)], 

287 ) 

288 

289 ext = self.gws_decorator(node, 'class') 

290 if ext: 

291 self.add(c=base.c.EXT, extName=ext, tTarget=typ.uid) 

292 

293 for nn in self.nodes(node.body): 

294 cc = _cls(nn) 

295 if cc in {'Assign', 'AnnAssign'}: 

296 doc = self.outer_doc(nn, node.body) 

297 self.parse_property(typ, nn, doc, annotated=(cc == 'AnnAssign')) 

298 elif cc == 'FunctionDef': 

299 self.parse_method(typ, nn) 

300 

301 def parse_enum(self, node): 

302 """Parse an ``Enum`` subclass. 

303 

304 Args: 

305 node: ``ClassDef`` node. 

306 

307 Raises: 

308 ``ValueError``: If an item value is not a literal. 

309 """ 

310 

311 docs = {} 

312 vals = {} 

313 

314 for nn in self.nodes(node.body): 

315 if _cls(nn) == 'Assign': 

316 ident = nn.targets[0].id 

317 c, value = self.parse_const_value(nn.value) 

318 if c != base.c.LITERAL: 

319 raise ValueError(f'invalid Enum item {ident!r}') 

320 docs[ident] = self.outer_doc(nn, node.body) 

321 vals[ident] = value 

322 

323 self.add( 

324 c=base.c.ENUM, 

325 doc=self.inner_doc(node), 

326 ident=node.name, 

327 name=self.qname(node), 

328 enumDocs=docs, 

329 enumValues=vals, 

330 ) 

331 

332 def parse_property(self, owner_typ: base.Type, node, doc: str, annotated: bool): 

333 """Parse a class property. 

334 

335 Private properties are skipped. A literal value becomes the default, 

336 a name or attribute becomes a default expression. ``Optional`` 

337 properties get ``None`` as default if they have none. 

338 

339 Args: 

340 owner_typ: Owning class type. 

341 node: ``Assign`` or ``AnnAssign`` node. 

342 doc: Docstring following the property. 

343 annotated: True for an ``AnnAssign`` node. 

344 """ 

345 

346 ident = node.target.id if annotated else node.targets[0].id 

347 if ident.startswith('_'): 

348 return 

349 

350 typ = self.add( 

351 c=base.c.PROPERTY, 

352 name=owner_typ.name + DOT + ident, 

353 doc=doc, 

354 ident=ident, 

355 tOwner=owner_typ.uid, 

356 tValue='any', 

357 defaultValue=None, 

358 hasDefault=False, 

359 ) 

360 

361 c, value = self.parse_const_value(node.value) 

362 if c == base.c.LITERAL: 

363 typ.defaultValue = value 

364 typ.hasDefault = True 

365 if c == base.c.EXPR: 

366 # evaluated later on in normalizer._evaluate_defaults 

367 typ.defaultExpression = [c, value] 

368 

369 property_type = None 

370 if hasattr(node, 'annotation'): 

371 property_type = self.type_from_node(node.annotation) 

372 

373 if not property_type: 

374 t = 'any' 

375 if typ.hasDefault: 

376 t = type(typ.defaultValue).__name__ 

377 property_type = self.type_from_name(t) 

378 

379 if property_type: 

380 if property_type.c == base.c.OPTIONAL: 

381 typ.tValue = property_type.tTarget 

382 if not typ.hasDefault: 

383 typ.defaultValue = None 

384 typ.hasDefault = True 

385 else: 

386 typ.tValue = property_type.uid 

387 

388 def parse_method(self, owner_typ: base.Type, node): 

389 """Parse a command method, or any method if ``parseAll`` is set. 

390 

391 Args: 

392 owner_typ: Owning class type. 

393 node: ``FunctionDef`` node. 

394 """ 

395 

396 ext = self.gws_decorator(node, 'method') 

397 

398 if not ext and not self.parseAll: 

399 return 

400 

401 typ = self.add( 

402 c=base.c.METHOD, 

403 doc=self.inner_doc(node), 

404 ident=node.name, 

405 name=owner_typ.name + DOT + node.name, 

406 tOwner=owner_typ.uid, 

407 tArgs=[], 

408 tArg='', 

409 tReturn='any', 

410 extName=ext, 

411 ) 

412 

413 args = node.args.args 

414 if not self.parseAll: 

415 # ext methods have only one spec'able arg (the last one) 

416 args = args[-1:] 

417 

418 for arg_node in args: 

419 t = 'any' 

420 if arg_node.annotation: 

421 arg_type = self.type_from_node(arg_node.annotation) 

422 if arg_type: 

423 t = arg_type.uid 

424 typ.tArgs.append(t) 

425 typ.tArg = t 

426 

427 if node.returns: 

428 ret_type = self.type_from_node(node.returns) 

429 typ.tReturn = ret_type.uid if ret_type else 'any' 

430 

431 def gws_decorator(self, node, kind): 

432 """Get the ``gws.ext`` name from a decorator, like ``@gws.ext.command.api('mapGetBox')``. 

433 

434 Args: 

435 node: Class or function node. 

436 kind: ``class`` or ``method``. 

437 

438 Returns: 

439 The full extension name, or an empty string if there is no ``gws.ext`` decorator. 

440 

441 Raises: 

442 ``ValueError``: If the decorator is not valid for the kind of node. 

443 """ 

444 

445 for d in getattr(node, 'decorator_list', []): 

446 if _cls(d) != 'Call' or len(d.args) != 1: 

447 continue 

448 

449 name = _name(d.func) 

450 if not name.startswith(base.v.EXT_PREFIX): 

451 continue 

452 

453 name = name + DOT + _name(d.args[0]) 

454 ns = name.split(DOT) 

455 

456 if kind == 'method': 

457 if len(ns) == 5 and name.startswith(base.v.EXT_COMMAND_PREFIX): 

458 # gws.ext.command.api.mapGetBox 

459 return name 

460 raise ValueError(f'invalid function decorator {name!r}') 

461 

462 if kind == 'class': 

463 if len(ns) == 5 and name.startswith((base.v.EXT_OBJECT_PREFIX, base.v.EXT_CONFIG_PREFIX, base.v.EXT_PROPS_PREFIX)): 

464 # gws.ext.object.layer.wms 

465 return name 

466 raise ValueError(f'invalid class decorator {name!r}') 

467 

468 return '' 

469 

470 ## 

471 

472 def type_from_node(self, node) -> base.Type: 

473 """Create a type for a type annotation or alias value. 

474 

475 Args: 

476 node: Annotation node. 

477 

478 Returns: 

479 The type. 

480 

481 Raises: 

482 ``ValueError``: If the annotation is not supported. 

483 """ 

484 

485 # here, node is a type declaration (an alias or an annotation) 

486 

487 cc = _cls(node) 

488 

489 # foo: SomeType 

490 if cc in {'Str', 'Name', 'Attribute', 'Constant'}: 

491 return self.type_from_name(self.qname(node)) 

492 

493 # foo: Generic[SomeType] 

494 if cc == 'Subscript': 

495 # Subscript(slice=Index(value=Name... in py3.8 

496 # Subscript(slice=Name... in py3.9 

497 return self.type_from_name(self.qname(node.value), node.slice.value if _cls(node.slice) == 'Index' else node.slice) 

498 

499 # foo: [SomeType, SomeType] 

500 if cc in {'List', 'Tuple'}: 

501 item_types = [self.type_from_node(e) for e in node.elts] 

502 return self.add(c=base.c.TUPLE, tItems=[typ.uid for typ in item_types]) 

503 

504 # foo: SomeType | SomeType | ... 

505 if cc == 'BinOp' and _cls(node.op) == 'BitOr': 

506 item_types = [] 

507 while _cls(node) == 'BinOp' and _cls(node.op) == 'BitOr': 

508 item_types.insert(0, self.type_from_node(node.right)) 

509 node = node.left 

510 item_types.insert(0, self.type_from_node(node)) 

511 return self.add(c=base.c.UNION, tItems=[typ.uid for typ in item_types]) 

512 

513 raise ValueError(f'unsupported type: {cc!r}') 

514 

515 def type_from_name(self, name: str, param=None) -> base.Type: 

516 """Create a type for a type name, optionally with generic parameters. 

517 

518 Handles ``Literal``, ``Optional``, ``list``, ``set``, ``dict``, 

519 ``Union``, ``tuple``, ``Callable`` and other built-in names. Unknown 

520 names become ``UNDEFINED`` types, which may be resolved later. 

521 

522 Args: 

523 name: Qualified type name. 

524 param: Subscript node with the generic parameters. 

525 

526 Returns: 

527 The type. 

528 

529 Raises: 

530 ``ValueError``: If the generic parameters are invalid. 

531 """ 

532 

533 if not param and name in self.gen.typeDict: 

534 return self.gen.typeDict[name] 

535 

536 g = _builtin_name(name) 

537 

538 if g == 'Any': 

539 return self.gen.typeDict['any'] 

540 

541 # literal - 'param' is a value or a tuple of values 

542 if g == 'Literal': 

543 if not param: 

544 raise ValueError('invalid literal') 

545 elts = param.elts if _cls(param) == 'Tuple' else [param] 

546 vals = [self.parse_literal_value(e) for e in elts] 

547 return self.add(c=base.c.LITERAL, literalValues=vals) 

548 

549 # in other cases, 'param' is a type or a tuple of types 

550 

551 param_typ = param_items = None 

552 if param: 

553 param_typ = self.type_from_node(param) 

554 if param_typ.c == base.c.TUPLE: 

555 param_items = param_typ.tItems 

556 

557 if g == 'Optional': 

558 if not param_typ: 

559 raise ValueError('invalid optional type') 

560 return self.add(c=base.c.OPTIONAL, tTarget=param_typ.uid) 

561 

562 if g.lower() == 'list': 

563 return self.add(c=base.c.LIST, tItem=param_typ.uid if param_typ else 'any') 

564 

565 if g.lower() == 'iterable': 

566 return self.add(c=base.c.LIST, tItem=param_typ.uid if param_typ else 'any') 

567 

568 if g.lower() == 'set': 

569 return self.add(c=base.c.SET, tItem=param_typ.uid if param_typ else 'any') 

570 

571 if g.lower() == 'dict': 

572 if param_items: 

573 if len(param_items) != 2: 

574 raise ValueError('invalid dict arguments') 

575 key, val = param_items 

576 elif param_typ: 

577 key = 'str' 

578 val = param_typ.uid 

579 else: 

580 key = 'str' 

581 val = 'any' 

582 return self.add(c=base.c.DICT, tKey=key, tValue=val) 

583 

584 if g == 'Union': 

585 if not param_items: 

586 raise ValueError('invalid Union') 

587 return self.add(c=base.c.UNION, tItems=sorted(param_items)) 

588 

589 if g.lower() == 'tuple': 

590 if not param_typ: 

591 return self.add(c=base.c.TUPLE, tItems=[]) 

592 if not param_items: 

593 raise ValueError('invalid Tuple') 

594 return self.add(c=base.c.TUPLE, tItems=list(param_items)) 

595 

596 if g.lower() == 'callable': 

597 if not param_typ: 

598 return self.add(c=base.c.CALLABLE, tItems=[]) 

599 if not param_items: 

600 raise ValueError('invalid Callable') 

601 return self.add(c=base.c.CALLABLE, tItems=list(param_items)) 

602 

603 if param: 

604 raise ValueError('invalid generic type') 

605 

606 if g: 

607 base.log.debug(f'created ATOM for {name!r}, builtin {g!r}') 

608 return self.add(c=base.c.ATOM, name=name) 

609 

610 return self.add(c=base.c.UNDEFINED, name=name) 

611 

612 ## 

613 

614 @property 

615 def pos(self): 

616 """Current source position. 

617 

618 Returns: 

619 The position as ``path:line``. 

620 """ 

621 

622 return self.modulePath + ':' + str(self.context[-1].lineno if self.context else 0) 

623 

624 def add(self, **kwargs) -> base.Type: 

625 """Add a type, with the current position and module. 

626 

627 Args: 

628 **kwargs: Type attributes. 

629 

630 Returns: 

631 The new type. 

632 """ 

633 

634 kwargs['pos'] = self.pos 

635 kwargs['tModule'] = self.tModule 

636 kwargs['doc'] = (kwargs.get('doc') or '').strip() 

637 typ = self.gen.add_type(**kwargs) 

638 base.log.debug(f'added {typ.uid=} {vars(typ)=}') 

639 return typ 

640 

641 def inner_doc(self, node): 

642 """Get the docstring of a module, class or function. 

643 

644 Args: 

645 node: Node with a body. 

646 

647 Returns: 

648 The docstring or an empty string. 

649 """ 

650 

651 return self.docstring_from(node.body[0]) if node.body else '' 

652 

653 def outer_doc(self, node, nodes): 

654 """Get the docstring that immediately follows a node, as used for attributes. 

655 

656 Args: 

657 node: Node. 

658 nodes: List of nodes that contains the node. 

659 

660 Returns: 

661 The docstring or an empty string. 

662 """ 

663 

664 try: 

665 nxt = nodes[nodes.index(node) + 1] 

666 except IndexError: 

667 return '' 

668 return self.docstring_from(nxt) 

669 

670 def docstring_from(self, node): 

671 """Get the content of a docstring node. 

672 

673 Args: 

674 node: Node. 

675 

676 Returns: 

677 The stripped string if the node is a string expression, otherwise an empty string. 

678 """ 

679 

680 if _cls(node) == 'Expr': 

681 if _cls(node.value) == 'Constant': 

682 v = node.value.value 

683 if isinstance(v, str): 

684 return v.strip() 

685 if _cls(node.value) == 'Str': 

686 return node.value.s.strip() 

687 return '' 

688 

689 def qname(self, node): 

690 """Get the qualified name for a name node. 

691 

692 Args: 

693 node: Name node or string. 

694 

695 Returns: 

696 The built-in name, or the name qualified by the imports or the module name. 

697 """ 

698 

699 name = _name(node) 

700 b = _builtin_name(name) 

701 if b: 

702 return b 

703 name = self.qualified(name) 

704 return name 

705 

706 def qualified(self, name): 

707 """Qualify a name using the module imports. 

708 

709 Args: 

710 name: Name, possibly dotted. 

711 

712 Returns: 

713 The qualified name. Names not found in the imports are prefixed with the module name. 

714 """ 

715 

716 for alias, mod in self.imports.items(): 

717 if name == mod or name.startswith(mod + DOT): 

718 return name 

719 if name == alias: 

720 return mod 

721 if name.startswith(alias + DOT): 

722 return mod + DOT + name[(len(alias) + 1) :] 

723 return self.moduleName + DOT + name 

724 

725 def nodes(self, where, *cls): 

726 """Iterate over nodes, keeping track of the current node for error positions. 

727 

728 Args: 

729 where: List of nodes. 

730 *cls: Node class names to include. Include all if empty. 

731 

732 Yields: 

733 Matching nodes. 

734 """ 

735 

736 for node in where: 

737 if not cls or _cls(node) in cls: 

738 self.context.append(node) 

739 yield node 

740 self.context.pop() 

741 

742 ## 

743 

744 def parse_literal_value(self, node): 

745 """Parse a scalar literal value. 

746 

747 Args: 

748 node: Value node. 

749 

750 Returns: 

751 The value. 

752 

753 Raises: 

754 ``ValueError``: If the node is not a scalar literal. 

755 """ 

756 

757 c, value = self.parse_const_value(node) 

758 if c == base.c.LITERAL and _is_scalar(value): 

759 return value 

760 raise ValueError(f'invalid literal value') 

761 

762 def parse_const_value(self, node): 

763 """Parse a constant value. 

764 

765 Literals, lists, tuples and dicts of literals are returned as ``LITERAL``. 

766 Names and attributes (references to constants or enum members), and 

767 lists and dicts containing them, are returned as ``EXPR``. 

768 

769 Args: 

770 node: Value node. 

771 

772 Returns: 

773 A tuple ``(kind, value)``, where kind is ``LITERAL`` or ``EXPR``, 

774 or a falsy kind if the value is not constant. 

775 """ 

776 

777 if node is None: 

778 return None, None 

779 

780 cc = _cls(node) 

781 

782 if cc == 'Num': 

783 return base.c.LITERAL, node.n 

784 

785 if cc in ('Str', 'Bytes'): 

786 return base.c.LITERAL, node.s 

787 

788 if cc in ('Constant', 'NameConstant'): 

789 return base.c.LITERAL, node.value 

790 

791 if cc in {'Name', 'Attribute'}: 

792 # SomeConstant or Something.someKey - possible constant/enum value 

793 return base.c.EXPR, self.qname(node) 

794 

795 if cc in {'List', 'Tuple'}: 

796 exprlst, lst = [], [] 

797 for elt in node.elts: 

798 c, value = self.parse_const_value(elt) 

799 if not c: 

800 return False, None 

801 if c == base.c.LITERAL: 

802 lst.append(value) 

803 exprlst.append([c, value]) 

804 if len(lst) == len(exprlst): 

805 return base.c.LITERAL, lst 

806 return base.c.EXPR, exprlst 

807 

808 if cc == 'Dict': 

809 exprdct, dct = {}, {} 

810 for k, v in zip(node.keys, node.values): 

811 c, key = self.parse_const_value(k) 

812 if c != base.c.LITERAL: 

813 return False, None 

814 c, value = self.parse_const_value(v) 

815 if not c: 

816 return False, None 

817 if c == base.c.LITERAL: 

818 dct[key] = value 

819 exprdct[key] = [c, value] 

820 if len(dct) == len(exprdct): 

821 return base.c.LITERAL, dct 

822 return base.c.EXPR, exprdct 

823 

824 return None, None 

825 

826 

827## 

828 

829 

830def _is_scalar(val): 

831 return isinstance(val, (str, bytes, int, float, bool)) 

832 

833 

834def _is_type_name(name: str) -> bool: 

835 return bool(name) and bool(re.match(r'^[A-Z]', name)) 

836 

837 

838def _builtin_name(name: str) -> str: 

839 if name in base.v.BUILTINS: 

840 return name 

841 if name in base.v.BUILTIN_TYPES: 

842 return name 

843 for b in base.v.BUILTIN_TYPES: 

844 if name.endswith(DOT + b): 

845 return b 

846 if name.startswith(b + DOT): 

847 return b 

848 return '' 

849 

850 

851def _is_a(full_name: str, name: str) -> bool: 

852 # if the name is like 'Object', check if the full name ends with it 

853 # if the name is like 'some.module', check if the full name starts with it 

854 if name[0].isupper(): 

855 return full_name == name or full_name.endswith(DOT + name) 

856 return full_name == name or full_name.startswith(name + DOT) 

857 

858 

859def _cls(node): 

860 return node.__class__.__name__ 

861 

862 

863def _name(node): 

864 if isinstance(node, str): 

865 return node 

866 

867 cc = _cls(node) 

868 

869 if cc == 'Name': 

870 return node.id 

871 if cc == 'Attribute': 

872 return _name(node.value) + DOT + node.attr 

873 if cc == 'Str': 

874 return node.s 

875 if cc == 'Constant': 

876 v = node.value 

877 return v if isinstance(v, str) else repr(v) 

878 if cc == 'ClassDef': 

879 return node.name 

880 if cc == 'FunctionDef': 

881 return node.name 

882 

883 raise ValueError(f'node name missing in {cc!r}') 

884 

885 

886def _camelize(name): 

887 p = name.split('_') 

888 return p[0] + ''.join(_ucfirst(s) for s in p[1:]) 

889 

890 

891def _ucfirst(s): 

892 return s[0].upper() + s[1:] 

893 

894 

895DOT = '.'