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

432 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +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: bool): 

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 moduleNode: ast.Module 

79 """Root node of the module.""" 

80 moduleName: str 

81 """Qualified module name.""" 

82 imports: dict[str, str] 

83 """Imported names mapped to qualified names.""" 

84 

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

86 self.gen = gen 

87 self.moduleName = module_name 

88 self.modulePath = path 

89 self.tModule = '' 

90 self.text = text 

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

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

93 self.context: list = [] 

94 self.parseAll = parse_all 

95 

96 def run(self): 

97 """Parse the module. 

98 

99 Raises: 

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

101 """ 

102 

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

104 return 

105 

106 tree = ast.parse(self.text) 

107 

108 for node in ast.walk(tree): 

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

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

111 break 

112 else: 

113 raise ValueError('module node not found') 

114 

115 typ = self.add( 

116 c=base.c.MODULE, 

117 name=self.moduleName, 

118 modPath=self.modulePath, 

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

120 ) 

121 self.tModule = typ.uid 

122 

123 self.imports = self.prepare_imports() 

124 

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

126 cc = _cls(node) 

127 if cc == 'Expr': 

128 self.parse_ext_declaration(node) 

129 elif cc == 'ClassDef': 

130 self.parse_class(node) 

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

132 self.parse_assign( 

133 node, 

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

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

136 ) 

137 

138 def prepare_imports(self): 

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

140 

141 Returns: 

142 A dict of imported names and their qualified names. 

143 """ 

144 

145 # map import names to module names 

146 imp = {} 

147 

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

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

150 for nn in node.names: 

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

152 

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

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

155 if node.level == 0: 

156 for nn in node.names: 

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

158 continue 

159 

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

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

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

163 

164 m = self.moduleName.split(DOT) 

165 level = node.level - self.is_init 

166 if level: 

167 m = m[:-level] 

168 m = DOT.join(m) 

169 if node.module: 

170 m += DOT + node.module 

171 for nn in node.names: 

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

173 

174 # create aliases for imported types 

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

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

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

178 

179 return imp 

180 

181 def parse_ext_declaration(self, node): 

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

183 

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

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

186 

187 Args: 

188 node: Expression node. 

189 

190 Raises: 

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

192 """ 

193 

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

195 return 

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

197 try: 

198 decl = _name(call.func) 

199 except ValueError: 

200 return 

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

202 return 

203 if not call.args: 

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

205 

206 args = list(call.args) 

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

208 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)) 

209 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)) 

210 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)) 

211 

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

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

214 

215 Args: 

216 node: ``Assign`` or ``AnnAssign`` node. 

217 doc: Docstring following the assignment. 

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

219 """ 

220 

221 if annotated: 

222 name_node = node.target 

223 else: 

224 if len(node.targets) > 1: 

225 return 

226 name_node = node.targets[0] 

227 

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

229 return 

230 

231 typ = None 

232 if hasattr(node, 'annotation'): 

233 typ = self.type_from_node(node.annotation) 

234 

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

236 # type alias 

237 target_type = self.type_from_node(node.value) 

238 self.add( 

239 c=base.c.TYPE, 

240 doc=doc, 

241 ident=name_node.id, 

242 name=self.qname(name_node), 

243 tTarget=target_type.uid, 

244 ) 

245 

246 return 

247 

248 # possibly, a constant 

249 

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

251 if c == base.c.LITERAL: 

252 self.add( 

253 c=base.c.CONSTANT, 

254 doc=doc, 

255 ident=name_node.id, 

256 name=self.qname(name_node), 

257 constValue=value, 

258 ) 

259 

260 def parse_class(self, node): 

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

262 

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

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

265 

266 Args: 

267 node: ``ClassDef`` node. 

268 """ 

269 

270 if not _is_type_name(node.name): 

271 return 

272 

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

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

275 return self.parse_enum(node) 

276 

277 typ = self.add( 

278 c=base.c.CLASS, 

279 doc=self.inner_doc(node), 

280 ident=node.name, 

281 name=self.qname(node), 

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

283 ) 

284 

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

286 if ext: 

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

288 

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

290 cc = _cls(nn) 

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

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

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

294 elif cc == 'FunctionDef': 

295 self.parse_method(typ, nn) 

296 

297 def parse_enum(self, node): 

298 """Parse an ``Enum`` subclass. 

299 

300 Args: 

301 node: ``ClassDef`` node. 

302 

303 Raises: 

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

305 """ 

306 

307 docs = {} 

308 vals = {} 

309 

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

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

312 ident = nn.targets[0].id 

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

314 if c != base.c.LITERAL: 

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

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

317 vals[ident] = value 

318 

319 self.add( 

320 c=base.c.ENUM, 

321 doc=self.inner_doc(node), 

322 ident=node.name, 

323 name=self.qname(node), 

324 enumDocs=docs, 

325 enumValues=vals, 

326 ) 

327 

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

329 """Parse a class property. 

330 

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

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

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

334 

335 Args: 

336 owner_typ: Owning class type. 

337 node: ``Assign`` or ``AnnAssign`` node. 

338 doc: Docstring following the property. 

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

340 """ 

341 

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

343 if ident.startswith('_'): 

344 return 

345 

346 typ = self.add( 

347 c=base.c.PROPERTY, 

348 name=owner_typ.name + DOT + ident, 

349 doc=doc, 

350 ident=ident, 

351 tOwner=owner_typ.uid, 

352 tValue='any', 

353 defaultValue=None, 

354 hasDefault=False, 

355 ) 

356 

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

358 if c == base.c.LITERAL: 

359 typ.defaultValue = value 

360 typ.hasDefault = True 

361 if c == base.c.EXPR: 

362 # evaluated later on in normalizer._evaluate_defaults 

363 typ.defaultExpression = [c, value] 

364 

365 property_type = None 

366 if hasattr(node, 'annotation'): 

367 property_type = self.type_from_node(node.annotation) 

368 

369 if not property_type: 

370 t = 'any' 

371 if typ.hasDefault: 

372 t = type(typ.defaultValue).__name__ 

373 property_type = self.type_from_name(t) 

374 

375 if property_type: 

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

377 typ.tValue = property_type.tTarget 

378 if not typ.hasDefault: 

379 typ.defaultValue = None 

380 typ.hasDefault = True 

381 else: 

382 typ.tValue = property_type.uid 

383 

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

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

386 

387 Args: 

388 owner_typ: Owning class type. 

389 node: ``FunctionDef`` node. 

390 """ 

391 

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

393 

394 if not ext and not self.parseAll: 

395 return 

396 

397 typ = self.add( 

398 c=base.c.METHOD, 

399 doc=self.inner_doc(node), 

400 ident=node.name, 

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

402 tOwner=owner_typ.uid, 

403 tArgs=[], 

404 tArg='', 

405 tReturn='any', 

406 extName=ext, 

407 ) 

408 

409 args = node.args.args 

410 if not self.parseAll: 

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

412 args = args[-1:] 

413 

414 for arg_node in args: 

415 t = 'any' 

416 if arg_node.annotation: 

417 arg_type = self.type_from_node(arg_node.annotation) 

418 if arg_type: 

419 t = arg_type.uid 

420 typ.tArgs.append(t) 

421 typ.tArg = t 

422 

423 if node.returns: 

424 ret_type = self.type_from_node(node.returns) 

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

426 

427 def gws_decorator(self, node, kind): 

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

429 

430 Args: 

431 node: Class or function node. 

432 kind: ``class`` or ``method``. 

433 

434 Returns: 

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

436 

437 Raises: 

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

439 """ 

440 

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

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

443 continue 

444 

445 name = _name(d.func) 

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

447 continue 

448 

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

450 ns = name.split(DOT) 

451 

452 if kind == 'method': 

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

454 # gws.ext.command.api.mapGetBox 

455 return name 

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

457 

458 if kind == 'class': 

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

460 # gws.ext.object.layer.wms 

461 return name 

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

463 

464 return '' 

465 

466 ## 

467 

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

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

470 

471 Args: 

472 node: Annotation node. 

473 

474 Returns: 

475 The type. 

476 

477 Raises: 

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

479 """ 

480 

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

482 

483 cc = _cls(node) 

484 

485 # foo: SomeType 

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

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

488 

489 # foo: Generic[SomeType] 

490 if cc == 'Subscript': 

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

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

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

494 

495 # foo: [SomeType, SomeType] 

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

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

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

499 

500 # foo: SomeType | SomeType | ... 

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

502 item_types = [] 

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

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

505 node = node.left 

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

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

508 

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

510 

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

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

513 

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

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

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

517 

518 Args: 

519 name: Qualified type name. 

520 param: Subscript node with the generic parameters. 

521 

522 Returns: 

523 The type. 

524 

525 Raises: 

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

527 """ 

528 

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

530 return self.gen.typeDict[name] 

531 

532 g = _builtin_name(name) 

533 

534 if g == 'Any': 

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

536 

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

538 if g == 'Literal': 

539 if not param: 

540 raise ValueError('invalid literal') 

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

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

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

544 

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

546 

547 param_typ = param_items = None 

548 if param: 

549 param_typ = self.type_from_node(param) 

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

551 param_items = param_typ.tItems 

552 

553 if g == 'Optional': 

554 if not param_typ: 

555 raise ValueError('invalid optional type') 

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

557 

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

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

560 

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

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

563 

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

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

566 

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

568 if param_items: 

569 if len(param_items) != 2: 

570 raise ValueError('invalid dict arguments') 

571 key, val = param_items 

572 elif param_typ: 

573 key = 'str' 

574 val = param_typ.uid 

575 else: 

576 key = 'str' 

577 val = 'any' 

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

579 

580 if g == 'Union': 

581 if not param_items: 

582 raise ValueError('invalid Union') 

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

584 

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

586 if not param_typ: 

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

588 if not param_items: 

589 raise ValueError('invalid Tuple') 

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

591 

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

593 if not param_typ: 

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

595 if not param_items: 

596 raise ValueError('invalid Callable') 

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

598 

599 if param: 

600 raise ValueError('invalid generic type') 

601 

602 if g: 

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

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

605 

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

607 

608 ## 

609 

610 @property 

611 def pos(self): 

612 """Current source position. 

613 

614 Returns: 

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

616 """ 

617 

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

619 

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

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

622 

623 Args: 

624 **kwargs: Type attributes. 

625 

626 Returns: 

627 The new type. 

628 """ 

629 

630 kwargs['pos'] = self.pos 

631 kwargs['tModule'] = self.tModule 

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

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

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

635 return typ 

636 

637 def inner_doc(self, node): 

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

639 

640 Args: 

641 node: Node with a body. 

642 

643 Returns: 

644 The docstring or an empty string. 

645 """ 

646 

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

648 

649 def outer_doc(self, node, nodes): 

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

651 

652 Args: 

653 node: Node. 

654 nodes: List of nodes that contains the node. 

655 

656 Returns: 

657 The docstring or an empty string. 

658 """ 

659 

660 try: 

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

662 except IndexError: 

663 return '' 

664 return self.docstring_from(nxt) 

665 

666 def docstring_from(self, node): 

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

668 

669 Args: 

670 node: Node. 

671 

672 Returns: 

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

674 """ 

675 

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

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

678 v = node.value.value 

679 if isinstance(v, str): 

680 return v.strip() 

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

682 return node.value.s.strip() 

683 return '' 

684 

685 def qname(self, node): 

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

687 

688 Args: 

689 node: Name node or string. 

690 

691 Returns: 

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

693 """ 

694 

695 name = _name(node) 

696 b = _builtin_name(name) 

697 if b: 

698 return b 

699 name = self.qualified(name) 

700 return name 

701 

702 def qualified(self, name): 

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

704 

705 Args: 

706 name: Name, possibly dotted. 

707 

708 Returns: 

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

710 """ 

711 

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

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

714 return name 

715 if name == alias: 

716 return mod 

717 if name.startswith(alias + DOT): 

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

719 return self.moduleName + DOT + name 

720 

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

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

723 

724 Args: 

725 where: List of nodes. 

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

727 

728 Yields: 

729 Matching nodes. 

730 """ 

731 

732 for node in where: 

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

734 self.context.append(node) 

735 yield node 

736 self.context.pop() 

737 

738 ## 

739 

740 def parse_literal_value(self, node): 

741 """Parse a scalar literal value. 

742 

743 Args: 

744 node: Value node. 

745 

746 Returns: 

747 The value. 

748 

749 Raises: 

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

751 """ 

752 

753 c, value = self.parse_const_value(node) 

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

755 return value 

756 raise ValueError(f'invalid literal value') 

757 

758 def parse_const_value(self, node): 

759 """Parse a constant value. 

760 

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

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

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

764 

765 Args: 

766 node: Value node. 

767 

768 Returns: 

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

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

771 """ 

772 

773 if node is None: 

774 return None, None 

775 

776 cc = _cls(node) 

777 

778 if cc == 'Num': 

779 return base.c.LITERAL, node.n 

780 

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

782 return base.c.LITERAL, node.s 

783 

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

785 return base.c.LITERAL, node.value 

786 

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

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

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

790 

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

792 exprlst, lst = [], [] 

793 for elt in node.elts: 

794 c, value = self.parse_const_value(elt) 

795 if not c: 

796 return False, None 

797 if c == base.c.LITERAL: 

798 lst.append(value) 

799 exprlst.append([c, value]) 

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

801 return base.c.LITERAL, lst 

802 return base.c.EXPR, exprlst 

803 

804 if cc == 'Dict': 

805 exprdct, dct = {}, {} 

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

807 c, key = self.parse_const_value(k) 

808 if c != base.c.LITERAL: 

809 return False, None 

810 c, value = self.parse_const_value(v) 

811 if not c: 

812 return False, None 

813 if c == base.c.LITERAL: 

814 dct[key] = value 

815 exprdct[key] = [c, value] 

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

817 return base.c.LITERAL, dct 

818 return base.c.EXPR, exprdct 

819 

820 return None, None 

821 

822 

823## 

824 

825 

826def _is_scalar(val): 

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

828 

829 

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

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

832 

833 

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

835 if name in base.v.BUILTINS: 

836 return name 

837 if name in base.v.BUILTIN_TYPES: 

838 return name 

839 for b in base.v.BUILTIN_TYPES: 

840 if name.endswith(DOT + b): 

841 return b 

842 if name.startswith(b + DOT): 

843 return b 

844 return '' 

845 

846 

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

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

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

850 if name[0].isupper(): 

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

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

853 

854 

855def _cls(node): 

856 return node.__class__.__name__ 

857 

858 

859def _name(node): 

860 if isinstance(node, str): 

861 return node 

862 

863 cc = _cls(node) 

864 

865 if cc == 'Name': 

866 return node.id 

867 if cc == 'Attribute': 

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

869 if cc == 'Str': 

870 return node.s 

871 if cc == 'Constant': 

872 v = node.value 

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

874 if cc == 'ClassDef': 

875 return node.name 

876 if cc == 'FunctionDef': 

877 return node.name 

878 

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

880 

881 

882def _camelize(name): 

883 p = name.split('_') 

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

885 

886 

887def _ucfirst(s): 

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

889 

890 

891DOT = '.'