pyfcstm 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyfcstm/__init__.py +0 -0
- pyfcstm/__main__.py +4 -0
- pyfcstm/config/__init__.py +0 -0
- pyfcstm/config/meta.py +20 -0
- pyfcstm/dsl/__init__.py +6 -0
- pyfcstm/dsl/error.py +226 -0
- pyfcstm/dsl/grammar/Grammar.g4 +190 -0
- pyfcstm/dsl/grammar/Grammar.interp +168 -0
- pyfcstm/dsl/grammar/Grammar.tokens +118 -0
- pyfcstm/dsl/grammar/GrammarLexer.interp +214 -0
- pyfcstm/dsl/grammar/GrammarLexer.py +523 -0
- pyfcstm/dsl/grammar/GrammarLexer.tokens +118 -0
- pyfcstm/dsl/grammar/GrammarListener.py +521 -0
- pyfcstm/dsl/grammar/GrammarParser.py +4373 -0
- pyfcstm/dsl/grammar/__init__.py +3 -0
- pyfcstm/dsl/listener.py +440 -0
- pyfcstm/dsl/node.py +1581 -0
- pyfcstm/dsl/parse.py +155 -0
- pyfcstm/entry/__init__.py +1 -0
- pyfcstm/entry/base.py +126 -0
- pyfcstm/entry/cli.py +12 -0
- pyfcstm/entry/dispatch.py +46 -0
- pyfcstm/entry/generate.py +83 -0
- pyfcstm/entry/plantuml.py +67 -0
- pyfcstm/model/__init__.py +3 -0
- pyfcstm/model/base.py +51 -0
- pyfcstm/model/expr.py +764 -0
- pyfcstm/model/model.py +1392 -0
- pyfcstm/render/__init__.py +3 -0
- pyfcstm/render/env.py +36 -0
- pyfcstm/render/expr.py +180 -0
- pyfcstm/render/func.py +77 -0
- pyfcstm/render/render.py +279 -0
- pyfcstm/utils/__init__.py +6 -0
- pyfcstm/utils/binary.py +38 -0
- pyfcstm/utils/doc.py +64 -0
- pyfcstm/utils/jinja2.py +121 -0
- pyfcstm/utils/json.py +125 -0
- pyfcstm/utils/text.py +91 -0
- pyfcstm/utils/validate.py +102 -0
- pyfcstm-0.0.1.dist-info/LICENSE +165 -0
- pyfcstm-0.0.1.dist-info/METADATA +205 -0
- pyfcstm-0.0.1.dist-info/RECORD +46 -0
- pyfcstm-0.0.1.dist-info/WHEEL +5 -0
- pyfcstm-0.0.1.dist-info/entry_points.txt +2 -0
- pyfcstm-0.0.1.dist-info/top_level.txt +1 -0
pyfcstm/render/env.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
"""
|
2
|
+
Jinja2 Environment Module
|
3
|
+
|
4
|
+
This module provides functionality for creating and configuring a Jinja2 environment
|
5
|
+
for template rendering. It sets up global variables and adds custom settings to the
|
6
|
+
environment to support state machine template rendering.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import jinja2
|
10
|
+
|
11
|
+
from ..dsl import INIT_STATE, EXIT_STATE
|
12
|
+
from ..utils import add_settings_for_env
|
13
|
+
|
14
|
+
|
15
|
+
def create_env():
|
16
|
+
"""
|
17
|
+
Create and configure a Jinja2 environment for template rendering.
|
18
|
+
|
19
|
+
This function initializes a Jinja2 Environment instance, adds custom settings
|
20
|
+
through the add_settings_for_env utility, and sets up global variables for
|
21
|
+
state machine templates including initial and exit states.
|
22
|
+
|
23
|
+
:return: A configured Jinja2 Environment instance
|
24
|
+
:rtype: jinja2.Environment
|
25
|
+
|
26
|
+
Example::
|
27
|
+
|
28
|
+
>>> env = create_env()
|
29
|
+
>>> template = env.from_string("Initial state: {{ INIT_STATE }}")
|
30
|
+
>>> rendered = template.render()
|
31
|
+
"""
|
32
|
+
env = jinja2.Environment()
|
33
|
+
env = add_settings_for_env(env)
|
34
|
+
env.globals['INIT_STATE'] = INIT_STATE
|
35
|
+
env.globals['EXIT_STATE'] = EXIT_STATE
|
36
|
+
return env
|
pyfcstm/render/expr.py
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
"""
|
2
|
+
Expression rendering module for converting DSL nodes to different language formats.
|
3
|
+
|
4
|
+
This module provides functionality to render expression nodes in various language styles
|
5
|
+
including DSL, C/C++, and Python. It uses Jinja2 templating to transform abstract syntax
|
6
|
+
tree nodes into string representations according to the specified language style.
|
7
|
+
|
8
|
+
The module contains predefined templates for different node types and operators,
|
9
|
+
and allows for custom template extensions.
|
10
|
+
"""
|
11
|
+
|
12
|
+
from functools import partial
|
13
|
+
from typing import Optional, Dict, Union, Any
|
14
|
+
|
15
|
+
import jinja2
|
16
|
+
|
17
|
+
from .env import create_env
|
18
|
+
from ..dsl import node as dsl_nodes
|
19
|
+
from ..model import Integer, Float, Boolean
|
20
|
+
|
21
|
+
_DSL_STYLE = {
|
22
|
+
'Float': '{{ node.value | repr }}',
|
23
|
+
'Integer': '{{ node.value | repr }}',
|
24
|
+
'Boolean': '{{ node.value | repr }}',
|
25
|
+
'Constant': '{{ node.value | repr }}',
|
26
|
+
'HexInt': '{{ node.value | hex }}',
|
27
|
+
'Paren': '({{ node.expr | expr_render }})',
|
28
|
+
'UFunc': '{{ node.func }}({{ node.expr | expr_render }})',
|
29
|
+
'Name': '{{ node.name }}',
|
30
|
+
'UnaryOp': '{{ node.op }}{{ node.expr | expr_render }}',
|
31
|
+
'BinaryOp': '{{ node.expr1 | expr_render }} {{ node.op }} {{ node.expr2 | expr_render }}',
|
32
|
+
'ConditionalOp': '({{ node.cond | expr_render }}) ? {{ node.value_true | expr_render }} : {{ node.value_false | expr_render }}',
|
33
|
+
}
|
34
|
+
|
35
|
+
_C_STYLE = {
|
36
|
+
**_DSL_STYLE,
|
37
|
+
'Boolean': '{{ (1 if node.value else 0) | hex }}',
|
38
|
+
'BinaryOp(**)': 'pow({{ node.expr1 | expr_render }}, {{ node.expr2 | expr_render }})',
|
39
|
+
}
|
40
|
+
|
41
|
+
_PY_STYLE = {
|
42
|
+
**_DSL_STYLE,
|
43
|
+
'UFunc': 'math.{{ node.func }}({{ node.expr | expr_render }})',
|
44
|
+
'UnaryOp(!)': 'not {{ node.expr | expr_render }}',
|
45
|
+
'BinaryOp(&&)': '{{ node.expr1 | expr_render }} and {{ node.expr2 | expr_render }}',
|
46
|
+
'BinaryOp(||)': '{{ node.expr1 | expr_render }} or {{ node.expr2 | expr_render }}',
|
47
|
+
'ConditionalOp': '{{ node.value_true | expr_render }} if {{ node.cond | expr_render }} else {{ node.value_false | expr_render }}',
|
48
|
+
}
|
49
|
+
|
50
|
+
_KNOWN_STYLES = {
|
51
|
+
'dsl': _DSL_STYLE,
|
52
|
+
'c': _C_STYLE,
|
53
|
+
'cpp': _C_STYLE,
|
54
|
+
'python': _PY_STYLE,
|
55
|
+
}
|
56
|
+
|
57
|
+
|
58
|
+
def fn_expr_render(node: Union[float, int, dict, dsl_nodes.Expr, Any], templates: Dict[str, str],
|
59
|
+
env: jinja2.Environment):
|
60
|
+
"""
|
61
|
+
Render an expression node using the provided templates and Jinja2 environment.
|
62
|
+
|
63
|
+
This function handles different types of expression nodes and selects the appropriate
|
64
|
+
template for rendering based on the node type and available templates.
|
65
|
+
|
66
|
+
:param node: The expression node to render, can be a DSL node or a primitive value
|
67
|
+
:type node: Union[float, int, dict, dsl_nodes.Expr, Any]
|
68
|
+
|
69
|
+
:param templates: Dictionary mapping node types to Jinja2 template strings
|
70
|
+
:type templates: Dict[str, str]
|
71
|
+
|
72
|
+
:param env: Jinja2 environment for template rendering
|
73
|
+
:type env: jinja2.Environment
|
74
|
+
|
75
|
+
:return: The rendered string representation of the expression node
|
76
|
+
:rtype: str
|
77
|
+
|
78
|
+
Example::
|
79
|
+
|
80
|
+
>>> env = create_env()
|
81
|
+
>>> templates = _DSL_STYLE
|
82
|
+
>>> fn_expr_render(Integer(42).to_ast_node(), templates, env)
|
83
|
+
'42'
|
84
|
+
"""
|
85
|
+
if isinstance(node, dsl_nodes.Expr):
|
86
|
+
if isinstance(node, (dsl_nodes.Float, dsl_nodes.Integer, dsl_nodes.Boolean, dsl_nodes.Constant,
|
87
|
+
dsl_nodes.HexInt, dsl_nodes.Paren, dsl_nodes.Name, dsl_nodes.ConditionalOp)) \
|
88
|
+
and type(node).__name__ in templates:
|
89
|
+
template_str = templates[type(node).__name__]
|
90
|
+
elif isinstance(node, dsl_nodes.UFunc) and f'{type(node).__name__}({node.func})' in templates:
|
91
|
+
template_str = templates[f'{type(node).__name__}({node.func})']
|
92
|
+
elif isinstance(node, dsl_nodes.UFunc) and type(node).__name__ in templates:
|
93
|
+
template_str = templates[type(node).__name__]
|
94
|
+
elif isinstance(node, dsl_nodes.UnaryOp) and f'{type(node).__name__}({node.op})' in templates:
|
95
|
+
template_str = templates[f'{type(node).__name__}({node.op})']
|
96
|
+
elif isinstance(node, dsl_nodes.UnaryOp) and type(node).__name__ in templates:
|
97
|
+
template_str = templates[type(node).__name__]
|
98
|
+
elif isinstance(node, dsl_nodes.BinaryOp) and f'{type(node).__name__}({node.op})' in templates:
|
99
|
+
template_str = templates[f'{type(node).__name__}({node.op})']
|
100
|
+
elif isinstance(node, dsl_nodes.BinaryOp) and type(node).__name__ in templates:
|
101
|
+
template_str = templates[type(node).__name__]
|
102
|
+
else:
|
103
|
+
template_str = templates['default']
|
104
|
+
|
105
|
+
tp: jinja2.Template = env.from_string(template_str)
|
106
|
+
return tp.render(node=node)
|
107
|
+
|
108
|
+
elif isinstance(node, bool):
|
109
|
+
return fn_expr_render(Boolean(node).to_ast_node(), templates=templates, env=env)
|
110
|
+
elif isinstance(node, int):
|
111
|
+
return fn_expr_render(Integer(node).to_ast_node(), templates=templates, env=env)
|
112
|
+
elif isinstance(node, float):
|
113
|
+
return fn_expr_render(Float(node).to_ast_node(), templates=templates, env=env)
|
114
|
+
else:
|
115
|
+
return repr(node)
|
116
|
+
|
117
|
+
|
118
|
+
def create_expr_render_template(lang_style: str = 'dsl', ext_configs: Optional[Dict[str, str]] = None):
|
119
|
+
"""
|
120
|
+
Create a template dictionary for expression rendering based on the specified language style.
|
121
|
+
|
122
|
+
This function combines the predefined templates for the specified language style with
|
123
|
+
any additional custom templates provided in ext_configs.
|
124
|
+
|
125
|
+
:param lang_style: The language style to use ('dsl', 'c', 'cpp', 'python')
|
126
|
+
:type lang_style: str
|
127
|
+
|
128
|
+
:param ext_configs: Optional additional template configurations to extend or override defaults
|
129
|
+
:type ext_configs: Optional[Dict[str, str]]
|
130
|
+
|
131
|
+
:return: A dictionary of templates for the specified language style
|
132
|
+
:rtype: Dict[str, str]
|
133
|
+
|
134
|
+
Example::
|
135
|
+
|
136
|
+
>>> templates = create_expr_render_template('python', {'CustomNode': '{{ node.custom_value }}'})
|
137
|
+
>>> 'UFunc' in templates and 'CustomNode' in templates
|
138
|
+
True
|
139
|
+
"""
|
140
|
+
return {**_KNOWN_STYLES[lang_style], **(ext_configs or {})}
|
141
|
+
|
142
|
+
|
143
|
+
def render_expr_node(expr: Union[float, int, dict, dsl_nodes.Expr, Any],
|
144
|
+
lang_style: str = 'dsl', ext_configs: Optional[Dict[str, str]] = None,
|
145
|
+
env: Optional[jinja2.Environment] = None):
|
146
|
+
"""
|
147
|
+
Render an expression node to a string representation in the specified language style.
|
148
|
+
|
149
|
+
This is a high-level function that sets up the environment and renders the expression
|
150
|
+
in one step. It's a convenient wrapper around add_expr_render_to_env and fn_expr_render.
|
151
|
+
|
152
|
+
:param expr: The expression to render
|
153
|
+
:type expr: Union[float, int, dict, dsl_nodes.Expr, Any]
|
154
|
+
|
155
|
+
:param lang_style: The language style to use ('dsl', 'c', 'cpp', 'python')
|
156
|
+
:type lang_style: str
|
157
|
+
|
158
|
+
:param ext_configs: Optional additional template configurations
|
159
|
+
:type ext_configs: Optional[Dict[str, str]]
|
160
|
+
|
161
|
+
:param env: Optional pre-configured Jinja2 environment
|
162
|
+
:type env: Optional[jinja2.Environment]
|
163
|
+
|
164
|
+
:return: The rendered string representation of the expression
|
165
|
+
:rtype: str
|
166
|
+
|
167
|
+
Example::
|
168
|
+
|
169
|
+
>>> from pyfcstm.dsl import Integer
|
170
|
+
>>> render_expr_node(Integer('42'), lang_style='python')
|
171
|
+
'42'
|
172
|
+
>>> render_expr_node(Integer('42'), lang_style='c')
|
173
|
+
'42'
|
174
|
+
"""
|
175
|
+
env = env or create_env()
|
176
|
+
templates = create_expr_render_template(lang_style, ext_configs)
|
177
|
+
_fn_expr_render = partial(fn_expr_render, templates=templates, env=env)
|
178
|
+
env.globals['expr_render'] = _fn_expr_render
|
179
|
+
env.filters['expr_render'] = _fn_expr_render
|
180
|
+
return _fn_expr_render(node=expr)
|
pyfcstm/render/func.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Module for processing items to objects with Jinja2 templates and dynamic imports.
|
3
|
+
|
4
|
+
This module provides functionality to convert dictionary configurations into callable objects,
|
5
|
+
templates, or imported objects based on their specified type. It supports creating template
|
6
|
+
renderers, importing objects from modules, and extracting values from configuration dictionaries.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import jinja2
|
10
|
+
from hbutils.reflection import quick_import_object
|
11
|
+
|
12
|
+
|
13
|
+
def process_item_to_object(f, env: jinja2.Environment):
|
14
|
+
"""
|
15
|
+
Process a configuration item into an object based on its type.
|
16
|
+
|
17
|
+
This function converts dictionary configurations into different types of objects:
|
18
|
+
|
19
|
+
- 'template': Creates a callable template renderer function
|
20
|
+
- 'import': Imports an object from a specified module
|
21
|
+
- 'value': Extracts a value from the configuration
|
22
|
+
- For other types or non-dictionary inputs, returns the input unchanged
|
23
|
+
|
24
|
+
:param f: The configuration item to process, can be a dictionary or any other type
|
25
|
+
:type f: dict or any
|
26
|
+
|
27
|
+
:param env: The Jinja2 environment used for template rendering
|
28
|
+
:type env: jinja2.Environment
|
29
|
+
|
30
|
+
:return: The processed object (function, imported object, value, or unchanged input)
|
31
|
+
:rtype: any
|
32
|
+
|
33
|
+
Example::
|
34
|
+
|
35
|
+
>>> env = jinja2.Environment()
|
36
|
+
>>> # Create a template renderer
|
37
|
+
>>> template_config = {'type': 'template', 'template': 'Hello {{ name }}', 'params': ['name']}
|
38
|
+
>>> renderer = process_item_to_object(template_config, env)
|
39
|
+
>>> renderer('World')
|
40
|
+
'Hello World'
|
41
|
+
|
42
|
+
>>> # Import an object
|
43
|
+
>>> import_config = {'type': 'import', 'from': 'math.sqrt'}
|
44
|
+
>>> sqrt_fn = process_item_to_object(import_config, env)
|
45
|
+
>>> sqrt_fn(16)
|
46
|
+
4.0
|
47
|
+
"""
|
48
|
+
if isinstance(f, dict):
|
49
|
+
type_ = f.pop('type', None)
|
50
|
+
if type_ == 'template':
|
51
|
+
params = f.pop('params', None)
|
52
|
+
template = f.pop('template')
|
53
|
+
if params is not None: # with params order
|
54
|
+
obj_template = env.from_string(template)
|
55
|
+
|
56
|
+
def _fn_render(*args, **kwargs):
|
57
|
+
render_args = dict(zip(params, args))
|
58
|
+
return obj_template.render(**render_args, **kwargs)
|
59
|
+
|
60
|
+
return _fn_render
|
61
|
+
|
62
|
+
else: # no params order
|
63
|
+
return env.from_string(template).render
|
64
|
+
|
65
|
+
elif type_ == 'import':
|
66
|
+
from_ = f.pop('from')
|
67
|
+
obj, _, _ = quick_import_object(from_)
|
68
|
+
return obj
|
69
|
+
|
70
|
+
elif type_ == 'value':
|
71
|
+
value = f.pop('value')
|
72
|
+
return value
|
73
|
+
|
74
|
+
else:
|
75
|
+
return f
|
76
|
+
else:
|
77
|
+
return f
|
pyfcstm/render/render.py
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
"""
|
2
|
+
State Machine Code Renderer Module
|
3
|
+
|
4
|
+
This module provides functionality for rendering state machine models into code using templates.
|
5
|
+
It uses Jinja2 templating engine to transform state machine models into various programming
|
6
|
+
languages and formats based on template configurations.
|
7
|
+
|
8
|
+
Template Directory Structure:
|
9
|
+
The template directory should contain:
|
10
|
+
|
11
|
+
- ``config.yaml``: Configuration file for the renderer
|
12
|
+
- ``*.j2`` files: Jinja2 templates that will be rendered
|
13
|
+
- Other files: Will be copied directly to the output directory
|
14
|
+
|
15
|
+
Configuration File (config.yaml) Structure:
|
16
|
+
- ``expr_styles``: Dictionary defining expression rendering styles
|
17
|
+
- ``default``: Default style configuration (will use 'dsl' as base_lang if not specified)
|
18
|
+
|
19
|
+
- ``[style_name]``: Additional named styles
|
20
|
+
- base_lang: Base language style ('dsl', 'c', 'cpp', 'python')
|
21
|
+
- [additional options]: Extra rendering options for the style
|
22
|
+
- ``globals``: Dictionary of global variables to be added to the Jinja2 environment
|
23
|
+
Each entry can be:
|
24
|
+
|
25
|
+
- ``type: template``: A template renderer function
|
26
|
+
- params: List[str], means the parameter list of this template rendering function, e.g. ``['a', 'b']``.
|
27
|
+
- template: str, means the Jinja2-format text render template, e.g. ``{{ a + b * 2 }}``.
|
28
|
+
- ``type: import``: An imported object
|
29
|
+
- from: str, means the import position, e.g. `math.sin`.
|
30
|
+
- ``type: value``: A direct value
|
31
|
+
- value: Any, means any possible value, e.g. ``1``, ``'Hello World'``.
|
32
|
+
- Other values (e.g. ``1``, ``'Hello World'``) means directly this value itself.
|
33
|
+
|
34
|
+
- ``filters``: Dictionary of filter functions to be added to the Jinja2 environment
|
35
|
+
(Same format as globals)
|
36
|
+
|
37
|
+
- ``tests``: Dictionary of test functions to be added to the Jinja2 environment
|
38
|
+
(Same format as globals)
|
39
|
+
|
40
|
+
- ``ignores``: List of file patterns to ignore (using gitignore syntax, e.g. ``.git``, ``*.md``, etc)
|
41
|
+
|
42
|
+
Expression Rendering:
|
43
|
+
The expr_styles configuration allows customizing how expressions are rendered in different
|
44
|
+
language styles. The base_lang determines the starting template set, which can be extended
|
45
|
+
or overridden with additional configuration.
|
46
|
+
|
47
|
+
And you can use these pre-defined styles in ``expr_render`` function/filter in the template.
|
48
|
+
When use ``{{ expression | expr_render }}`` it will use ``default`` style to render your expression.
|
49
|
+
When use ``{{ expression | expr_render(style='c') }}`` it will use ``c`` style to render your expression.
|
50
|
+
|
51
|
+
"""
|
52
|
+
import copy
|
53
|
+
import os.path
|
54
|
+
import pathlib
|
55
|
+
import shutil
|
56
|
+
import warnings
|
57
|
+
from functools import partial
|
58
|
+
from typing import Dict, Callable, Union, Any
|
59
|
+
|
60
|
+
import pathspec
|
61
|
+
import yaml
|
62
|
+
|
63
|
+
from .env import create_env
|
64
|
+
from .expr import create_expr_render_template, fn_expr_render, _KNOWN_STYLES
|
65
|
+
from .func import process_item_to_object
|
66
|
+
from ..dsl import node as dsl_nodes
|
67
|
+
from ..model import StateMachine
|
68
|
+
|
69
|
+
|
70
|
+
class StateMachineCodeRenderer:
|
71
|
+
"""
|
72
|
+
Renderer for generating code from state machine models using templates.
|
73
|
+
|
74
|
+
This class handles the rendering of state machine models into code using
|
75
|
+
Jinja2 templates. It supports custom expression styles, global functions,
|
76
|
+
filters, and tests through a configuration file.
|
77
|
+
|
78
|
+
:param template_dir: Directory containing the templates and configuration
|
79
|
+
:type template_dir: str
|
80
|
+
|
81
|
+
:param config_file: Name of the configuration file within the template directory
|
82
|
+
:type config_file: str, default: 'config.yaml'
|
83
|
+
"""
|
84
|
+
|
85
|
+
def __init__(self, template_dir: str, config_file: str = 'config.yaml'):
|
86
|
+
"""
|
87
|
+
Initialize the StateMachineCodeRenderer.
|
88
|
+
|
89
|
+
:param template_dir: Directory containing the templates and configuration
|
90
|
+
:type template_dir: str
|
91
|
+
|
92
|
+
:param config_file: Name of the configuration file within the template directory
|
93
|
+
:type config_file: str, default: 'config.yaml'
|
94
|
+
"""
|
95
|
+
self.template_dir = os.path.abspath(template_dir)
|
96
|
+
self.config_file = os.path.join(self.template_dir, config_file)
|
97
|
+
|
98
|
+
self.env = create_env()
|
99
|
+
self._ignore_patterns = ['.git']
|
100
|
+
self._prepare_for_configs()
|
101
|
+
|
102
|
+
self._path_spec = pathspec.PathSpec.from_lines(
|
103
|
+
pathspec.patterns.GitWildMatchPattern, self._ignore_patterns
|
104
|
+
)
|
105
|
+
|
106
|
+
self._file_mappings: Dict[str, Callable] = {}
|
107
|
+
self._prepare_for_file_mapping()
|
108
|
+
|
109
|
+
def _prepare_for_configs(self):
|
110
|
+
"""
|
111
|
+
Load and process the configuration file.
|
112
|
+
|
113
|
+
This method reads the configuration file, sets up expression rendering styles,
|
114
|
+
and registers globals, filters, and tests in the Jinja2 environment.
|
115
|
+
|
116
|
+
:raises FileNotFoundError: If the configuration file does not exist
|
117
|
+
:raises yaml.YAMLError: If the configuration file contains invalid YAML
|
118
|
+
"""
|
119
|
+
with open(self.config_file, 'r') as f:
|
120
|
+
config_info = yaml.safe_load(f)
|
121
|
+
|
122
|
+
expr_styles = config_info.pop('expr_styles', None) or {}
|
123
|
+
expr_styles['default'] = expr_styles.get('default') or {'base_lang': 'dsl'}
|
124
|
+
d_templates = copy.deepcopy(_KNOWN_STYLES)
|
125
|
+
for style_name, expr_style in expr_styles.items():
|
126
|
+
lang_style = expr_style.pop('base_lang')
|
127
|
+
d_templates[style_name] = create_expr_render_template(
|
128
|
+
lang_style=lang_style,
|
129
|
+
ext_configs=expr_style,
|
130
|
+
)
|
131
|
+
|
132
|
+
def _fn_expr_render(node: Union[float, int, dict, dsl_nodes.Expr, Any], style: str = 'default'):
|
133
|
+
"""
|
134
|
+
Render an expression node using the specified style.
|
135
|
+
|
136
|
+
:param node: The expression node to render
|
137
|
+
:type node: Union[float, int, dict, dsl_nodes.Expr, Any]
|
138
|
+
|
139
|
+
:param style: The expression rendering style to use
|
140
|
+
:type style: str, default: 'default'
|
141
|
+
|
142
|
+
:return: The rendered expression as a string
|
143
|
+
:rtype: str
|
144
|
+
"""
|
145
|
+
return fn_expr_render(
|
146
|
+
node=node,
|
147
|
+
templates=d_templates[style],
|
148
|
+
env=self.env,
|
149
|
+
)
|
150
|
+
|
151
|
+
self.env.globals['expr_render'] = _fn_expr_render
|
152
|
+
self.env.filters['expr_render'] = _fn_expr_render
|
153
|
+
|
154
|
+
globals_ = config_info.pop('globals', None) or {}
|
155
|
+
for name, value in globals_.items():
|
156
|
+
self.env.globals[name] = process_item_to_object(value, env=self.env)
|
157
|
+
filters_ = config_info.pop('filters', None) or {}
|
158
|
+
for name, value in filters_.items():
|
159
|
+
self.env.filters[name] = process_item_to_object(value, env=self.env)
|
160
|
+
tests = config_info.pop('tests', None) or {}
|
161
|
+
for name, value in tests.items():
|
162
|
+
self.env.tests[name] = process_item_to_object(value, env=self.env)
|
163
|
+
|
164
|
+
ignores = list(config_info.pop('ignores', None) or [])
|
165
|
+
self._ignore_patterns.extend(ignores)
|
166
|
+
|
167
|
+
def _prepare_for_file_mapping(self):
|
168
|
+
"""
|
169
|
+
Prepare file mappings for rendering or copying.
|
170
|
+
|
171
|
+
This method walks through the template directory and creates mappings for:
|
172
|
+
- .j2 files: Will be rendered using Jinja2
|
173
|
+
- Other files: Will be copied directly to the output directory
|
174
|
+
|
175
|
+
Files matching the ignore patterns will be excluded.
|
176
|
+
"""
|
177
|
+
for root, _, files in os.walk(self.template_dir):
|
178
|
+
for file in files:
|
179
|
+
_, ext = os.path.splitext(file)
|
180
|
+
current_file = os.path.abspath(os.path.join(root, file))
|
181
|
+
rel_file = os.path.relpath(current_file, self.template_dir)
|
182
|
+
if self._path_spec.match_file(rel_file):
|
183
|
+
continue
|
184
|
+
if ext == '.j2':
|
185
|
+
rel_file = os.path.splitext(rel_file)[0]
|
186
|
+
self._file_mappings[rel_file] = partial(
|
187
|
+
self.render_one_file,
|
188
|
+
template_file=current_file,
|
189
|
+
)
|
190
|
+
elif not os.path.samefile(current_file, self.config_file):
|
191
|
+
self._file_mappings[rel_file] = partial(
|
192
|
+
self.copy_one_file,
|
193
|
+
src_file=current_file,
|
194
|
+
)
|
195
|
+
|
196
|
+
def render_one_file(self, model: StateMachine, output_file: str, template_file: str):
|
197
|
+
"""
|
198
|
+
Render a single template file.
|
199
|
+
|
200
|
+
:param model: The state machine model to render
|
201
|
+
:type model: StateMachine
|
202
|
+
|
203
|
+
:param output_file: Path to the output file
|
204
|
+
:type output_file: str
|
205
|
+
|
206
|
+
:param template_file: Path to the template file
|
207
|
+
:type template_file: str
|
208
|
+
|
209
|
+
:raises jinja2.exceptions.TemplateError: If there's an error in the template
|
210
|
+
:raises IOError: If there's an error reading or writing files
|
211
|
+
"""
|
212
|
+
tp = self.env.from_string(pathlib.Path(template_file).read_text())
|
213
|
+
if os.path.dirname(output_file):
|
214
|
+
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
215
|
+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
216
|
+
with open(output_file, 'w') as f:
|
217
|
+
f.write(tp.render(model=model))
|
218
|
+
|
219
|
+
def copy_one_file(self, model: StateMachine, output_file: str, src_file: str):
|
220
|
+
"""
|
221
|
+
Copy a single file to the output directory.
|
222
|
+
|
223
|
+
:param model: The state machine model (unused in this method)
|
224
|
+
:type model: StateMachine
|
225
|
+
|
226
|
+
:param output_file: Path to the output file
|
227
|
+
:type output_file: str
|
228
|
+
|
229
|
+
:param src_file: Path to the source file
|
230
|
+
:type src_file: str
|
231
|
+
|
232
|
+
:raises IOError: If there's an error copying the file
|
233
|
+
"""
|
234
|
+
_ = model
|
235
|
+
if os.path.dirname(output_file):
|
236
|
+
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
237
|
+
shutil.copyfile(src_file, output_file)
|
238
|
+
|
239
|
+
def render(self, model: StateMachine, output_dir: str, clear_previous_directory: bool = False):
|
240
|
+
"""
|
241
|
+
Render the state machine model to the output directory.
|
242
|
+
|
243
|
+
This method processes all template files and copies all other files
|
244
|
+
from the template directory to the output directory according to the
|
245
|
+
configured mappings.
|
246
|
+
|
247
|
+
:param model: The state machine model to render
|
248
|
+
:type model: StateMachine
|
249
|
+
|
250
|
+
:param output_dir: Directory where the rendered files will be placed
|
251
|
+
:type output_dir: str
|
252
|
+
|
253
|
+
:param clear_previous_directory: Whether to clear the output directory before rendering
|
254
|
+
:type clear_previous_directory: bool, default: False
|
255
|
+
|
256
|
+
:raises IOError: If there's an error accessing or writing to the output directory
|
257
|
+
|
258
|
+
Example::
|
259
|
+
|
260
|
+
>>> renderer = StateMachineCodeRenderer('./templates')
|
261
|
+
>>> renderer.render(my_state_machine, './output', clear_previous_directory=True)
|
262
|
+
"""
|
263
|
+
output_dir = os.path.abspath(output_dir)
|
264
|
+
os.makedirs(output_dir, exist_ok=True)
|
265
|
+
if clear_previous_directory:
|
266
|
+
for file in os.listdir(output_dir):
|
267
|
+
dst_file = os.path.join(output_dir, file)
|
268
|
+
if os.path.isfile(dst_file):
|
269
|
+
os.remove(dst_file)
|
270
|
+
elif os.path.isdir(dst_file):
|
271
|
+
shutil.rmtree(dst_file)
|
272
|
+
elif os.path.islink(dst_file):
|
273
|
+
os.unlink(dst_file)
|
274
|
+
else:
|
275
|
+
warnings.warn(f'Unable to clean file {dst_file!r}.') # pragma: no cover
|
276
|
+
|
277
|
+
for rel_file, fn_op in self._file_mappings.items():
|
278
|
+
dst_file = os.path.join(output_dir, rel_file)
|
279
|
+
fn_op(model=model, output_file=dst_file)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
from .binary import is_binary_file
|
2
|
+
from .doc import format_multiline_comment
|
3
|
+
from .jinja2 import add_builtins_to_env, add_settings_for_env
|
4
|
+
from .json import IJsonOp
|
5
|
+
from .text import normalize, to_identifier
|
6
|
+
from .validate import ValidationError, ModelValidationError, IValidatable
|
pyfcstm/utils/binary.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
"""
|
2
|
+
This module provides functionality to determine whether a given file is a binary file or a text file.
|
3
|
+
It does so by reading the first 1024 bytes of the file and checking for the presence of non-text characters.
|
4
|
+
|
5
|
+
.. note::
|
6
|
+
Inspired from https://stackoverflow.com/a/7392391/6995899
|
7
|
+
"""
|
8
|
+
|
9
|
+
_TEXT_CHARS = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
|
10
|
+
|
11
|
+
|
12
|
+
def is_binary_file(file) -> bool:
|
13
|
+
"""
|
14
|
+
Check if a given file is binary.
|
15
|
+
|
16
|
+
This function reads the first 1024 bytes of the file and checks if it contains any
|
17
|
+
non-text (binary) characters. It uses a predefined set of text characters to determine
|
18
|
+
the nature of the file.
|
19
|
+
|
20
|
+
:param file: The path to the file to be checked.
|
21
|
+
:type file: str
|
22
|
+
|
23
|
+
:return: True if the file is binary, False if it is a text file.
|
24
|
+
:rtype: bool
|
25
|
+
|
26
|
+
:raises FileNotFoundError: If the specified file does not exist.
|
27
|
+
:raises IOError: If there is an error reading the file.
|
28
|
+
|
29
|
+
:example:
|
30
|
+
|
31
|
+
>>> is_binary_file('example.txt')
|
32
|
+
False
|
33
|
+
>>> is_binary_file('example.bin')
|
34
|
+
True
|
35
|
+
"""
|
36
|
+
with open(file, 'rb') as f:
|
37
|
+
prefix = f.read(1024)
|
38
|
+
return bool(prefix.translate(None, _TEXT_CHARS))
|
pyfcstm/utils/doc.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Provides functionality for formatting multiline comments in code.
|
3
|
+
|
4
|
+
This module contains utilities for cleaning and formatting multiline comments
|
5
|
+
that have been parsed from source code, particularly those extracted by ANTLR4.
|
6
|
+
It handles removing comment markers, aligning indentation, and cleaning up
|
7
|
+
whitespace to produce readable documentation text.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
import re
|
12
|
+
import textwrap
|
13
|
+
|
14
|
+
|
15
|
+
def format_multiline_comment(raw_doc):
|
16
|
+
"""
|
17
|
+
Format multiline comments parsed by ANTLR4 by removing comment markers
|
18
|
+
and aligning indentation.
|
19
|
+
|
20
|
+
This function takes a raw multiline comment (including /* */ markers) and
|
21
|
+
processes it to produce clean, properly formatted documentation text.
|
22
|
+
It removes comment delimiters, trims unnecessary whitespace, and
|
23
|
+
normalizes indentation.
|
24
|
+
|
25
|
+
:param raw_doc: Raw comment text including /* */ markers
|
26
|
+
:type raw_doc: str
|
27
|
+
|
28
|
+
:return: Formatted comment text with markers removed and proper indentation
|
29
|
+
:rtype: str
|
30
|
+
|
31
|
+
Example::
|
32
|
+
|
33
|
+
>>> raw = \"\"\"/* This is a
|
34
|
+
... * multiline comment
|
35
|
+
... */\"\"\"
|
36
|
+
>>> format_multiline_comment(raw)
|
37
|
+
'This is a\\nmultiline comment'
|
38
|
+
"""
|
39
|
+
if re.fullmatch(r'\s*/\*+/\s*', raw_doc.strip()):
|
40
|
+
return ""
|
41
|
+
|
42
|
+
# Use regex to remove opening comment markers (/* with one or more asterisks)
|
43
|
+
content = re.sub(r'^\s*/\*+', '', raw_doc.strip())
|
44
|
+
|
45
|
+
# Use regex to remove closing comment markers
|
46
|
+
content = re.sub(r'\*+/\s*$', '', content)
|
47
|
+
|
48
|
+
# Split into lines
|
49
|
+
lines = content.splitlines()
|
50
|
+
|
51
|
+
i = 0
|
52
|
+
while i < len(lines) and not lines[i].strip():
|
53
|
+
i += 1
|
54
|
+
lines = lines[i:]
|
55
|
+
|
56
|
+
i = len(lines) - 1
|
57
|
+
while i > 0 and not lines[i].strip():
|
58
|
+
i -= 1
|
59
|
+
lines = lines[:i + 1]
|
60
|
+
|
61
|
+
# Use textwrap.dedent to align indentation
|
62
|
+
formatted_text = textwrap.dedent(os.linesep.join(map(str.rstrip, lines)))
|
63
|
+
|
64
|
+
return formatted_text
|