add proxy for the first part and try to develop the second part

This commit is contained in:
Pierre Geeraert
2025-12-03 12:08:56 +00:00
parent 6c34d510e5
commit 460f18d85a
1095 changed files with 358799 additions and 5 deletions
@@ -0,0 +1,18 @@
from __future__ import annotations
from ..exceptions import CodegenError
from .cgbase import ( # noqa: F401
CodeGenerator,
DelegatingRenderingFormatter,
ModelRenderer,
NullModelRenderer,
)
def codegen(model, target='python'):
if target.lower() == 'python':
from tatsu.codegen import python
return python.codegen(model)
else:
raise CodegenError(f'Unknown target language: {target}')
@@ -0,0 +1,159 @@
from __future__ import annotations
from ..exceptions import CodegenError
from ..objectmodel import Node
from ..rendering import Renderer, RenderingFormatter, render
__all__ = [
'DelegatingRenderingFormatter',
'ModelRenderer',
'NullModelRenderer',
'CodeGenerator',
]
class DelegatingRenderingFormatter(RenderingFormatter):
def __init__(self, delegate):
assert hasattr(delegate, 'render')
super().__init__()
self.delegate = delegate
# override
def render(self, item, join='', **fields):
result = self.delegate.render(item, join=join, **fields)
if result is None:
result = super().render(item, join=join, **fields)
return result
def convert_field(self, value, conversion):
if isinstance(value, Node):
return self.render(value)
else:
return super().convert_field(value, conversion)
class ModelRenderer(Renderer):
def __init__(self, codegen, node, template=None):
super().__init__(template=template)
self._codegen = codegen
self._node = node
self.formatter = codegen.formatter
self.__postinit__()
def __postinit__(self):
pass
def __getattr__(self, name):
try:
super().__getattr__(name)
except AttributeError:
if name.startswith('_'):
raise
return getattr(self.node, name)
@property
def node(self):
return self._node
@property
def codegen(self):
return self._codegen
@property
def context(self):
return self._codegen
def get_renderer(self, item):
return self.codegen.get_renderer(item)
def render(self, **fields):
template = fields.pop('template', None)
if isinstance(self.node, Node):
fields.update(
{
k: v
for k, v in vars(self.node).items()
if not k.startswith('_')
},
)
else:
fields.update(value=self.node)
return super().render(template=template, **fields)
class NullModelRenderer(ModelRenderer):
"""A `ModelRenderer` that generates nothing."""
template = ''
class CodeGenerator:
"""
A **CodeGenerator** is an abstract class that finds a
``ModelRenderer`` class with the same name as each model's node and
uses it to render the node.
"""
def __init__(self, modules=None):
self.formatter = DelegatingRenderingFormatter(self)
self._renderers = {}
if modules is not None:
self._renderers = self._find_module_renderers(modules)
def _find_module_renderers(self, modules):
result = {}
for module in modules:
for name, dtype in vars(module).items():
if not isinstance(dtype, type):
continue
if not issubclass(dtype, ModelRenderer):
continue
if dtype is not ModelRenderer:
result[name] = dtype
return result
def _find_renderer_class(self, node):
if not isinstance(node, Node):
return None
node_class_name = node.__class__.__name__
classes = [node.__class__]
while classes:
cls = classes.pop()
name = cls.__name__
if name in self._renderers:
renderer = self._renderers[name]
self._renderers[node_class_name] = renderer
return renderer
for base in cls.__bases__:
if base not in classes:
classes.append(base)
raise CodegenError(f'Renderer for {node_class_name} not found')
def get_renderer(self, item):
if not isinstance(item, Node):
return None
renderer_class = self._find_renderer_class(item)
if renderer_class is None:
raise CodegenError(
f'Renderer not found for {type(item).__name__}',
)
try:
assert issubclass(renderer_class, ModelRenderer)
return renderer_class(self, item)
except Exception as e:
raise type(e)(str(e), renderer_class.__name__) from e
def render(self, item, join='', **fields):
renderer = self.get_renderer(item)
if renderer is None:
return render(item, join=join, **fields)
return str(renderer.render(**fields))
@@ -0,0 +1,233 @@
import inspect
from collections import namedtuple
from datetime import datetime
from tatsu.util import compress_seq, indent, re, safe_name
from ..codegen.cgbase import CodeGenerator, ModelRenderer
from ..exceptions import CodegenError
from ..objectmodel import BASE_CLASS_TOKEN, Node
from ..rendering import Renderer
NODE_NAME_PATTERN = r'(?!\d)\w+(' + rf'{BASE_CLASS_TOKEN}' + r'(?!\d)\w+)*'
TypeSpec = namedtuple('TypeSpec', ['class_name', 'base'])
DEFAULT_BASE_TYPE = """
@dataclass(eq=False)
class ModelBase(Node):
pass
"""
def codegen(model, base_type=None):
return ObjectModelCodeGenerator().render(model, base_type=base_type)
def _get_node_class_name(rule):
if not rule.params:
return None
typespec = rule.params[0]
if not isinstance(typespec, str):
return None
if not re.match(NODE_NAME_PATTERN, typespec):
return None
if not typespec[0].isupper():
return None
return typespec
def _typespec(rule):
if not _get_node_class_name(rule):
return []
spec = rule.params[0].split(BASE_CLASS_TOKEN)
class_names = [safe_name(n) for n in spec] + ['ModelBase']
typespec = []
for i, class_name in enumerate(class_names[:-1]):
base = class_names[i + 1]
typespec.append(TypeSpec(class_name, base))
return typespec
def _get_full_name(cls):
if not inspect.isclass(cls):
raise CodegenError('Base type has to be a class')
module = inspect.getmodule(cls)
if not module:
raise CodegenError('Base type has to be inside a module')
modulename = module.__name__
name = cls.__qualname__
# Try to reference the class
try:
idents = name.split('.')
cls_ = getattr(module, idents[0])
for ident in idents[1:]:
cls_ = getattr(cls_, ident)
assert cls_ == cls
except AttributeError as e:
raise CodegenError(
"Couldn't find base type, it has to be importable",
) from e
return modulename, name
class BaseTypeRenderer(Renderer):
def __init__(self, base_type):
super().__init__()
self.base_type = base_type
def render_fields(self, fields):
module, name = _get_full_name(self.base_type)
if '.' in name:
lookup = f'\nModelBase = {name}'
name = name.split('.')[0]
else:
lookup = ' as ModelBase'
fields.update(module=module, name=name, lookup=lookup)
template = """
from {module} import {name}{lookup}\
"""
class BaseClassRenderer(Renderer):
def __init__(self, spec):
super().__init__()
self.class_name = spec.class_name
self.base = spec.base
template = """
@dataclass(eq=False)
class {class_name}({base}):
pass\
"""
class ObjectModelCodeGenerator(CodeGenerator):
def _find_renderer_class(self, node):
if not isinstance(node, Node):
return None
name = node.__class__.__name__
renderer = globals().get(name)
if not renderer or not issubclass(renderer, ModelRenderer):
raise CodegenError(f'Renderer for {name} not found')
return renderer
class Rule(ModelRenderer):
def render_fields(self, fields):
defs = [safe_name(d) for d, _ in compress_seq(self.defines())]
defs = sorted(set(defs))
spec = fields['spec']
kwargs = '\n'.join(f'{d}: Any = None' for d in defs)
kwargs = indent(kwargs) if kwargs else indent('pass')
fields.update(
class_name=spec.class_name, base=spec.base, kwargs=kwargs,
)
template = """
@dataclass(eq=False)
class {class_name}({base}):
{kwargs}\
"""
class Grammar(ModelRenderer):
def render_fields(self, fields):
node_class_names = set()
bases = []
model_rules = []
for rule in self.node.rules:
specs = _typespec(rule)
if not specs:
continue
node_spec = specs[0]
base_spec = reversed(specs[1:])
if node_spec.class_name not in node_class_names:
model_rules.append((rule, node_spec))
bases.extend(
base
for base in base_spec
if base.class_name not in node_class_names
)
node_class_names.update(s.class_name for s in specs)
base_class_declarations = [
BaseClassRenderer(spec).render() for spec in bases
]
model_class_declarations = [
self.get_renderer(rule).render(spec=spec)
for rule, spec in model_rules
]
base_class_declarations = '\n\n\n'.join(base_class_declarations)
if base_class_declarations:
base_class_declarations += '\n\n'
model_class_declarations = '\n\n\n'.join(model_class_declarations)
version = datetime.now().strftime('%Y.%m.%d.%H')
base_type = fields['base_type']
fields.update(
name=self.node.name,
base_class_declarations=base_class_declarations,
model_class_declarations=model_class_declarations,
version=version,
base_type=BaseTypeRenderer(base_type).render()
if base_type
else DEFAULT_BASE_TYPE,
)
template = """\
#!/usr/bin/env python
# CAVEAT UTILITOR
#
# This file was automatically generated by TatSu.
#
# https://pypi.python.org/pypi/tatsu/
#
# Any changes you make to it will be overwritten the next time
# the file is generated.
from __future__ import annotations
from typing import Any
from dataclasses import dataclass
from tatsu.objectmodel import Node
from tatsu.semantics import ModelBuilderSemantics
{base_type}
class {name}ModelBuilderSemantics(ModelBuilderSemantics):
def __init__(self, context=None, types=None):
types = [
t for t in globals().values()
if type(t) is type and issubclass(t, ModelBase)
] + (types or [])
super().__init__(context=context, types=types)
{base_class_declarations}
{model_class_declarations}
"""
@@ -0,0 +1,594 @@
"""
Python code generation for models defined with tatsu.model
"""
from __future__ import annotations
import re
import textwrap
from .. import grammars
from ..collections import OrderedSet as oset
from ..exceptions import CodegenError
from ..objectmodel import BASE_CLASS_TOKEN, Node
from ..util import compress_seq, indent, safe_name, timestamp, trim
from .cgbase import CodeGenerator, ModelRenderer
class PythonCodeGenerator(CodeGenerator):
def _find_renderer_class(self, node):
if not isinstance(node, Node):
return None
name = node.__class__.__name__
renderer = globals().get(name)
if not renderer or not issubclass(renderer, Base):
raise CodegenError(f'Renderer for {name} not found')
return renderer
def codegen(model):
return PythonCodeGenerator().render(model)
class Base(ModelRenderer):
def defines(self):
return self.node.defines()
def make_defines_declaration(self):
defines = compress_seq(self.defines())
ldefs = oset(safe_name(d) for d, value in defines if value)
sdefs = oset(
safe_name(d)
for d, value in defines
if not value and d not in ldefs
)
if not (sdefs or ldefs):
return ''
else:
sdefs = '[{}]'.format(', '.join(sorted(repr(d) for d in sdefs)))
ldefs = '[{}]'.format(', '.join(sorted(repr(d) for d in ldefs)))
if not ldefs:
return f'\n\n self._define({sdefs}, {ldefs})'
else:
return '\n' + trim(self.define_template % (sdefs, ldefs))
define_template = """\
self._define(
%s,
%s,
)\
"""
class Void(Base):
template = 'self._void()'
class Any(Base):
template = 'self._any()'
class Fail(Base):
template = 'self._fail()'
class Comment(Base):
def render_fields(self, fields):
lines = '\n'.join(
f'# {c!s}' for c in self.node.comment.splitlines()
)
fields.update(lines=lines)
template = '\n{lines}\n'
class EOLComment(Comment):
pass
class EOF(Base):
template = 'self._check_eof()'
class _Decorator(Base):
template = '{exp}'
class Group(_Decorator):
template = """\
with self._group():
{exp:1::}\
"""
class Token(Base):
def render_fields(self, fields):
fields.update(token=repr(self.node.token))
template = 'self._token({token})'
class Constant(Base):
def render_fields(self, fields):
fields.update(constant=repr(self.node.literal))
template = 'self._constant({constant})'
class Alert(Base):
def render_fields(self, fields):
fields.update(literal=repr(self.node.literal), level=self.node.level)
template = 'self._alert({literal}, {level})'
class Pattern(Base):
def render_fields(self, fields):
raw_repr = repr(self.node.pattern)
fields.update(pattern=raw_repr)
template = 'self._pattern({pattern})'
class Lookahead(_Decorator):
template = """\
with self._if():
{exp:1::}\
"""
class NegativeLookahead(_Decorator):
template = """\
with self._ifnot():
{exp:1::}\
"""
class Sequence(Base):
def render_fields(self, fields):
fields.update(seq='\n'.join(self.rend(s) for s in self.node.sequence))
defines = self.make_defines_declaration()
fields.update(defines=defines)
template = """{seq}
{defines}\
"""
class Choice(Base):
def render_fields(self, fields):
firstset = self.node.lookahead_str()
if firstset:
msglines = textwrap.wrap(firstset, width=40)
error = ['expecting one of: ', *msglines]
else:
error = ['no available options']
error = [repr(e) for e in error]
fields.update(n=self.counter(), error=error)
fields.update(
options='\n'.join(self.rend(o) for o in self.node.options),
)
def render(self, **fields):
if len(self.node.options) == 1:
return self.rend(self.options[0], **fields)
else:
return super().render(**fields)
template = """\
with self._choice():
{options:1::}
self._error(
{error:2:\\n:} # noqa: COM812
)\
"""
class Option(_Decorator):
def render_fields(self, fields):
defines = self.make_defines_declaration()
fields.update(defines=defines)
template = """\
with self._option():
{exp:1::}\
"""
class Closure(_Decorator):
def render_fields(self, fields):
fields.update(n=self.counter())
def render(self, **fields):
if () in self.node.exp.lookahead():
raise CodegenError(f'{self.node} may repeat empty sequence')
return '\n' + super().render(**fields)
template = """\
def block{n}():
{exp:1::}
self._closure(block{n})\
"""
class PositiveClosure(Closure):
template = """\
def block{n}():
{exp:1::}
self._positive_closure(block{n})\
"""
class Join(_Decorator):
def render_fields(self, fields):
fields.update(n=self.counter())
def render(self, **fields):
if () in self.node.exp.lookahead():
raise CodegenError('may repeat empty sequence')
return '\n' + super().render(**fields)
template = """\
def sep{n}():
{sep:1::}
def block{n}():
{exp:1::}
self._join(block{n}, sep{n})\
"""
class PositiveJoin(Join):
template = """\
def sep{n}():
{sep:1::}
def block{n}():
{exp:1::}
self._positive_join(block{n}, sep{n})\
"""
class Gather(Join):
template = """\
def sep{n}():
{sep:1::}
def block{n}():
{exp:1::}
self._gather(block{n}, sep{n})\
"""
class PositiveGather(Join):
template = """\
def sep{n}():
{sep:1::}
def block{n}():
{exp:1::}
self._positive_gather(block{n}, sep{n})\
"""
class LeftJoin(PositiveJoin):
template = """\
def sep{n}():
{sep:1::}
def block{n}():
{exp:1::}
self._left_join(block{n}, sep{n})\
"""
class RightJoin(PositiveJoin):
template = """\
def sep{n}():
{sep:1::}
def block{n}():
{exp:1::}
self._right_join(block{n}, sep{n})\
"""
class EmptyClosure(Base):
template = 'self._empty_closure()'
class SkipTo(Closure):
template = """\
def block{n}():
{exp:1::}
self._skip_to(block{n})\
"""
class Optional(_Decorator):
template = """\
with self._optional():
{exp:1::}\
"""
class Cut(Base):
template = 'self._cut()'
class Named(_Decorator):
def __str__(self):
return f'{self.name}:{self.rend(self.exp)}'
def render_fields(self, fields):
fields.update(n=self.counter(), name=safe_name(self.node.name))
template = """
{exp}
self.name_last_node('{name}')\
"""
class NamedList(Named):
template = """
{exp}
self.add_last_node_to_name('{name}')\
"""
class Override(Named):
pass
class OverrideList(NamedList):
pass
class Special(Base):
pass
class RuleRef(Base):
template = 'self._{name}_()'
class RuleInclude(_Decorator):
def render_fields(self, fields):
super().render_fields(fields)
fields.update(exp=self.rend(self.node.rule.exp))
template = """
{exp}
"""
class Rule(_Decorator):
@staticmethod
def param_repr(p):
if isinstance(p, int | float):
return str(p)
else:
return repr(p.split(BASE_CLASS_TOKEN)[0])
def render(self, **fields):
try:
return super().render(**fields)
except CodegenError as e:
raise CodegenError(f'{self.node.name}={e}') from e
def render_fields(self, fields):
self.reset_counter()
params = kwparams = ''
if self.node.params:
params = ', '.join(
self.param_repr(self.rend(p)) for p in self.node.params
)
if self.node.kwparams:
kwparams = ', '.join(
f'{k}={self.param_repr(self.rend(v))}'
for k, v in self.kwparams.items()
)
if params and kwparams:
params = params + ', ' + kwparams
elif kwparams:
params = kwparams
fields.update(params=params)
sdefines = ''
if not isinstance(self.node, grammars.Choice):
sdefines = self.make_defines_declaration()
fields.update(defines=sdefines)
leftrec = self.node.is_leftrec
fields.update(leftrec='\n@leftrec' if leftrec else '')
fields.update(
nomemo='\n@nomemo'
if not self.node.is_memoizable and not leftrec
else '',
)
fields.update(isname='\n@isname' if self.node.is_name else '')
template = """
@tatsumasu({params})\
{leftrec}\
{nomemo}\
{isname}
def _{name}_(self):
{exp:1::}
"""
define_template = """\
self._define(
%s,
%s
)\
"""
class BasedRule(Rule):
def defines(self):
return self.rhs.defines()
def render_fields(self, fields):
super().render_fields(fields)
fields.update(exp=self.rhs)
class Grammar(Base):
def render_fields(self, fields):
abstract_template = trim(self.abstract_rule_template)
abstract_rules = [
abstract_template.format(name=safe_name(rule.name))
for rule in self.node.rules
]
abstract_rules = indent('\n'.join(abstract_rules))
whitespace = self.node.config.whitespace
if not whitespace:
whitespace = 'None'
elif isinstance(whitespace, re.Pattern):
whitespace = repr(whitespace)
else:
whitespace = f're.compile(r"{whitespace}")'
if self.node.config.nameguard is not None:
nameguard = repr(self.node.config.nameguard)
else:
nameguard = 'None'
ignorecase = self.node.config.ignorecase
left_recursion = self.node.config.left_recursion
parseinfo = self.node.config.parseinfo
namechars = repr(self.node.config.namechars or '')
comments = repr(self.node.config.comments)
eol_comments = repr(self.node.config.eol_comments)
rules = '\n'.join(
[self.get_renderer(rule).render() for rule in self.node.rules],
)
version = str(tuple(int(n) for n in str(timestamp()).split('.')))
keywords = [str(k) for k in self.keywords if k is not None]
keywords = '\n'.join(f' {k!r},' for k in keywords)
if keywords:
keywords = f'(\n{keywords}\n)'
fields.update(
rules=indent(rules),
start=self.node.rules[0].name,
abstract_rules=abstract_rules,
version=version,
whitespace=whitespace,
nameguard=nameguard,
ignorecase=ignorecase,
left_recursion=left_recursion,
parseinfo=parseinfo,
keywords=keywords,
namechars=namechars,
comments=comments,
eol_comments=eol_comments,
)
abstract_rule_template = """
def {name}(self, ast):
return ast
"""
template = """\
#!/usr/bin/env python3
# WARNING: CAVEAT UTILITOR
#
# This file was automatically generated by TatSu.
#
# https://pypi.python.org/pypi/tatsu/
#
# Any changes you make to it will be overwritten the next time
# the file is generated.
# ruff: noqa: C405, I001, F401, SIM117
import sys
from pathlib import Path
from tatsu.buffering import Buffer
from tatsu.parsing import Parser
from tatsu.parsing import tatsumasu
from tatsu.parsing import leftrec, nomemo, isname
from tatsu.infos import ParserConfig
from tatsu.util import re, generic_main
KEYWORDS: set[str] = set({keywords})
class {name}Buffer(Buffer):
def __init__(self, text, /, config: ParserConfig | None = None, **settings):
config = ParserConfig.new(
config,
owner=self,
whitespace={whitespace},
nameguard={nameguard},
ignorecase={ignorecase},
namechars={namechars},
parseinfo={parseinfo},
comments={comments},
eol_comments={eol_comments},
keywords=KEYWORDS,
start={start!r},
)
config = config.replace(**settings)
super().__init__(text, config=config)
class {name}Parser(Parser):
def __init__(self, /, config: ParserConfig | None = None, **settings):
config = ParserConfig.new(
config,
owner=self,
whitespace={whitespace},
nameguard={nameguard},
ignorecase={ignorecase},
namechars={namechars},
parseinfo={parseinfo},
comments={comments},
eol_comments={eol_comments},
left_recursion={left_recursion},
keywords=KEYWORDS,
start={start!r},
)
config = config.replace(**settings)
super().__init__(config=config)
{rules}
class {name}Semantics:
{abstract_rules}
def main(filename, **kwargs):
if not filename or filename == '-':
text = sys.stdin.read()
else:
text = Path(filename).read_text()
parser = {name}Parser()
return parser.parse(
text,
filename=filename,
**kwargs,
)
if __name__ == '__main__':
import json
from tatsu.util import asjson
ast = generic_main(main, {name}Parser, name='{name}')
data = asjson(ast)
print(json.dumps(data, indent=2))
"""