PySerials 0.0.0.dev60__tar.gz → 0.0.0.dev62__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.
Files changed (24) hide show
  1. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/PKG-INFO +4 -4
  2. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/pyproject.toml +4 -4
  3. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/PySerials.egg-info/PKG-INFO +4 -4
  4. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/PySerials.egg-info/SOURCES.txt +0 -1
  5. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/PySerials.egg-info/requires.txt +3 -3
  6. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/exception/update.py +2 -2
  7. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/nested_dict.py +2 -0
  8. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/update.py +271 -57
  9. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/write.py +23 -5
  10. pyserials-0.0.0.dev60/README.md +0 -3
  11. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/setup.cfg +0 -0
  12. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/PySerials.egg-info/dependency_links.txt +0 -0
  13. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/PySerials.egg-info/not-zip-safe +0 -0
  14. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/PySerials.egg-info/top_level.txt +0 -0
  15. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/__init__.py +0 -0
  16. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/compare.py +0 -0
  17. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/exception/__init__.py +0 -0
  18. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/exception/_base.py +0 -0
  19. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/exception/read.py +0 -0
  20. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/exception/validate.py +0 -0
  21. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/format.py +0 -0
  22. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/property_dict.py +0 -0
  23. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/read.py +0 -0
  24. {pyserials-0.0.0.dev60 → pyserials-0.0.0.dev62}/src/pyserials/validate.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PySerials
3
- Version: 0.0.0.dev60
3
+ Version: 0.0.0.dev62
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: jsonschema<5,>=4.21.0
6
6
  Requires-Dist: referencing>=0.35.1
7
7
  Requires-Dist: jsonpath-ng<2,>=1.6.1
8
8
  Requires-Dist: ruamel.yaml>=0.18
9
9
  Requires-Dist: ruamel.yaml.string<1,>=0.1.1
10
- Requires-Dist: tomlkit<0.12,>=0.11.8
11
- Requires-Dist: MDit==0.0.0.dev57
12
- Requires-Dist: ExceptionMan==0.0.0.dev57
10
+ Requires-Dist: tomlkit<0.14,>=0.11.8
11
+ Requires-Dist: MDit==0.0.0.dev59
12
+ Requires-Dist: ExceptionMan==0.0.0.dev59
13
13
  Requires-Dist: ProtocolMan==0.0.0.dev2
@@ -17,7 +17,7 @@ namespaces = true
17
17
  # ----------------------------------------- Project Metadata -------------------------------------
18
18
  #
19
19
  [project]
20
- version = "0.0.0.dev60"
20
+ version = "0.0.0.dev62"
21
21
  name = "PySerials"
22
22
  dependencies = [
23
23
  "jsonschema >= 4.21.0, < 5",
@@ -25,9 +25,9 @@ dependencies = [
25
25
  "jsonpath-ng >= 1.6.1, < 2",
26
26
  "ruamel.yaml >= 0.18", # https://yaml.readthedocs.io/en/stable/
27
27
  "ruamel.yaml.string >= 0.1.1, < 1",
28
- "tomlkit >= 0.11.8, < 0.12", # https://tomlkit.readthedocs.io/en/stable/,
29
- "MDit == 0.0.0.dev57",
30
- "ExceptionMan == 0.0.0.dev57",
28
+ "tomlkit >= 0.11.8, < 0.14", # https://tomlkit.readthedocs.io/en/stable/,
29
+ "MDit == 0.0.0.dev59",
30
+ "ExceptionMan == 0.0.0.dev59",
31
31
  "ProtocolMan == 0.0.0.dev2",
32
32
  ]
33
33
  requires-python = ">=3.10"
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PySerials
3
- Version: 0.0.0.dev60
3
+ Version: 0.0.0.dev62
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: jsonschema<5,>=4.21.0
6
6
  Requires-Dist: referencing>=0.35.1
7
7
  Requires-Dist: jsonpath-ng<2,>=1.6.1
8
8
  Requires-Dist: ruamel.yaml>=0.18
9
9
  Requires-Dist: ruamel.yaml.string<1,>=0.1.1
10
- Requires-Dist: tomlkit<0.12,>=0.11.8
11
- Requires-Dist: MDit==0.0.0.dev57
12
- Requires-Dist: ExceptionMan==0.0.0.dev57
10
+ Requires-Dist: tomlkit<0.14,>=0.11.8
11
+ Requires-Dist: MDit==0.0.0.dev59
12
+ Requires-Dist: ExceptionMan==0.0.0.dev59
13
13
  Requires-Dist: ProtocolMan==0.0.0.dev2
@@ -1,4 +1,3 @@
1
- README.md
2
1
  pyproject.toml
3
2
  src/PySerials.egg-info/PKG-INFO
4
3
  src/PySerials.egg-info/SOURCES.txt
@@ -3,7 +3,7 @@ referencing>=0.35.1
3
3
  jsonpath-ng<2,>=1.6.1
4
4
  ruamel.yaml>=0.18
5
5
  ruamel.yaml.string<1,>=0.1.1
6
- tomlkit<0.12,>=0.11.8
7
- MDit==0.0.0.dev57
8
- ExceptionMan==0.0.0.dev57
6
+ tomlkit<0.14,>=0.11.8
7
+ MDit==0.0.0.dev59
8
+ ExceptionMan==0.0.0.dev59
9
9
  ProtocolMan==0.0.0.dev2
@@ -49,8 +49,8 @@ class PySerialsUpdateException(_base.PySerialsException):
49
49
  return
50
50
 
51
51
 
52
- class PySerialsUpdateDictFromAddonError(PySerialsUpdateException):
53
- """Base class for all exceptions raised by `pyserials.update.dict_from_addon`.
52
+ class PySerialsUpdateRecursiveDataError(PySerialsUpdateException):
53
+ """Base class for all exceptions raised by `pyserials.update.recursive_update`.
54
54
 
55
55
  Attributes
56
56
  ----------
@@ -36,6 +36,7 @@ class NestedDict:
36
36
  relative_key_key: str | None = None,
37
37
  implicit_root: bool = True,
38
38
  getter_function_name: str = "get",
39
+ skip_key_func: Callable[[list[str]], bool] | None = None,
39
40
  ):
40
41
  self._data = data or {}
41
42
  self._templater = _ps.update.TemplateFiller(
@@ -62,6 +63,7 @@ class NestedDict:
62
63
  relative_key_key=relative_key_key,
63
64
  implicit_root=implicit_root,
64
65
  getter_function_name=getter_function_name,
66
+ skip_key_func=skip_key_func,
65
67
  )
66
68
  return
67
69
 
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- import re
4
3
  from typing import TYPE_CHECKING as _TYPE_CHECKING
5
4
  import re as _re
6
5
  from functools import partial as _partial
@@ -11,69 +10,261 @@ from jsonpath_ng import exceptions as _jsonpath_exceptions
11
10
  import pyserials.exception as _exception
12
11
 
13
12
  if _TYPE_CHECKING:
14
- from typing import Literal, Sequence, Any, Callable
13
+ from typing import Literal, Sequence, Any, Callable, Iterable
14
+ UPDATE_OPTIONS = Literal["skip", "write", "raise"] | Callable[
15
+ [tuple[Any] | tuple[Any, Any]], None | tuple[Any, str]
16
+ ]
17
+ FUNC_ITEMS = Callable[[Any], Iterable[tuple[Any, Any]]]
18
+ FUNC_CONTAINS = Callable[[Any, Any], bool]
19
+ FUNC_GET = Callable[[Any, Any], Any]
20
+ FUNC_SET = Callable[[Any, Any, Any], None]
21
+ FUNC_CONSTRUCT = Callable[[], Any]
22
+ RECURSIVE_DTYPE_FUNCS = tuple[FUNC_ITEMS, FUNC_CONTAINS, FUNC_GET, FUNC_SET, FUNC_CONSTRUCT]
23
+
24
+
25
+ def recursive_update(
26
+ source: Any,
27
+ addon: Any,
28
+ recursive_types: dict[type | tuple[type, ...], RECURSIVE_DTYPE_FUNCS] | None = None,
29
+ types: dict[type | tuple[type, ...], UPDATE_OPTIONS | tuple[UPDATE_OPTIONS, UPDATE_OPTIONS]] | None = None,
30
+ paths: dict[str, UPDATE_OPTIONS] | None = None,
31
+ constructor: Callable[[], Any] | None = None,
32
+ undefined_new: UPDATE_OPTIONS = "write",
33
+ undefined_existing: UPDATE_OPTIONS = "skip",
34
+ type_mismatch: UPDATE_OPTIONS = "raise",
35
+ ) -> dict[str, list[str]]:
36
+ """Recursively update a complex data structure using another data structure.
37
+
38
+ Parameters
39
+ ----------
40
+ source
41
+ Data structure to update in place.
42
+ The type of this object must be
43
+ defined in the `recursive_types` argument.
44
+ addon
45
+ Data structure containing additional data to update `source` with.
46
+ recursive_types
47
+ Definition of recursive data types.
48
+ Each key is a type (or tuple of types) used
49
+ to identify data types with the `isinstance` function.
50
+ Each value is a tuple of three functions:
51
+ 1. Function to extract items from the data. It must accept an instance
52
+ of the type and return an iterable of key-value pairs.
53
+ 2. Function to check if a key is in the data. It must accept an instance
54
+ of the type and a key, and return a boolean.
55
+ 3. Function to get a value from the data. It must accept an instance
56
+ of the type and a key, and return the value.
57
+ 4. Function to set a key-value pair in the data. It must accept an instance
58
+ of the type, a key, and a value, respectively.
59
+ 5. Function to construct a new instance of the type. It must accept no arguments.
60
+
61
+ By default, `dict` is defined as follows:
62
+ ```python
63
+ recursive_types = {
64
+ dict: (
65
+ lambda dic: dic.items(),
66
+ lambda dic, key: key in dic,
67
+ lambda dic, key: dic[key],
68
+ lambda dic, key, value: dic.update({key: value}),
69
+ lambda: dict(),
70
+ )
71
+ }
72
+ ```
73
+ This default argument will be updated/overwritten with any custom types provided.
74
+ For example, to add support for a custom class `MyClass`, you can use:
75
+ ```python
76
+ recursive_types = {
77
+ MyClass: (
78
+ lambda obj: vars(object).items(),
79
+ lambda obj, key: hasattr(obj, key),
80
+ lambda obj, key: getattr(obj, key),
81
+ lambda obj, key, value: setattr(obj, key, value),
82
+ lambda: MyClass(),
83
+ )
84
+ }
85
+ ```
86
+ types
87
+ Update behavior for specific data types.
88
+ Each key is a type (or tuple of types) used
89
+ to identify data types with the `isinstance` function.
90
+ Each value is can be single update option for all cases,
91
+ or a tuple of two update options for when the corresponding
92
+ key/attribute does not exist in the source data, and when it does, respectively.
93
+ Each update option can be either a keyword specifying a predefined behavior,
94
+ or a function for custom behavior. The available keywords are:
95
+ - "skip": Ignore the key/attribute; do not change anything.
96
+ - "write": Write the key/attribute in source data with the value from the addon, overwriting if it exists.
97
+
98
+ A custom function must accept a single argument, a tuple of either one or two values.
99
+ If it is two values, the first value is the current value in the source data,
100
+ and the second value is the value from the addon.
101
+ If it is one value, the value is the value from the addon,
102
+ meaning the key/attribute does not exist in the source data.
103
+ The function must either return `None` (for when nothing must be changes in the source)
104
+ or a tuple of two values:
105
+ 1. The new value to write in the source data.
106
+ 2. A string specifying the change type.
107
+
108
+ By default, the following behavior is defined for basic types:
109
+ ```python
110
+ types = {list: ("write", lambda data: (data[0] + data[1], "append"))}
111
+ ```
112
+
113
+ The default behavior for any data type not specified in this argument
114
+ is determined by the `undefined_new` and `undefined_existing` arguments.
115
+ paths
116
+ Update behavior for specific keys using JSONPath expressions.
117
+ This is the same as the `types` argument, but targeting specific keys
118
+ instead of data types.
119
+ Everything is the same as in the `types` argument, except that the keys
120
+ are JSONPath expressions as strings.
121
+ constructor
122
+ Custom constructor for creating new instances of the source data type.
123
+ This is used when the addon data contains a recursive key/attribute
124
+ that is not present in the source data. If not provided,
125
+ the type of the addon value will be used to create a new instance.
126
+ undefined_new
127
+ Behavior for when a non-recursive data with no defined behavior
128
+ in the `types` argument is found in the addon data
129
+ but not in the source data.
130
+ undefined_existing
131
+ Behavior for when a non-recursive data with no defined behavior
132
+ in the `types` argument is found in the addon data
133
+ and in the source data.
134
+ type_mismatch
135
+ Behavior for when a key/attribute in the source data
136
+ is not a recursive type, but the corresponding key/attribute
137
+ in the addon data is a recursive type.
138
+ """
139
+ def get_funcs(data: Any) -> RECURSIVE_DTYPE_FUNCS | None:
140
+ for typ, funcs in recursive_types.items():
141
+ if isinstance(data, typ):
142
+ return funcs
143
+ return None
15
144
 
145
+ def recursive(
146
+ src: Any,
147
+ add: Any,
148
+ src_funcs: RECURSIVE_DTYPE_FUNCS,
149
+ add_funcs: RECURSIVE_DTYPE_FUNCS,
150
+ path: str,
151
+ ):
16
152
 
17
- def dict_from_addon(
18
- data: dict,
19
- addon: dict,
20
- append_list: bool = True,
21
- append_dict: bool = True,
22
- raise_duplicates: bool = False,
23
- raise_type_mismatch: bool = True,
24
- ) -> dict[str, list[str]]:
25
- """Recursively update a dictionary from another dictionary."""
26
- def recursive(source: dict, add: dict, path: str, log: dict):
153
+ def apply(behavior: UPDATE_OPTIONS | tuple[UPDATE_OPTIONS, UPDATE_OPTIONS]):
154
+ action = behavior[int(key_exists_in_src)] if isinstance(behavior, tuple) else behavior
155
+ if action == "raise":
156
+ raise_error(typ="duplicate")
157
+ elif action == "skip":
158
+ change_type = "skip"
159
+ elif action == "write":
160
+ change_type = "write"
161
+ fn_src_set(src, key, value)
162
+ elif not isinstance(action, str):
163
+ out = action((source_value, value) if key_exists_in_src else (value,))
164
+ if out:
165
+ new_value, change_type = out
166
+ fn_src_set(src, key, new_value)
167
+ else:
168
+ change_type = "skip"
169
+ else:
170
+ raise ValueError(f"Invalid update behavior '{action}' for key '{key}' at path '{path}'.")
171
+ log[fullpath] = (type(source_value) if key_exists_in_src else None, type(value), change_type)
172
+ return
27
173
 
28
174
  def raise_error(typ: Literal["duplicate", "type_mismatch"]):
29
- raise _exception.update.PySerialsUpdateDictFromAddonError(
175
+ raise _exception.update.PySerialsUpdateRecursiveDataError(
30
176
  problem_type=typ,
31
177
  path=fullpath,
32
- data=source[key],
33
- data_full=data,
178
+ data=src[key],
179
+ data_full=src,
34
180
  data_addon=value,
35
181
  data_addon_full=addon,
36
182
  )
37
183
 
38
- for key, value in add.items():
39
- fullpath = f"{path}.{key}"
40
- if key not in source:
41
- log["added"].append(fullpath)
42
- source[key] = value
43
- continue
44
- if type(source[key]) is not type(value):
45
- if raise_type_mismatch:
46
- raise_error(typ="type_mismatch")
47
- continue
48
- if not isinstance(value, (list, dict)):
49
- if raise_duplicates:
50
- raise_error(typ="duplicate")
51
- log["skipped"].append(fullpath)
52
- elif isinstance(value, list):
53
- if append_list:
54
- appended = False
55
- for elem in value:
56
- if elem not in source[key]:
57
- source[key].append(elem)
58
- appended = True
59
- if appended:
60
- log["list_appended"].append(fullpath)
61
- elif raise_duplicates:
62
- raise_error(typ="duplicate")
63
- else:
64
- log["skipped"].append(fullpath)
184
+ _, fn_src_contains, fn_src_get, fn_src_set, _ = src_funcs
185
+ fn_add_items, _, _, _, fn_add_construct = add_funcs
186
+
187
+ for key, value in fn_add_items(add):
188
+ fullpath = f"{path}.'{key}'"
189
+ try:
190
+ full_jpath = _jsonpath.parse(fullpath) # quote to avoid JSONPath syntax errors
191
+ except Exception as e:
192
+ print(fullpath)
193
+ raise e
194
+ key_exists_in_src = fn_src_contains(src, key)
195
+ source_value = fn_src_get(src, key) if key_exists_in_src else None
196
+ for jpath_str, matches in jsonpath_match.items():
197
+ if full_jpath in matches:
198
+ apply(paths[jpath_str])
199
+ break
65
200
  else:
66
- if append_dict:
67
- recursive(source=source[key], add=value, path=f"{fullpath}.", log=log)
68
- elif raise_duplicates:
69
- raise_error(typ="duplicate")
201
+ for typ, action in type_to_arg.items():
202
+ if isinstance(value, typ):
203
+ apply(action)
204
+ break
70
205
  else:
71
- log["skipped"].append(fullpath)
72
- return log
73
- full_log = recursive(
74
- source=data, add=addon, path="$", log={"added": [], "skipped": [], "list_appended": []}
206
+ funcs_value = get_funcs(value)
207
+ if funcs_value:
208
+ # Value is a recursive type
209
+ if key_exists_in_src:
210
+ funcs_src_value = get_funcs(source_value)
211
+ if not funcs_src_value:
212
+ # Source value is not a recursive type
213
+ apply(type_mismatch)
214
+ else:
215
+ recursive(
216
+ src=fn_src_get(src, key),
217
+ add=value,
218
+ path=fullpath,
219
+ src_funcs=src_funcs,
220
+ add_funcs=funcs_value,
221
+ )
222
+ else:
223
+ # Source value does not exist; create a new instance
224
+ new_instance = constructor() if constructor else fn_add_construct()
225
+ fn_src_set(src, key, new_instance)
226
+ funcs_src_value = get_funcs(new_instance)
227
+ recursive(
228
+ src=new_instance,
229
+ add=value,
230
+ path=fullpath,
231
+ src_funcs=funcs_src_value,
232
+ add_funcs=funcs_value,
233
+ )
234
+ else:
235
+ # addon value is of a non-recursive type that does not have any defined behavior;
236
+ # Apply the default behavior for of ("write", "skip") for the key.
237
+ apply(undefined_existing if key_exists_in_src else undefined_new)
238
+ return
239
+
240
+ type_to_arg = {list: ("write", lambda data: (data[0] + data[1], "append"))} | (types or {})
241
+ recursive_types = {
242
+ dict: (
243
+ lambda dic: dic.items(),
244
+ lambda dic, key: key in dic,
245
+ lambda dic, key: dic[key],
246
+ lambda dic, key, value: dic.update({key: value}),
247
+ lambda: dict(),
248
+ )
249
+ } | (recursive_types or {})
250
+ jsonpath_match = {
251
+ jpath_str: [match.full_path for match in _jsonpath.parse(jpath_str).find(addon)]
252
+ for jpath_str in (paths or {}).keys()
253
+ }
254
+ log = {}
255
+ funcs_src = get_funcs(source)
256
+ funcs_add = get_funcs(addon)
257
+ for funcs, param_name, data in ((funcs_src, "source", source), (funcs_add, "addon", addon)):
258
+ if not funcs:
259
+ raise ValueError(f"Data type '{type(data)}' of '{param_name}' is not provided in 'recursive_types'.")
260
+ recursive(
261
+ src=source,
262
+ add=addon,
263
+ path="$",
264
+ src_funcs=funcs_src,
265
+ add_funcs=funcs_add,
75
266
  )
76
- return full_log
267
+ return log
77
268
 
78
269
 
79
270
  def data_from_jsonschema(data: dict | list, schema: dict) -> None:
@@ -129,6 +320,7 @@ class TemplateFiller:
129
320
  relative_key_key: str | None = None,
130
321
  implicit_root: bool = True,
131
322
  getter_function_name: str = "get",
323
+ skip_key_func: Callable[[list[str]], bool] | None = None,
132
324
  ):
133
325
  self._marker_start_value = marker_start_value
134
326
  self._marker_end_value = marker_end_value
@@ -151,6 +343,7 @@ class TemplateFiller:
151
343
  self._template_keys = relative_template_keys or []
152
344
  self._relative_key_key = relative_key_key
153
345
  self._getter_function_name = getter_function_name
346
+ self._skip_func = skip_key_func
154
347
 
155
348
  self._pattern_value: dict[int, _RegexPattern] = {}
156
349
  self._data = None
@@ -190,7 +383,11 @@ class TemplateFiller:
190
383
  code_lines = ["def __inline_code__():"]
191
384
  code_lines.extend([f" {line}" for line in code_str.strip("\n").splitlines()])
192
385
  code_str_full = "\n".join(code_lines)
193
- global_context = self._code_context.copy() | {self._getter_function_name: getter_function}
386
+ global_context = self._code_context.copy() | {
387
+ self._getter_function_name: getter_function,
388
+ "__current_path__": current_path,
389
+ "__relative_path_anchor__": relative_path_anchor
390
+ }
194
391
  for name, partial_func_data in self._code_context_partial.items():
195
392
  if isinstance(partial_func_data, tuple):
196
393
  func, arg_name = partial_func_data
@@ -250,6 +447,7 @@ class TemplateFiller:
250
447
  return output, True
251
448
  return output
252
449
  path_expr = self._concat_json_paths(root_path_expr, path_expr)
450
+ # print("IN GET ADD VAL", path_expr)
253
451
  cached_result = self._visited_paths.get(path_expr)
254
452
  if cached_result:
255
453
  value, matched = cached_result
@@ -266,7 +464,7 @@ class TemplateFiller:
266
464
  return self._no_match_value
267
465
 
268
466
  def get_value(jsonpath, return_all_matches: bool, from_code: bool) -> tuple[Any, bool]:
269
- matches = _rec_match(jsonpath)
467
+ matches = recursive_match(jsonpath)
270
468
  if not matches:
271
469
  if from_code:
272
470
  return None, False
@@ -294,14 +492,14 @@ class TemplateFiller:
294
492
  current_chain=current_chain + (jsonpath,),
295
493
  ), True
296
494
 
297
- def _rec_match(expr) -> list:
495
+ def recursive_match(expr) -> list:
298
496
  matches = expr.find(self._data)
299
497
  if matches:
300
498
  return matches
301
499
  if isinstance(expr.left, _jsonpath.Root):
302
500
  return []
303
501
  whole_matches = []
304
- left_matches = _rec_match(expr.left)
502
+ left_matches = recursive_match(expr.left)
305
503
  for left_match in left_matches:
306
504
  left_match_filled = self._recursive_subst(
307
505
  templ=left_match.value,
@@ -357,6 +555,11 @@ class TemplateFiller:
357
555
  template_end=self._marker_end_value,
358
556
  ) from exception
359
557
 
558
+ # print("IN MAIN", self._extract_fields(current_path))
559
+
560
+ if self._skip_func and self._skip_func(self._extract_fields(current_path)):
561
+ return templ
562
+
360
563
  if current_path in self._visited_paths:
361
564
  return self._visited_paths[current_path][0]
362
565
 
@@ -432,6 +635,7 @@ class TemplateFiller:
432
635
 
433
636
  if isinstance(templ, dict):
434
637
  new_dict = {}
638
+ addons = []
435
639
  for key, val in templ.items():
436
640
  key_filled = self._recursive_subst(
437
641
  templ=key,
@@ -442,7 +646,7 @@ class TemplateFiller:
442
646
  is_key=True,
443
647
  )
444
648
  if isinstance(key, str) and self._pattern_unpack.fullmatch(key):
445
- new_dict.update(key_filled)
649
+ addons.append((key_filled, val))
446
650
  continue
447
651
  if key_filled in self._template_keys:
448
652
  new_dict[key_filled] = val
@@ -455,6 +659,15 @@ class TemplateFiller:
455
659
  level=0,
456
660
  current_chain=current_chain + (new_path,),
457
661
  )
662
+ for addon_dict, addon_settings in sorted(
663
+ addons, key=lambda addon: addon[1].get("priority", 0) if addon[1] else 0
664
+ ):
665
+ addon_settings = {k: v for k, v in (addon_settings or {}).items() if k not in ("priority",)}
666
+ recursive_update(
667
+ source=new_dict,
668
+ addon=addon_dict,
669
+ **addon_settings,
670
+ )
458
671
  if not is_relative_template:
459
672
  self._visited_paths[current_path] = (new_dict, True)
460
673
  return new_dict
@@ -534,7 +747,7 @@ class _RegexPattern:
534
747
  def __init__(self, start: str, end: str):
535
748
  start_esc = _re.escape(start)
536
749
  end_esc = _re.escape(end)
537
- self.pattern = _re.compile(rf"{start_esc}(.*?)(?={end_esc}){end_esc}", re.DOTALL)
750
+ self.pattern = _re.compile(rf"{start_esc}(.*?)(?={end_esc}){end_esc}", _re.DOTALL)
538
751
  return
539
752
 
540
753
  def fullmatch(self, string: str) -> _re.Match | None:
@@ -547,3 +760,4 @@ class _RegexPattern:
547
760
 
548
761
  def sub(self, repl, string: str) -> str:
549
762
  return self.pattern.sub(repl, string)
763
+
@@ -15,13 +15,27 @@ def to_string(
15
15
  data_type: _Literal["json", "yaml", "toml"],
16
16
  sort_keys: bool = False,
17
17
  indent: int | None = None,
18
+ default: Callable[[Any], Any] | None = None,
18
19
  end_of_file_newline: bool = True,
20
+ indent_mapping: int = 2,
21
+ indent_sequence: int = 4,
22
+ indent_sequence_offset: int = 2,
23
+ multiline_string_to_block: bool = True,
24
+ remove_top_level_indent: bool = True
19
25
  ):
20
26
  if data_type == "json":
21
- return to_json_string(data, sort_keys=sort_keys, indent=indent)
27
+ return to_json_string(data, sort_keys=sort_keys, indent=indent, default=default, end_of_file_newline=end_of_file_newline)
22
28
  if data_type == "yaml":
23
- return to_yaml_string(data, end_of_file_newline=end_of_file_newline)
24
- return to_toml_string(data, sort_keys=sort_keys)
29
+ return to_yaml_string(
30
+ data,
31
+ end_of_file_newline=end_of_file_newline,
32
+ indent_mapping=indent_mapping,
33
+ indent_sequence=indent_sequence,
34
+ indent_sequence_offset=indent_sequence_offset,
35
+ multiline_string_to_block=multiline_string_to_block,
36
+ remove_top_level_indent=remove_top_level_indent
37
+ )
38
+ return to_toml_string(data, sort_keys=sort_keys, end_of_file_newline=end_of_file_newline)
25
39
 
26
40
 
27
41
  def to_yaml_string(
@@ -49,8 +63,10 @@ def to_yaml_string(
49
63
  def to_toml_string(
50
64
  data: dict | list | str | int | float | bool | _yaml.CommentedMap | _yaml.CommentedSeq,
51
65
  sort_keys: bool = False,
66
+ end_of_file_newline: bool = True,
52
67
  ) -> str:
53
- return _tomlkit.dumps(data, sort_keys=sort_keys)
68
+ string = _tomlkit.dumps(data, sort_keys=sort_keys)
69
+ return f"{string.rstrip("\n")}\n" if end_of_file_newline else string
54
70
 
55
71
 
56
72
  def to_json_string(
@@ -58,8 +74,10 @@ def to_json_string(
58
74
  sort_keys: bool = False,
59
75
  indent: int | None = None,
60
76
  default: Callable[[Any], Any] | None = None,
77
+ end_of_file_newline: bool = True,
61
78
  ) -> str:
62
- return _json.dumps(data, indent=indent, sort_keys=sort_keys, default=default)
79
+ string = _json.dumps(data, indent=indent, sort_keys=sort_keys, default=default)
80
+ return f"{string.rstrip("\n")}\n" if end_of_file_newline else string
63
81
 
64
82
 
65
83
  def to_yaml_file(
@@ -1,3 +0,0 @@
1
- # PySerials
2
-
3
- Work with serializable data in Python.