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.
Files changed (46) hide show
  1. pyfcstm/__init__.py +0 -0
  2. pyfcstm/__main__.py +4 -0
  3. pyfcstm/config/__init__.py +0 -0
  4. pyfcstm/config/meta.py +20 -0
  5. pyfcstm/dsl/__init__.py +6 -0
  6. pyfcstm/dsl/error.py +226 -0
  7. pyfcstm/dsl/grammar/Grammar.g4 +190 -0
  8. pyfcstm/dsl/grammar/Grammar.interp +168 -0
  9. pyfcstm/dsl/grammar/Grammar.tokens +118 -0
  10. pyfcstm/dsl/grammar/GrammarLexer.interp +214 -0
  11. pyfcstm/dsl/grammar/GrammarLexer.py +523 -0
  12. pyfcstm/dsl/grammar/GrammarLexer.tokens +118 -0
  13. pyfcstm/dsl/grammar/GrammarListener.py +521 -0
  14. pyfcstm/dsl/grammar/GrammarParser.py +4373 -0
  15. pyfcstm/dsl/grammar/__init__.py +3 -0
  16. pyfcstm/dsl/listener.py +440 -0
  17. pyfcstm/dsl/node.py +1581 -0
  18. pyfcstm/dsl/parse.py +155 -0
  19. pyfcstm/entry/__init__.py +1 -0
  20. pyfcstm/entry/base.py +126 -0
  21. pyfcstm/entry/cli.py +12 -0
  22. pyfcstm/entry/dispatch.py +46 -0
  23. pyfcstm/entry/generate.py +83 -0
  24. pyfcstm/entry/plantuml.py +67 -0
  25. pyfcstm/model/__init__.py +3 -0
  26. pyfcstm/model/base.py +51 -0
  27. pyfcstm/model/expr.py +764 -0
  28. pyfcstm/model/model.py +1392 -0
  29. pyfcstm/render/__init__.py +3 -0
  30. pyfcstm/render/env.py +36 -0
  31. pyfcstm/render/expr.py +180 -0
  32. pyfcstm/render/func.py +77 -0
  33. pyfcstm/render/render.py +279 -0
  34. pyfcstm/utils/__init__.py +6 -0
  35. pyfcstm/utils/binary.py +38 -0
  36. pyfcstm/utils/doc.py +64 -0
  37. pyfcstm/utils/jinja2.py +121 -0
  38. pyfcstm/utils/json.py +125 -0
  39. pyfcstm/utils/text.py +91 -0
  40. pyfcstm/utils/validate.py +102 -0
  41. pyfcstm-0.0.1.dist-info/LICENSE +165 -0
  42. pyfcstm-0.0.1.dist-info/METADATA +205 -0
  43. pyfcstm-0.0.1.dist-info/RECORD +46 -0
  44. pyfcstm-0.0.1.dist-info/WHEEL +5 -0
  45. pyfcstm-0.0.1.dist-info/entry_points.txt +2 -0
  46. pyfcstm-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ from .env import create_env
2
+ from .expr import render_expr_node, fn_expr_render, create_expr_render_template
3
+ from .render import StateMachineCodeRenderer
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
@@ -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
@@ -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