nemtus-catparser 3.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: nemtus-catparser
3
+ Version: 3.2.0
4
+ Summary: Symbol Catbuffer Parser
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: symbol,catbuffer,catparser,parser,catbuffer-parser
8
+ Author: Symbol Contributors
9
+ Author-email: contributors@symbol.dev
10
+ Maintainer: Symbol Contributors
11
+ Maintainer-email: contributors@symbol.dev
12
+ Requires-Python: >=3.10,<4.0
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Project-URL: Repository, https://github.com/nemtus/symbol/tree/dev/catbuffer/parser
21
+ Description-Content-Type: text/markdown
22
+
23
+ <!-- nemtus-mirror-notice -->
24
+ > **Note (nemtus mirror):** This package is a content mirror of the catbuffer
25
+ > parser from [`symbol/symbol`](https://github.com/symbol/symbol)
26
+ > (`catbuffer/parser`, upstream PyPI name `catparser`). nemtus republishes it on
27
+ > PyPI as [`nemtus-catparser`](https://pypi.org/project/nemtus-catparser/); the
28
+ > only change from upstream is the published distribution name. The import module
29
+ > name is unchanged — you still `import catparser`. For the canonical project, see
30
+ > [symbol/symbol](https://github.com/symbol/symbol).
31
+ <!-- /nemtus-mirror-notice -->
32
+
33
+ # catbuffer-parser
34
+
35
+ [![Build Status](https://api.travis-ci.com/symbol/catbuffer-parser.svg?branch=main)](https://travis-ci.com/symbol/catbuffer-parser)
36
+
37
+ This project is the reference parser implementation for the CATS (Compact Affinitized Transfer Schema) DSL that is used by the Symbol blockchain. The parser converts a CATS file or files into an AST that can be used by a generator to produce serialization and deserialization code for defined objects.
38
+
39
+ - A reference to the DSL format can be found [here](docs/cats_dsl.md).
40
+ - For a sampling of real world schemas, those used by the Symbol blockchain can be found [here](https://github.com/symbol/catbuffer-schemas).
41
+
42
+ ## Requirements
43
+
44
+ * Python >= 3.7
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ git clone https://github.com/symbol/catbuffer-parser
50
+ pip3 install -r requirements.txt
51
+ pip3 install -r lint_requirements.txt # optional
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ```
57
+ usage: python -m catparser [-h] -s SCHEMA -i INCLUDE [-o OUTPUT]
58
+
59
+ CATS code generator
60
+
61
+ optional arguments:
62
+ -h, --help show this help message and exit
63
+ -s SCHEMA, --schema SCHEMA
64
+ input CATS file
65
+ -i INCLUDE, --include INCLUDE
66
+ schema root directory
67
+ -o OUTPUT, --output OUTPUT
68
+ yaml output file
69
+ -g GENERATOR, --generator GENERATOR
70
+ generator class to use to produce output files (defaults to YAML output)
71
+ -q, --quiet do not print type descriptors to console
72
+ ```
73
+
74
+ ## Examples
75
+
76
+ ``catparser`` can be used on its own to parse input files, check their validity and optionally output a YAML file containing the parsed type descriptors:
77
+
78
+ ```bash
79
+ # parse but don't output anything
80
+ python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol --quiet
81
+
82
+ # parse and output the AST
83
+ python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol
84
+
85
+ # parse and generate code using the specified generator (`generator.Generator`)
86
+ python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol --generator generator.Generator
87
+ ```
88
+
@@ -0,0 +1,65 @@
1
+ <!-- nemtus-mirror-notice -->
2
+ > **Note (nemtus mirror):** This package is a content mirror of the catbuffer
3
+ > parser from [`symbol/symbol`](https://github.com/symbol/symbol)
4
+ > (`catbuffer/parser`, upstream PyPI name `catparser`). nemtus republishes it on
5
+ > PyPI as [`nemtus-catparser`](https://pypi.org/project/nemtus-catparser/); the
6
+ > only change from upstream is the published distribution name. The import module
7
+ > name is unchanged — you still `import catparser`. For the canonical project, see
8
+ > [symbol/symbol](https://github.com/symbol/symbol).
9
+ <!-- /nemtus-mirror-notice -->
10
+
11
+ # catbuffer-parser
12
+
13
+ [![Build Status](https://api.travis-ci.com/symbol/catbuffer-parser.svg?branch=main)](https://travis-ci.com/symbol/catbuffer-parser)
14
+
15
+ This project is the reference parser implementation for the CATS (Compact Affinitized Transfer Schema) DSL that is used by the Symbol blockchain. The parser converts a CATS file or files into an AST that can be used by a generator to produce serialization and deserialization code for defined objects.
16
+
17
+ - A reference to the DSL format can be found [here](docs/cats_dsl.md).
18
+ - For a sampling of real world schemas, those used by the Symbol blockchain can be found [here](https://github.com/symbol/catbuffer-schemas).
19
+
20
+ ## Requirements
21
+
22
+ * Python >= 3.7
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ git clone https://github.com/symbol/catbuffer-parser
28
+ pip3 install -r requirements.txt
29
+ pip3 install -r lint_requirements.txt # optional
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```
35
+ usage: python -m catparser [-h] -s SCHEMA -i INCLUDE [-o OUTPUT]
36
+
37
+ CATS code generator
38
+
39
+ optional arguments:
40
+ -h, --help show this help message and exit
41
+ -s SCHEMA, --schema SCHEMA
42
+ input CATS file
43
+ -i INCLUDE, --include INCLUDE
44
+ schema root directory
45
+ -o OUTPUT, --output OUTPUT
46
+ yaml output file
47
+ -g GENERATOR, --generator GENERATOR
48
+ generator class to use to produce output files (defaults to YAML output)
49
+ -q, --quiet do not print type descriptors to console
50
+ ```
51
+
52
+ ## Examples
53
+
54
+ ``catparser`` can be used on its own to parse input files, check their validity and optionally output a YAML file containing the parsed type descriptors:
55
+
56
+ ```bash
57
+ # parse but don't output anything
58
+ python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol --quiet
59
+
60
+ # parse and output the AST
61
+ python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol
62
+
63
+ # parse and generate code using the specified generator (`generator.Generator`)
64
+ python3 -m catparser --schema ../schemas/symbol/transfer/transfer.cats --include ../schemas/symbol --generator generator.Generator
65
+ ```
@@ -0,0 +1,97 @@
1
+ from .ast import AstException, Struct, StructInlinePlaceholder
2
+
3
+
4
+ class AstPostProcessor:
5
+ """Post processes AST type descriptors."""
6
+
7
+ def __init__(self, type_descriptors):
8
+ self.raw_type_descriptors = type_descriptors
9
+ self.type_descriptor_map = {model.name: model for model in self.raw_type_descriptors}
10
+
11
+ @property
12
+ def type_descriptors(self):
13
+ # filter out inline structs
14
+ return [model for model in self.raw_type_descriptors if not hasattr(model, 'disposition') or 'inline' != model.disposition]
15
+
16
+ def apply_attributes(self):
17
+ """Sets properties from attributes within all structures."""
18
+ for model in self._structs():
19
+ for field in model.fields:
20
+ if not hasattr(field, 'attributes') or not field.attributes:
21
+ continue
22
+
23
+ for attribute in field.attributes:
24
+ if not hasattr(field.field_type, attribute.name):
25
+ raise AstException(f'field {field.name} ({field.field_type}) does not have property {attribute.name}')
26
+
27
+ setattr(field.field_type, attribute.name, attribute.values)
28
+
29
+ def _structs(self):
30
+ return [model for _, model in self.type_descriptor_map.items() if isinstance(model, Struct)]
31
+
32
+ def expand_named_inlines(self):
33
+ """Expands named inline fields within all structures."""
34
+
35
+ for model in self._structs_with_named_inlines():
36
+ original_fields = model.fields[:]
37
+ model.fields = []
38
+
39
+ for field in original_fields:
40
+ if not self._is_named_inline(field):
41
+ model.fields.append(field)
42
+ continue
43
+
44
+ if field.field_type not in self.type_descriptor_map:
45
+ raise AstException(f'struct {model.name} contains named inline of unknown type {field.field_type}')
46
+
47
+ referenced_type_model = self.type_descriptor_map[field.field_type]
48
+ model.fields.extend(referenced_type_model.apply_inline_template(field))
49
+
50
+ def _structs_with_named_inlines(self):
51
+ return [
52
+ model for _, model in self.type_descriptor_map.items()
53
+ if isinstance(model, Struct) and any(self._is_named_inline(field) for field in model.fields)
54
+ ]
55
+
56
+ @staticmethod
57
+ def _is_named_inline(field):
58
+ return hasattr(field, 'disposition') and 'inline' == field.disposition and hasattr(field, 'name')
59
+
60
+ def expand_unnamed_inlines(self):
61
+ """Expands unnamed inline fields within all structures."""
62
+
63
+ for model in self._structs_with_unnamed_inlines():
64
+ while self._has_unnamed_inline_field(model):
65
+ original_fields = model.fields[:]
66
+ model.fields = []
67
+
68
+ for field in original_fields:
69
+ if not isinstance(field, StructInlinePlaceholder):
70
+ model.fields.append(field)
71
+ continue
72
+
73
+ if field.inlined_typename not in self.type_descriptor_map:
74
+ raise AstException(f'struct {model.name} contains unnamed inline of unknown type {field.inlined_typename}')
75
+
76
+ referenced_type_model = self.type_descriptor_map[field.inlined_typename]
77
+ if 'abstract' == referenced_type_model.disposition:
78
+ model.factory_type = referenced_type_model.name
79
+ elif referenced_type_model.factory_type:
80
+ model.factory_type = referenced_type_model.factory_type
81
+
82
+ model.fields.extend(referenced_type_model.fields)
83
+ if referenced_type_model.attributes:
84
+ if not model.attributes:
85
+ model.attributes = []
86
+
87
+ model.attributes.extend(referenced_type_model.attributes)
88
+
89
+ def _structs_with_unnamed_inlines(self):
90
+ return [
91
+ model for _, model in self.type_descriptor_map.items()
92
+ if self._has_unnamed_inline_field(model)
93
+ ]
94
+
95
+ @staticmethod
96
+ def _has_unnamed_inline_field(model):
97
+ return isinstance(model, Struct) and any(isinstance(field, StructInlinePlaceholder) for field in model.fields)
@@ -0,0 +1,243 @@
1
+ from enum import Enum
2
+
3
+ from .ast import Array, Conditional, Enum, FixedSizeInteger, Struct, StructInlinePlaceholder
4
+
5
+
6
+ class ErrorDescriptor:
7
+ """Describes an AST validation error."""
8
+
9
+ def __init__(self, message, typename, field_names=None):
10
+ self.message = message
11
+ self.typename = typename
12
+ self.field_names = [field_names] if isinstance(field_names, str) else field_names
13
+
14
+ def __eq__(self, rhs):
15
+ return isinstance(rhs, ErrorDescriptor) and str(self) == str(rhs)
16
+
17
+ def __repr__(self):
18
+ if self.field_names:
19
+ joined_field_names = ', '.join(self.field_names)
20
+ return f'[{self.typename}::{{ {joined_field_names} }}] {self.message}'
21
+
22
+ return f'[{self.typename}] {self.message}'
23
+
24
+
25
+ class AstValidator:
26
+ """Validates AST type descriptors."""
27
+
28
+ class Mode(Enum):
29
+ """Validation mode."""
30
+
31
+ PRE_EXPANSION = 1
32
+ POST_EXPANSION = 2
33
+
34
+ def __init__(self, type_descriptors):
35
+ self.raw_type_descriptors = type_descriptors
36
+ self.type_descriptor_map = {model.name: model for model in self.raw_type_descriptors}
37
+
38
+ self.mode = self.Mode.PRE_EXPANSION
39
+ self.errors = []
40
+
41
+ def set_validation_mode(self, mode):
42
+ """Sets the validation mode."""
43
+ self.mode = mode
44
+
45
+ def validate(self):
46
+ """Validates all types for correctness."""
47
+
48
+ for _, model in self.type_descriptor_map.items():
49
+ if isinstance(model, Enum):
50
+ self._validate_enum(model)
51
+
52
+ if isinstance(model, Struct):
53
+ self._validate_struct(model)
54
+
55
+ def _validate_enum(self, model):
56
+ duplicate_names = self._find_duplicate_names(model.values)
57
+ if duplicate_names:
58
+ self.errors.append(ErrorDescriptor('duplicate enum values', model.name, duplicate_names))
59
+
60
+ def _validate_struct(self, model):
61
+ duplicate_names = self._find_duplicate_names(model.fields)
62
+ if duplicate_names:
63
+ self.errors.append(ErrorDescriptor('duplicate struct fields', model.name, duplicate_names))
64
+
65
+ field_map = {field.name: field for field in model.fields if hasattr(field, 'name')}
66
+ for field in model.fields:
67
+ def create_error_descriptor(message, error_field=field):
68
+ return ErrorDescriptor(message, model.name, error_field.name)
69
+
70
+ if isinstance(field, StructInlinePlaceholder):
71
+ self._validate_unnamed_inline(field, lambda message: ErrorDescriptor(message, model.name))
72
+ else:
73
+ self._validate_struct_field(field, field_map, create_error_descriptor)
74
+
75
+ if self.Mode.PRE_EXPANSION != self.mode:
76
+ self._check_struct_attributes(model, field_map)
77
+
78
+ def _validate_unnamed_inline(self, field, create_error_descriptor):
79
+ if not self._is_known_type(field.inlined_typename):
80
+ self.errors.append(create_error_descriptor(f'reference to unknown inlined type "{field.inlined_typename}"'))
81
+ else:
82
+ # all dispositions are allowed as unnamed inline
83
+ pass
84
+
85
+ def _validate_struct_field(self, field, field_map, create_error_descriptor):
86
+ if not self._is_known_type(field.field_type):
87
+ self.errors.append(create_error_descriptor(f'reference to unknown type "{field.field_type}"'))
88
+ else:
89
+ if 'inline' == field.disposition and 'inline' != self.type_descriptor_map[field.field_type].disposition:
90
+ self.errors.append(create_error_descriptor(f'named inline field referencing non inline struct "{field.field_type}"'))
91
+
92
+ if isinstance(field.field_type, FixedSizeInteger):
93
+ self._validate_integer(field.field_type, field_map, create_error_descriptor)
94
+
95
+ if isinstance(field.field_type, Array):
96
+ self._validate_array(field.field_type, field_map, create_error_descriptor)
97
+
98
+ if field.value is not None:
99
+ if 'sizeof' == field.disposition:
100
+ self._validate_sizeof(field, field_map, create_error_descriptor)
101
+ elif isinstance(field.value, Conditional):
102
+ self._validate_conditional(field, field_map, create_error_descriptor)
103
+ else:
104
+ self._validate_in_range(field.field_type, field.value, create_error_descriptor)
105
+
106
+ if field.attributes:
107
+ for attribute in field.attributes:
108
+ if not hasattr(field.field_type, attribute.name):
109
+ self.errors.append(create_error_descriptor(f'inapplicable attribute "{attribute.name}"'))
110
+
111
+ def _validate_integer(self, field_type, field_map, create_error_descriptor):
112
+ sizeref = field_type.sizeref
113
+ if not sizeref:
114
+ return
115
+
116
+ if sizeref.property_name not in field_map:
117
+ self.errors.append(create_error_descriptor(f'reference to unknown sizeref property "{sizeref.property_name}"'))
118
+
119
+ def _validate_array(self, field_type, field_map, create_error_descriptor):
120
+ element_type = field_type.element_type
121
+ is_sort_key_valid = True
122
+ sort_key = field_type.sort_key
123
+ if not self._is_known_type(element_type):
124
+ self.errors.append(create_error_descriptor(f'reference to unknown element type "{element_type}"'))
125
+ is_sort_key_valid = not sort_key
126
+ else:
127
+ is_sort_key_valid = not sort_key or any(
128
+ sort_key == element_field.name for element_field in self.type_descriptor_map[element_type].fields
129
+ )
130
+
131
+ if not is_sort_key_valid:
132
+ self.errors.append(create_error_descriptor(f'reference to unknown sort_key property "{sort_key}"'))
133
+
134
+ size = field_type.size
135
+ if isinstance(size, str) and size not in field_map:
136
+ self.errors.append(create_error_descriptor(f'reference to unknown size property "{size}"'))
137
+
138
+ def _validate_sizeof(self, field, field_map, create_error_descriptor):
139
+ if field.value not in field_map:
140
+ self.errors.append(create_error_descriptor(f'reference to unknown sizeof property "{field.value}"'))
141
+ return
142
+
143
+ reference_typename = field_map[field.value].field_type
144
+ reference_type = None if not isinstance(reference_typename, str) else self.type_descriptor_map[reference_typename]
145
+ if not isinstance(reference_type, Struct):
146
+ self.errors.append(create_error_descriptor(f'sizeof property references fixed size type "{reference_typename}"'))
147
+ elif not reference_type.is_size_implicit:
148
+ message = f'sizeof property references type "{reference_typename}" without is_size_implicit attribute'
149
+ self.errors.append(create_error_descriptor(message))
150
+
151
+ def _validate_conditional(self, field, field_map, create_error_descriptor):
152
+ linked_field_name = field.value.linked_field_name
153
+
154
+ if linked_field_name not in field_map:
155
+ self.errors.append(create_error_descriptor(f'reference to unknown condition field "{linked_field_name}"'))
156
+ else:
157
+ self._validate_in_range(field_map[linked_field_name].field_type, field.value.value, create_error_descriptor)
158
+
159
+ def _validate_in_range(self, value_type, value, create_error_descriptor):
160
+ if isinstance(value_type, str):
161
+ if self._is_known_type(value_type):
162
+ value_type = self.type_descriptor_map[value_type]
163
+
164
+ if isinstance(value_type, Enum):
165
+ if not any(value == enum_value.name for enum_value in value_type.values):
166
+ self.errors.append(create_error_descriptor(f'field value "{value}" is not a valid enum value'))
167
+
168
+ return
169
+
170
+ if not isinstance(value, int):
171
+ self.errors.append(create_error_descriptor(f'field value "{value}" is not a valid numeric value'))
172
+
173
+ def _check_struct_attributes(self, model, field_map):
174
+ if self._check_known_field(model, field_map, 'size'):
175
+ if not isinstance(field_map[model.size].field_type, FixedSizeInteger):
176
+ self.errors.append(ErrorDescriptor(f'reference to "size" property "{model.size}" has unexpected type', model.name))
177
+
178
+ self._check_known_field(model, field_map, 'discriminator', True)
179
+
180
+ self._check_comparer(model, field_map)
181
+ self._check_initializers(model, field_map)
182
+
183
+ def _check_comparer(self, model, field_map):
184
+ if not model.comparer:
185
+ return
186
+
187
+ for (property_name, transform) in model.comparer:
188
+ if property_name not in field_map:
189
+ self.errors.append(ErrorDescriptor(f'reference to unknown "comparer" property "{property_name}"', model.name))
190
+ if transform not in (None, 'ripemd_keccak_256'):
191
+ self.errors.append(ErrorDescriptor(f'reference to unknown "comparer" transform "{transform}"', model.name))
192
+
193
+ def _check_initializers(self, model, field_map):
194
+ if not model.initializers:
195
+ return
196
+
197
+ for initializer in model.initializers:
198
+ is_valid = True
199
+ is_concrete = model.disposition not in ('abstract', 'inline')
200
+ for property_name, raise_error in [(initializer.target_property_name, True), (initializer.value, is_concrete)]:
201
+ if property_name not in field_map:
202
+ is_valid = False
203
+ if raise_error:
204
+ self.errors.append(ErrorDescriptor(
205
+ f'reference to unknown "intializes" property "{property_name}"',
206
+ model.name))
207
+
208
+ if not is_valid:
209
+ continue
210
+
211
+ # str compare is good enough as it lets us differentiate among subtypes of things like FixedSizeInteger
212
+ if str(field_map[initializer.target_property_name].field_type) != str(field_map[initializer.value].field_type):
213
+ self.errors.append(ErrorDescriptor(
214
+ f'property "{initializer.target_property_name}" has initializer "{initializer.value}" of different type',
215
+ model.name))
216
+
217
+ def _check_known_field(self, model, field_map, property_name, multi_value=False):
218
+ values = getattr(model, property_name)
219
+ if not values:
220
+ return False
221
+
222
+ if not multi_value:
223
+ values = [values]
224
+
225
+ has_error = False
226
+ for value in values:
227
+ if value not in field_map:
228
+ self.errors.append(ErrorDescriptor(f'reference to unknown "{property_name}" property "{value}"', model.name))
229
+ has_error = True
230
+
231
+ return not has_error
232
+
233
+ def _is_known_type(self, typename):
234
+ return not isinstance(typename, str) or typename in self.type_descriptor_map
235
+
236
+ @staticmethod
237
+ def _find_duplicate_names(items):
238
+ unique_names = set()
239
+ duplicate_names = set()
240
+ for item in filter(lambda item: hasattr(item, 'name'), items):
241
+ (unique_names if item.name not in unique_names else duplicate_names).add(item.name)
242
+
243
+ return duplicate_names