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
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +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: bool):
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 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."""
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
96 def run(self):
97 """Parse the module.
99 Raises:
100 ``ValueError``: If the module contains an invalid declaration.
101 """
103 if any('# gws:nospec' in ln for ln in self.source_lines):
104 return
106 tree = ast.parse(self.text)
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')
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
123 self.imports = self.prepare_imports()
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 )
138 def prepare_imports(self):
139 """Map imported names to qualified names and register aliases for imported types.
141 Returns:
142 A dict of imported names and their qualified names.
143 """
145 # map import names to module names
146 imp = {}
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
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
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"
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
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
179 return imp
181 def parse_ext_declaration(self, node):
182 """Parse a ``gws.ext.new.<category>('<name>', ...)`` declaration.
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.
187 Args:
188 node: Expression node.
190 Raises:
191 ``ValueError``: If the declaration has no arguments.
192 """
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')
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))
212 def parse_assign(self, node, doc, annotated):
213 """Parse a module level assignment, possibly a type alias or a constant.
215 Args:
216 node: ``Assign`` or ``AnnAssign`` node.
217 doc: Docstring following the assignment.
218 annotated: True for an ``AnnAssign`` node.
219 """
221 if annotated:
222 name_node = node.target
223 else:
224 if len(node.targets) > 1:
225 return
226 name_node = node.targets[0]
228 if _cls(name_node) != 'Name' or not _is_type_name(name_node.id):
229 return
231 typ = None
232 if hasattr(node, 'annotation'):
233 typ = self.type_from_node(node.annotation)
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 )
246 return
248 # possibly, a constant
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 )
260 def parse_class(self, node):
261 """Parse a class with its properties and command methods.
263 ``Enum`` subclasses are parsed by ``parse_enum``. A ``gws.ext``
264 class decorator adds an ``EXT`` type pointing to the class.
266 Args:
267 node: ``ClassDef`` node.
268 """
270 if not _is_type_name(node.name):
271 return
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)
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 )
285 ext = self.gws_decorator(node, 'class')
286 if ext:
287 self.add(c=base.c.EXT, extName=ext, tTarget=typ.uid)
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)
297 def parse_enum(self, node):
298 """Parse an ``Enum`` subclass.
300 Args:
301 node: ``ClassDef`` node.
303 Raises:
304 ``ValueError``: If an item value is not a literal.
305 """
307 docs = {}
308 vals = {}
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
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 )
328 def parse_property(self, owner_typ: base.Type, node, doc: str, annotated: bool):
329 """Parse a class property.
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.
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 """
342 ident = node.target.id if annotated else node.targets[0].id
343 if ident.startswith('_'):
344 return
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 )
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]
365 property_type = None
366 if hasattr(node, 'annotation'):
367 property_type = self.type_from_node(node.annotation)
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)
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
384 def parse_method(self, owner_typ: base.Type, node):
385 """Parse a command method, or any method if ``parseAll`` is set.
387 Args:
388 owner_typ: Owning class type.
389 node: ``FunctionDef`` node.
390 """
392 ext = self.gws_decorator(node, 'method')
394 if not ext and not self.parseAll:
395 return
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 )
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:]
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
423 if node.returns:
424 ret_type = self.type_from_node(node.returns)
425 typ.tReturn = ret_type.uid if ret_type else 'any'
427 def gws_decorator(self, node, kind):
428 """Get the ``gws.ext`` name from a decorator, like ``@gws.ext.command.api('mapGetBox')``.
430 Args:
431 node: Class or function node.
432 kind: ``class`` or ``method``.
434 Returns:
435 The full extension name, or an empty string if there is no ``gws.ext`` decorator.
437 Raises:
438 ``ValueError``: If the decorator is not valid for the kind of node.
439 """
441 for d in getattr(node, 'decorator_list', []):
442 if _cls(d) != 'Call' or len(d.args) != 1:
443 continue
445 name = _name(d.func)
446 if not name.startswith(base.v.EXT_PREFIX):
447 continue
449 name = name + DOT + _name(d.args[0])
450 ns = name.split(DOT)
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}')
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}')
464 return ''
466 ##
468 def type_from_node(self, node) -> base.Type:
469 """Create a type for a type annotation or alias value.
471 Args:
472 node: Annotation node.
474 Returns:
475 The type.
477 Raises:
478 ``ValueError``: If the annotation is not supported.
479 """
481 # here, node is a type declaration (an alias or an annotation)
483 cc = _cls(node)
485 # foo: SomeType
486 if cc in {'Str', 'Name', 'Attribute', 'Constant'}:
487 return self.type_from_name(self.qname(node))
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)
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])
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])
509 raise ValueError(f'unsupported type: {cc!r}')
511 def type_from_name(self, name: str, param=None) -> base.Type:
512 """Create a type for a type name, optionally with generic parameters.
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.
518 Args:
519 name: Qualified type name.
520 param: Subscript node with the generic parameters.
522 Returns:
523 The type.
525 Raises:
526 ``ValueError``: If the generic parameters are invalid.
527 """
529 if not param and name in self.gen.typeDict:
530 return self.gen.typeDict[name]
532 g = _builtin_name(name)
534 if g == 'Any':
535 return self.gen.typeDict['any']
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)
545 # in other cases, 'param' is a type or a tuple of types
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
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)
558 if g.lower() == 'list':
559 return self.add(c=base.c.LIST, tItem=param_typ.uid if param_typ else 'any')
561 if g.lower() == 'iterable':
562 return self.add(c=base.c.LIST, tItem=param_typ.uid if param_typ else 'any')
564 if g.lower() == 'set':
565 return self.add(c=base.c.SET, tItem=param_typ.uid if param_typ else 'any')
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)
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))
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))
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))
599 if param:
600 raise ValueError('invalid generic type')
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)
606 return self.add(c=base.c.UNDEFINED, name=name)
608 ##
610 @property
611 def pos(self):
612 """Current source position.
614 Returns:
615 The position as ``path:line``.
616 """
618 return self.modulePath + ':' + str(self.context[-1].lineno if self.context else 0)
620 def add(self, **kwargs) -> base.Type:
621 """Add a type, with the current position and module.
623 Args:
624 **kwargs: Type attributes.
626 Returns:
627 The new type.
628 """
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
637 def inner_doc(self, node):
638 """Get the docstring of a module, class or function.
640 Args:
641 node: Node with a body.
643 Returns:
644 The docstring or an empty string.
645 """
647 return self.docstring_from(node.body[0]) if node.body else ''
649 def outer_doc(self, node, nodes):
650 """Get the docstring that immediately follows a node, as used for attributes.
652 Args:
653 node: Node.
654 nodes: List of nodes that contains the node.
656 Returns:
657 The docstring or an empty string.
658 """
660 try:
661 nxt = nodes[nodes.index(node) + 1]
662 except IndexError:
663 return ''
664 return self.docstring_from(nxt)
666 def docstring_from(self, node):
667 """Get the content of a docstring node.
669 Args:
670 node: Node.
672 Returns:
673 The stripped string if the node is a string expression, otherwise an empty string.
674 """
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 ''
685 def qname(self, node):
686 """Get the qualified name for a name node.
688 Args:
689 node: Name node or string.
691 Returns:
692 The built-in name, or the name qualified by the imports or the module name.
693 """
695 name = _name(node)
696 b = _builtin_name(name)
697 if b:
698 return b
699 name = self.qualified(name)
700 return name
702 def qualified(self, name):
703 """Qualify a name using the module imports.
705 Args:
706 name: Name, possibly dotted.
708 Returns:
709 The qualified name. Names not found in the imports are prefixed with the module name.
710 """
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
721 def nodes(self, where, *cls):
722 """Iterate over nodes, keeping track of the current node for error positions.
724 Args:
725 where: List of nodes.
726 *cls: Node class names to include. Include all if empty.
728 Yields:
729 Matching nodes.
730 """
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()
738 ##
740 def parse_literal_value(self, node):
741 """Parse a scalar literal value.
743 Args:
744 node: Value node.
746 Returns:
747 The value.
749 Raises:
750 ``ValueError``: If the node is not a scalar literal.
751 """
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')
758 def parse_const_value(self, node):
759 """Parse a constant value.
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``.
765 Args:
766 node: Value node.
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 """
773 if node is None:
774 return None, None
776 cc = _cls(node)
778 if cc == 'Num':
779 return base.c.LITERAL, node.n
781 if cc in ('Str', 'Bytes'):
782 return base.c.LITERAL, node.s
784 if cc in ('Constant', 'NameConstant'):
785 return base.c.LITERAL, node.value
787 if cc in {'Name', 'Attribute'}:
788 # SomeConstant or Something.someKey - possible constant/enum value
789 return base.c.EXPR, self.qname(node)
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
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
820 return None, None
823##
826def _is_scalar(val):
827 return isinstance(val, (str, bytes, int, float, bool))
830def _is_type_name(name: str) -> bool:
831 return bool(name) and bool(re.match(r'^[A-Z]', name))
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 ''
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)
855def _cls(node):
856 return node.__class__.__name__
859def _name(node):
860 if isinstance(node, str):
861 return node
863 cc = _cls(node)
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
879 raise ValueError(f'node name missing in {cc!r}')
882def _camelize(name):
883 p = name.split('_')
884 return p[0] + ''.join(_ucfirst(s) for s in p[1:])
887def _ucfirst(s):
888 return s[0].upper() + s[1:]
891DOT = '.'