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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Parse Python source files into spec types."""
3import ast
4import re
6from typing import cast
8from . import base, util
11def parse(gen: base.Generator, parse_all=False):
12 """Parse the Python files of all chunks and add their types to the generator.
14 Modules containing ``# gws:nospec`` are skipped.
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.
21 Raises:
22 ``GeneratorError``: If a file cannot be parsed.
23 """
25 _init_parser(gen)
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)
32def _init_parser(gen: base.Generator):
33 for b in base.v.BUILTINS:
34 gen.add_type(c=base.c.ATOM, name=b)
37def _parse_path(gen: base.Generator, path: str, base_name: str, base_dir: str, parse_all=True):
38 pp = None
40 base.log.debug(f'parsing {path=}')
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
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)
54 text = util.read_file(path)
55 pp = _PythonParser(gen, mod_name, path, text, parse_all)
56 pp.run()
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}')
66##
69class _PythonParser:
70 """Parser for a single Python module.
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 """
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."""
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
100 def run(self):
101 """Parse the module.
103 Raises:
104 ``ValueError``: If the module contains an invalid declaration.
105 """
107 if any('# gws:nospec' in ln for ln in self.source_lines):
108 return
110 tree = ast.parse(self.text)
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')
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
127 self.imports = self.prepare_imports()
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 )
142 def prepare_imports(self):
143 """Map imported names to qualified names and register aliases for imported types.
145 Returns:
146 A dict of imported names and their qualified names.
147 """
149 # map import names to module names
150 imp = {}
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
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
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"
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
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
183 return imp
185 def parse_ext_declaration(self, node):
186 """Parse a ``gws.ext.new.<category>('<name>', ...)`` declaration.
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.
191 Args:
192 node: Expression node.
194 Raises:
195 ``ValueError``: If the declaration has no arguments.
196 """
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')
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))
216 def parse_assign(self, node, doc, annotated):
217 """Parse a module level assignment, possibly a type alias or a constant.
219 Args:
220 node: ``Assign`` or ``AnnAssign`` node.
221 doc: Docstring following the assignment.
222 annotated: True for an ``AnnAssign`` node.
223 """
225 if annotated:
226 name_node = node.target
227 else:
228 if len(node.targets) > 1:
229 return
230 name_node = node.targets[0]
232 if _cls(name_node) != 'Name' or not _is_type_name(name_node.id):
233 return
235 typ = None
236 if hasattr(node, 'annotation'):
237 typ = self.type_from_node(node.annotation)
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 )
250 return
252 # possibly, a constant
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 )
264 def parse_class(self, node):
265 """Parse a class with its properties and command methods.
267 ``Enum`` subclasses are parsed by ``parse_enum``. A ``gws.ext``
268 class decorator adds an ``EXT`` type pointing to the class.
270 Args:
271 node: ``ClassDef`` node.
272 """
274 if not _is_type_name(node.name):
275 return
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)
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 )
289 ext = self.gws_decorator(node, 'class')
290 if ext:
291 self.add(c=base.c.EXT, extName=ext, tTarget=typ.uid)
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)
301 def parse_enum(self, node):
302 """Parse an ``Enum`` subclass.
304 Args:
305 node: ``ClassDef`` node.
307 Raises:
308 ``ValueError``: If an item value is not a literal.
309 """
311 docs = {}
312 vals = {}
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
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 )
332 def parse_property(self, owner_typ: base.Type, node, doc: str, annotated: bool):
333 """Parse a class property.
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.
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 """
346 ident = node.target.id if annotated else node.targets[0].id
347 if ident.startswith('_'):
348 return
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 )
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]
369 property_type = None
370 if hasattr(node, 'annotation'):
371 property_type = self.type_from_node(node.annotation)
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)
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
388 def parse_method(self, owner_typ: base.Type, node):
389 """Parse a command method, or any method if ``parseAll`` is set.
391 Args:
392 owner_typ: Owning class type.
393 node: ``FunctionDef`` node.
394 """
396 ext = self.gws_decorator(node, 'method')
398 if not ext and not self.parseAll:
399 return
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 )
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:]
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
427 if node.returns:
428 ret_type = self.type_from_node(node.returns)
429 typ.tReturn = ret_type.uid if ret_type else 'any'
431 def gws_decorator(self, node, kind):
432 """Get the ``gws.ext`` name from a decorator, like ``@gws.ext.command.api('mapGetBox')``.
434 Args:
435 node: Class or function node.
436 kind: ``class`` or ``method``.
438 Returns:
439 The full extension name, or an empty string if there is no ``gws.ext`` decorator.
441 Raises:
442 ``ValueError``: If the decorator is not valid for the kind of node.
443 """
445 for d in getattr(node, 'decorator_list', []):
446 if _cls(d) != 'Call' or len(d.args) != 1:
447 continue
449 name = _name(d.func)
450 if not name.startswith(base.v.EXT_PREFIX):
451 continue
453 name = name + DOT + _name(d.args[0])
454 ns = name.split(DOT)
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}')
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}')
468 return ''
470 ##
472 def type_from_node(self, node) -> base.Type:
473 """Create a type for a type annotation or alias value.
475 Args:
476 node: Annotation node.
478 Returns:
479 The type.
481 Raises:
482 ``ValueError``: If the annotation is not supported.
483 """
485 # here, node is a type declaration (an alias or an annotation)
487 cc = _cls(node)
489 # foo: SomeType
490 if cc in {'Str', 'Name', 'Attribute', 'Constant'}:
491 return self.type_from_name(self.qname(node))
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)
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])
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])
513 raise ValueError(f'unsupported type: {cc!r}')
515 def type_from_name(self, name: str, param=None) -> base.Type:
516 """Create a type for a type name, optionally with generic parameters.
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.
522 Args:
523 name: Qualified type name.
524 param: Subscript node with the generic parameters.
526 Returns:
527 The type.
529 Raises:
530 ``ValueError``: If the generic parameters are invalid.
531 """
533 if not param and name in self.gen.typeDict:
534 return self.gen.typeDict[name]
536 g = _builtin_name(name)
538 if g == 'Any':
539 return self.gen.typeDict['any']
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)
549 # in other cases, 'param' is a type or a tuple of types
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
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)
562 if g.lower() == 'list':
563 return self.add(c=base.c.LIST, tItem=param_typ.uid if param_typ else 'any')
565 if g.lower() == 'iterable':
566 return self.add(c=base.c.LIST, tItem=param_typ.uid if param_typ else 'any')
568 if g.lower() == 'set':
569 return self.add(c=base.c.SET, tItem=param_typ.uid if param_typ else 'any')
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)
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))
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))
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))
603 if param:
604 raise ValueError('invalid generic type')
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)
610 return self.add(c=base.c.UNDEFINED, name=name)
612 ##
614 @property
615 def pos(self):
616 """Current source position.
618 Returns:
619 The position as ``path:line``.
620 """
622 return self.modulePath + ':' + str(self.context[-1].lineno if self.context else 0)
624 def add(self, **kwargs) -> base.Type:
625 """Add a type, with the current position and module.
627 Args:
628 **kwargs: Type attributes.
630 Returns:
631 The new type.
632 """
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
641 def inner_doc(self, node):
642 """Get the docstring of a module, class or function.
644 Args:
645 node: Node with a body.
647 Returns:
648 The docstring or an empty string.
649 """
651 return self.docstring_from(node.body[0]) if node.body else ''
653 def outer_doc(self, node, nodes):
654 """Get the docstring that immediately follows a node, as used for attributes.
656 Args:
657 node: Node.
658 nodes: List of nodes that contains the node.
660 Returns:
661 The docstring or an empty string.
662 """
664 try:
665 nxt = nodes[nodes.index(node) + 1]
666 except IndexError:
667 return ''
668 return self.docstring_from(nxt)
670 def docstring_from(self, node):
671 """Get the content of a docstring node.
673 Args:
674 node: Node.
676 Returns:
677 The stripped string if the node is a string expression, otherwise an empty string.
678 """
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 ''
689 def qname(self, node):
690 """Get the qualified name for a name node.
692 Args:
693 node: Name node or string.
695 Returns:
696 The built-in name, or the name qualified by the imports or the module name.
697 """
699 name = _name(node)
700 b = _builtin_name(name)
701 if b:
702 return b
703 name = self.qualified(name)
704 return name
706 def qualified(self, name):
707 """Qualify a name using the module imports.
709 Args:
710 name: Name, possibly dotted.
712 Returns:
713 The qualified name. Names not found in the imports are prefixed with the module name.
714 """
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
725 def nodes(self, where, *cls):
726 """Iterate over nodes, keeping track of the current node for error positions.
728 Args:
729 where: List of nodes.
730 *cls: Node class names to include. Include all if empty.
732 Yields:
733 Matching nodes.
734 """
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()
742 ##
744 def parse_literal_value(self, node):
745 """Parse a scalar literal value.
747 Args:
748 node: Value node.
750 Returns:
751 The value.
753 Raises:
754 ``ValueError``: If the node is not a scalar literal.
755 """
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')
762 def parse_const_value(self, node):
763 """Parse a constant value.
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``.
769 Args:
770 node: Value node.
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 """
777 if node is None:
778 return None, None
780 cc = _cls(node)
782 if cc == 'Num':
783 return base.c.LITERAL, node.n
785 if cc in ('Str', 'Bytes'):
786 return base.c.LITERAL, node.s
788 if cc in ('Constant', 'NameConstant'):
789 return base.c.LITERAL, node.value
791 if cc in {'Name', 'Attribute'}:
792 # SomeConstant or Something.someKey - possible constant/enum value
793 return base.c.EXPR, self.qname(node)
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
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
824 return None, None
827##
830def _is_scalar(val):
831 return isinstance(val, (str, bytes, int, float, bool))
834def _is_type_name(name: str) -> bool:
835 return bool(name) and bool(re.match(r'^[A-Z]', name))
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 ''
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)
859def _cls(node):
860 return node.__class__.__name__
863def _name(node):
864 if isinstance(node, str):
865 return node
867 cc = _cls(node)
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
883 raise ValueError(f'node name missing in {cc!r}')
886def _camelize(name):
887 p = name.split('_')
888 return p[0] + ''.join(_ucfirst(s) for s in p[1:])
891def _ucfirst(s):
892 return s[0].upper() + s[1:]
895DOT = '.'