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
pyfcstm/dsl/parse.py ADDED
@@ -0,0 +1,155 @@
1
+ """
2
+ Grammar parsing module for processing and interpreting grammar-based input text.
3
+
4
+ This module provides functions to parse input text according to grammar rules defined
5
+ in the GrammarParser. It uses ANTLR4 for lexical analysis and parsing, and provides
6
+ specialized functions for parsing different grammar elements like conditions, preambles,
7
+ and operations.
8
+ """
9
+
10
+ from antlr4 import CommonTokenStream, InputStream, ParseTreeWalker
11
+
12
+ from .error import CollectingErrorListener
13
+ from .grammar import GrammarParser, GrammarLexer
14
+ from .listener import GrammarParseListener
15
+
16
+
17
+ def _parse_as_element(input_text, fn_element, force_finished: bool = True):
18
+ """
19
+ Parse input text using the specified grammar element function.
20
+
21
+ This internal function handles the common parsing logic for all grammar elements,
22
+ including error handling and parse tree walking.
23
+
24
+ :param input_text: The text to parse
25
+ :type input_text: str
26
+
27
+ :param fn_element: The parser function to use for parsing
28
+ :type fn_element: callable
29
+
30
+ :param force_finished: Whether to check if parsing consumed all input
31
+ :type force_finished: bool
32
+
33
+ :return: The parsed node representation of the input
34
+ :rtype: object
35
+
36
+ :raises: Various parsing errors if the input doesn't match the grammar
37
+ """
38
+ error_listener = CollectingErrorListener()
39
+
40
+ input_stream = InputStream(input_text)
41
+ lexer = GrammarLexer(input_stream)
42
+ lexer.removeErrorListeners()
43
+ lexer.addErrorListener(error_listener)
44
+
45
+ stream = CommonTokenStream(lexer)
46
+ parser = GrammarParser(stream)
47
+ parser.removeErrorListeners()
48
+ parser.addErrorListener(error_listener)
49
+
50
+ parse_tree = fn_element(parser)
51
+ if force_finished:
52
+ error_listener.check_unfinished_parsing_error(stream)
53
+ error_listener.check_errors()
54
+
55
+ listener = GrammarParseListener()
56
+ walker = ParseTreeWalker()
57
+ walker.walk(listener, parse_tree)
58
+ return listener.nodes[parse_tree]
59
+
60
+
61
+ def parse_with_grammar_entry(input_text: str, entry_name: str, force_finished: bool = True):
62
+ """
63
+ Parse input text using a specified grammar entry point.
64
+
65
+ This function allows parsing with any entry point in the grammar by name.
66
+
67
+ :param input_text: The text to parse
68
+ :type input_text: str
69
+
70
+ :param entry_name: The name of the grammar rule to use as entry point
71
+ :type entry_name: str
72
+
73
+ :param force_finished: Whether to check if parsing consumed all input
74
+ :type force_finished: bool
75
+
76
+ :return: The parsed node representation of the input
77
+ :rtype: object
78
+
79
+ :raises: Various parsing errors if the input doesn't match the grammar
80
+
81
+ Example::
82
+
83
+ >>> result = parse_with_grammar_entry("x > 5", "condition")
84
+ """
85
+ return _parse_as_element(
86
+ input_text=input_text,
87
+ fn_element=getattr(GrammarParser, entry_name),
88
+ force_finished=force_finished
89
+ )
90
+
91
+
92
+ def parse_condition(input_text: str):
93
+ """
94
+ Parse input text as a condition expression.
95
+
96
+ This function specifically parses conditional expressions according to
97
+ the grammar's condition rule.
98
+
99
+ :param input_text: The condition text to parse
100
+ :type input_text: str
101
+
102
+ :return: The parsed condition node
103
+ :rtype: object
104
+
105
+ :raises: Various parsing errors if the input doesn't match the condition grammar
106
+
107
+ Example::
108
+
109
+ >>> condition_node = parse_condition("x > 5 && y < 10")
110
+ """
111
+ return parse_with_grammar_entry(input_text, 'condition')
112
+
113
+
114
+ def parse_preamble(input_text: str):
115
+ """
116
+ Parse input text as a preamble program.
117
+
118
+ This function specifically parses preamble programs according to
119
+ the grammar's preamble_program rule.
120
+
121
+ :param input_text: The preamble program text to parse
122
+ :type input_text: str
123
+
124
+ :return: The parsed preamble program node
125
+ :rtype: object
126
+
127
+ :raises: Various parsing errors if the input doesn't match the preamble grammar
128
+
129
+ Example::
130
+
131
+ >>> preamble_node = parse_preamble("x = 10;")
132
+ """
133
+ return parse_with_grammar_entry(input_text, 'preamble_program')
134
+
135
+
136
+ def parse_operation(input_text: str):
137
+ """
138
+ Parse input text as an operation program.
139
+
140
+ This function specifically parses operation programs according to
141
+ the grammar's operation_program rule.
142
+
143
+ :param input_text: The operation program text to parse
144
+ :type input_text: str
145
+
146
+ :return: The parsed operation program node
147
+ :rtype: object
148
+
149
+ :raises: Various parsing errors if the input doesn't match the operation grammar
150
+
151
+ Example::
152
+
153
+ >>> operation_node = parse_operation("x := 10;")
154
+ """
155
+ return parse_with_grammar_entry(input_text, 'operation_program')
@@ -0,0 +1 @@
1
+ from .cli import cli as pyfcstmcli
pyfcstm/entry/base.py ADDED
@@ -0,0 +1,126 @@
1
+ import builtins
2
+ import itertools
3
+ import os
4
+ import sys
5
+ import traceback
6
+ from functools import wraps, partial
7
+ from typing import Optional, IO, Callable
8
+
9
+ import click
10
+ from click.exceptions import ClickException
11
+
12
+ CONTEXT_SETTINGS = dict(
13
+ help_option_names=['-h', '--help']
14
+ )
15
+
16
+
17
+ class ClickWarningException(ClickException):
18
+ """
19
+ Custom exception class for displaying warnings in yellow color.
20
+
21
+ :param message: The error message.
22
+ :type message: str
23
+ """
24
+
25
+ def show(self, file: Optional[IO] = None) -> None:
26
+ """
27
+ Display the warning message in yellow.
28
+
29
+ :param file: File to write the output to.
30
+ :type file: Optional[IO]
31
+ """
32
+ click.secho(self.format_message(), fg='yellow', file=sys.stderr)
33
+
34
+
35
+ class ClickErrorException(ClickException):
36
+ """
37
+ Custom exception class for displaying errors in red color.
38
+
39
+ :param message: The error message.
40
+ :type message: str
41
+ """
42
+
43
+ def show(self, file: Optional[IO] = None) -> None:
44
+ """
45
+ Display the error message in red.
46
+
47
+ :param file: File to write the output to.
48
+ :type file: Optional[IO]
49
+ """
50
+ click.secho(self.format_message(), fg='red', file=sys.stderr)
51
+
52
+
53
+ def print_exception(err: BaseException, print: Optional[Callable] = None):
54
+ """
55
+ Print exception information, including traceback.
56
+
57
+ :param err: The exception object.
58
+ :type err: BaseException
59
+ :param print: Custom print function. If not provided, uses built-in print.
60
+ :type print: Optional[Callable]
61
+ """
62
+ print = print or builtins.print
63
+
64
+ lines = list(itertools.chain(*map(
65
+ lambda x: x.splitlines(keepends=False),
66
+ traceback.format_tb(err.__traceback__)
67
+ )))
68
+
69
+ if lines:
70
+ print('Traceback (most recent call last):')
71
+ print(os.linesep.join(lines))
72
+
73
+ if len(err.args) == 0:
74
+ print(f'{type(err).__name__}')
75
+ elif len(err.args) == 1:
76
+ print(f'{type(err).__name__}: {err.args[0]}')
77
+ else:
78
+ print(f'{type(err).__name__}: {err.args}')
79
+
80
+
81
+ class KeyboardInterrupted(ClickWarningException):
82
+ """
83
+ Exception class for handling keyboard interruptions.
84
+
85
+ :param msg: Custom message to display.
86
+ :type msg: Optional[str]
87
+ """
88
+ exit_code = 0x7
89
+
90
+ def __init__(self, msg=None):
91
+ """
92
+ Initialize the exception.
93
+
94
+ :param msg: Custom message to display.
95
+ :type msg: Optional[str]
96
+ """
97
+ ClickWarningException.__init__(self, msg or 'Interrupted.')
98
+
99
+
100
+ def command_wrap():
101
+ """
102
+ Decorator for wrapping Click commands.
103
+
104
+ This decorator catches exceptions and provides consistent error handling.
105
+
106
+ :return: Decorator function.
107
+ :rtype: Callable
108
+ """
109
+
110
+ def _decorator(func):
111
+ @wraps(func)
112
+ def _new_func(*args, **kwargs):
113
+ try:
114
+ return func(*args, **kwargs)
115
+ except ClickException:
116
+ raise
117
+ except KeyboardInterrupt:
118
+ raise KeyboardInterrupted
119
+ except BaseException as err:
120
+ click.secho('Unexpected error found when running pyfcstm!', fg='red', file=sys.stderr)
121
+ print_exception(err, partial(click.secho, fg='red', file=sys.stderr))
122
+ click.get_current_context().exit(1)
123
+
124
+ return _new_func
125
+
126
+ return _decorator
pyfcstm/entry/cli.py ADDED
@@ -0,0 +1,12 @@
1
+ from .dispatch import pyfcstmcli
2
+ from .generate import _add_generate_subcommand
3
+ from .plantuml import _add_plantuml_subcommand
4
+
5
+ _DECORATORS = [
6
+ _add_generate_subcommand,
7
+ _add_plantuml_subcommand,
8
+ ]
9
+
10
+ cli = pyfcstmcli
11
+ for deco in _DECORATORS:
12
+ cli = deco(cli)
@@ -0,0 +1,46 @@
1
+ import click
2
+ from click.core import Context, Option
3
+
4
+ from .base import CONTEXT_SETTINGS
5
+ from ..config.meta import __TITLE__, __VERSION__, __AUTHOR__, __AUTHOR_EMAIL__, __DESCRIPTION__
6
+
7
+ _raw_authors = [item.strip() for item in __AUTHOR__.split(',') if item.strip()]
8
+ _raw_emails = [item.strip() for item in __AUTHOR_EMAIL__.split(',')]
9
+ if len(_raw_emails) < len(_raw_authors): # pragma: no cover
10
+ _raw_emails += [None] * (len(_raw_authors) - len(_raw_emails))
11
+ elif len(_raw_emails) > len(_raw_authors): # pragma: no cover
12
+ _raw_emails[len(_raw_authors) - 1] = tuple(_raw_emails[len(_raw_authors) - 1:])
13
+ del _raw_emails[len(_raw_authors):]
14
+
15
+ _author_tuples = [
16
+ (author, tuple([item for item in (email if isinstance(email, tuple) else ((email,) if email else ())) if item]))
17
+ for author, email in zip(_raw_authors, _raw_emails)
18
+ ]
19
+ _authors = [
20
+ author if not emails else '{author} ({emails})'.format(author=author, emails=', '.join(emails))
21
+ for author, emails in _author_tuples
22
+ ]
23
+
24
+
25
+ # noinspection PyUnusedLocal
26
+ def print_version(ctx: Context, param: Option, value: bool) -> None:
27
+ """
28
+ Print version information of cli
29
+ :param ctx: click context
30
+ :param param: current parameter's metadata
31
+ :param value: value of current parameter
32
+ """
33
+ if not value or ctx.resilient_parsing:
34
+ return # pragma: no cover
35
+ click.echo('{title}, version {version}.'.format(title=__TITLE__.capitalize(), version=__VERSION__))
36
+ if _authors:
37
+ click.echo('Developed by {authors}.'.format(authors=', '.join(_authors)))
38
+ ctx.exit()
39
+
40
+
41
+ @click.group(context_settings=CONTEXT_SETTINGS, help=__DESCRIPTION__)
42
+ @click.option('-v', '--version', is_flag=True,
43
+ callback=print_version, expose_value=False, is_eager=True,
44
+ help="Show pyfcstm's version information.")
45
+ def pyfcstmcli():
46
+ pass # pragma: no cover
@@ -0,0 +1,83 @@
1
+ """
2
+ State Machine Code Generation Module
3
+
4
+ This module provides command line interface functionality for generating code from a state machine DSL.
5
+ It includes functionality to parse DSL code, convert it to a state machine model, and render the model
6
+ using templates to generate output code.
7
+ """
8
+
9
+ import pathlib
10
+
11
+ import click
12
+
13
+ from .base import CONTEXT_SETTINGS
14
+ from ..dsl import parse_with_grammar_entry
15
+ from ..model import parse_dsl_node_to_state_machine
16
+ from ..render import StateMachineCodeRenderer
17
+
18
+
19
+ def _add_generate_subcommand(cli: click.Group) -> click.Group:
20
+ """
21
+ Add the 'generate' subcommand to the CLI group.
22
+
23
+ This function adds a 'generate' subcommand to the provided CLI group that allows users to
24
+ generate code from a state machine DSL file using specified templates.
25
+
26
+ :param cli: The click Group object to which the subcommand will be added
27
+ :type cli: click.Group
28
+
29
+ :return: The modified CLI group with the 'generate' subcommand added
30
+ :rtype: click.Group
31
+
32
+ Example::
33
+
34
+ >>> app = click.Group()
35
+ >>> app = _add_generate_subcommand(app)
36
+ """
37
+
38
+ @cli.command('generate', help='Generate code with template of a given state machine DSL code.',
39
+ context_settings=CONTEXT_SETTINGS)
40
+ @click.option('-i', '--input-code', 'input_code_file', type=str, required=True,
41
+ help='Input code file of state machine DSL.')
42
+ @click.option('-t', '--template-dir', 'template_dir', type=str, required=True,
43
+ help='Template directory of the code generation.')
44
+ @click.option('-o', '--output-dir', 'output_dir', type=str, required=True,
45
+ help='Output directory of the code generation.')
46
+ @click.option('--clear', '--clear-directory', 'clear_directory', type=bool, is_flag=True,
47
+ help='Clear the destination directory of the output directory.')
48
+ def generate(input_code_file, template_dir, output_dir, clear_directory):
49
+ """
50
+ Generate code from a state machine DSL file using templates.
51
+
52
+ This function reads a state machine DSL file, parses it into an AST node,
53
+ converts the AST node to a state machine model, and renders the model using
54
+ templates to generate output code.
55
+
56
+ :param input_code_file: Path to the input DSL code file
57
+ :type input_code_file: str
58
+
59
+ :param template_dir: Path to the directory containing templates
60
+ :type template_dir: str
61
+
62
+ :param output_dir: Path to the directory where generated code will be written
63
+ :type output_dir: str
64
+
65
+ :param clear_directory: Whether to clear the output directory before generating code
66
+ :type clear_directory: bool
67
+
68
+ :return: None
69
+ """
70
+ code = pathlib.Path(input_code_file).read_text()
71
+ ast_node = parse_with_grammar_entry(code, entry_name='state_machine_dsl')
72
+ model = parse_dsl_node_to_state_machine(ast_node)
73
+
74
+ renderer = StateMachineCodeRenderer(
75
+ template_dir=template_dir,
76
+ )
77
+ renderer.render(
78
+ model,
79
+ output_dir=output_dir,
80
+ clear_previous_directory=clear_directory
81
+ )
82
+
83
+ return cli
@@ -0,0 +1,67 @@
1
+ """
2
+ State Machine DSL to PlantUML Converter Module
3
+
4
+ This module provides functionality to convert state machine DSL code into PlantUML format.
5
+ It includes a command-line interface for easy conversion of state machine DSL files
6
+ to PlantUML code, which can be used to generate visual state machine diagrams.
7
+ """
8
+
9
+ import pathlib
10
+
11
+ import click
12
+
13
+ from .base import CONTEXT_SETTINGS
14
+ from ..dsl import parse_with_grammar_entry
15
+ from ..model import parse_dsl_node_to_state_machine
16
+
17
+
18
+ def _add_plantuml_subcommand(cli: click.Group) -> click.Group:
19
+ """
20
+ Add the 'plantuml' subcommand to the provided CLI group.
21
+
22
+ This function adds a subcommand that converts state machine DSL code to PlantUML format.
23
+ The subcommand reads DSL code from a file, parses it into a state machine model,
24
+ and outputs the corresponding PlantUML code.
25
+
26
+ :param cli: The click Group to which the subcommand should be added
27
+ :type cli: click.Group
28
+
29
+ :return: The modified CLI group with the plantuml subcommand added
30
+ :rtype: click.Group
31
+
32
+ Example::
33
+
34
+ >>> from click import Group
35
+ >>> cli = Group()
36
+ >>> cli = _add_plantuml_subcommand(cli)
37
+ """
38
+
39
+ @cli.command('plantuml', help='Create Plantuml code of a given state machine DSL code.',
40
+ context_settings=CONTEXT_SETTINGS)
41
+ @click.option('-i', '--input-code', 'input_code_file', type=str, required=True,
42
+ help='Input code file of state machine DSL.')
43
+ @click.option('-o', '--output', 'output_file', type=str, default=None,
44
+ help='Output directory of the code generation, output to stdout when not assigned.')
45
+ def plantuml(input_code_file, output_file):
46
+ """
47
+ Convert state machine DSL code to PlantUML format.
48
+
49
+ This command reads state machine DSL code from the specified input file,
50
+ parses it into a state machine model, and outputs the corresponding PlantUML code
51
+ either to a file or to stdout.
52
+
53
+ :param input_code_file: Path to the file containing state machine DSL code
54
+ :type input_code_file: str
55
+ :param output_file: Path to the output file for PlantUML code (stdout if None)
56
+ :type output_file: str or None
57
+ """
58
+ code = pathlib.Path(input_code_file).read_text()
59
+ ast_node = parse_with_grammar_entry(code, entry_name='state_machine_dsl')
60
+ model = parse_dsl_node_to_state_machine(ast_node)
61
+ if output_file is not None:
62
+ with open(output_file, 'w') as f:
63
+ f.write(model.to_plantuml())
64
+ else:
65
+ click.echo(model.to_plantuml())
66
+
67
+ return cli
@@ -0,0 +1,3 @@
1
+ from .base import AstExportable, PlantUMLExportable
2
+ from .expr import *
3
+ from .model import *
pyfcstm/model/base.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ Module for defining exportable interfaces for AST and PlantUML formats.
3
+
4
+ This module provides abstract base classes that define interfaces for objects
5
+ that can be exported to Abstract Syntax Tree (AST) nodes or PlantUML diagram
6
+ representations.
7
+ """
8
+
9
+ from ..dsl import node as dsl_nodes
10
+
11
+
12
+ class AstExportable:
13
+ """
14
+ Abstract base class for objects that can be exported to AST nodes.
15
+
16
+ Classes that inherit from this interface should implement the to_ast_node
17
+ method to convert themselves into appropriate AST node representations.
18
+
19
+ :raises NotImplementedError: If the subclass does not implement the to_ast_node method.
20
+ """
21
+
22
+ def to_ast_node(self) -> dsl_nodes.ASTNode:
23
+ """
24
+ Convert the object to an AST node representation.
25
+
26
+ :return: An AST node representing this object
27
+ :rtype: dsl_nodes.ASTNode
28
+ :raises NotImplementedError: This is an abstract method that must be implemented by subclasses
29
+ """
30
+ raise NotImplementedError # pragma: no cover
31
+
32
+
33
+ class PlantUMLExportable:
34
+ """
35
+ Abstract base class for objects that can be exported to PlantUML format.
36
+
37
+ Classes that inherit from this interface should implement the to_plantuml
38
+ method to convert themselves into PlantUML diagram syntax.
39
+
40
+ :raises NotImplementedError: If the subclass does not implement the to_plantuml method.
41
+ """
42
+
43
+ def to_plantuml(self) -> str:
44
+ """
45
+ Convert the object to a PlantUML diagram representation.
46
+
47
+ :return: A string containing PlantUML syntax representing this object
48
+ :rtype: str
49
+ :raises NotImplementedError: This is an abstract method that must be implemented by subclasses
50
+ """
51
+ raise NotImplementedError # pragma: no cover