zs-yaml 0.10.1.dev2__tar.gz → 0.10.2.dev3__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 (42) hide show
  1. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/CHANGELOG.md +5 -0
  2. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/PKG-INFO +1 -1
  3. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/test_all_conversions.py +100 -1
  4. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml/_version.py +3 -3
  5. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml/convert.py +38 -14
  6. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml/main.py +42 -5
  7. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml.egg-info/PKG-INFO +1 -1
  8. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/.github/workflows/ci.yml +0 -0
  9. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/.gitignore +0 -0
  10. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/LICENSE +0 -0
  11. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/README.md +0 -0
  12. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/api_docs_review.md +0 -0
  13. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/doc/zs-yaml.png +0 -0
  14. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/doc/zs-yaml.svg +0 -0
  15. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/extern_compression/extern_compression.zs +0 -0
  16. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/extern_compression/payload.yaml +0 -0
  17. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/extern_compression/test_compression.py +0 -0
  18. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/.gitignore +0 -0
  19. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/benchmark.py +0 -0
  20. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/generate_perf_yaml.py +0 -0
  21. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/perf.zs +0 -0
  22. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/perf_reference.bin +0 -0
  23. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/perf_reference_dict.sha256 +0 -0
  24. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/perf/test_fast_path.py +0 -0
  25. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/addresses.yaml +0 -0
  26. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/custom_transformations.py +0 -0
  27. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/hobbies.yaml +0 -0
  28. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/skills.yaml +0 -0
  29. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/team.zs +0 -0
  30. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/team1.yaml +0 -0
  31. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/test_pyobj_to_yaml.py +0 -0
  32. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/examples/team/work_experience.yaml +0 -0
  33. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/pyproject.toml +0 -0
  34. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/setup.cfg +0 -0
  35. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml/__init__.py +0 -0
  36. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml/built_in_transformations.py +0 -0
  37. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml/yaml_transformer.py +0 -0
  38. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml.egg-info/SOURCES.txt +0 -0
  39. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml.egg-info/dependency_links.txt +0 -0
  40. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml.egg-info/entry_points.txt +0 -0
  41. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml.egg-info/requires.txt +0 -0
  42. {zs_yaml-0.10.1.dev2 → zs_yaml-0.10.2.dev3}/zs_yaml.egg-info/top_level.txt +0 -0
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0] - 2026-06-09
11
+
12
+ ### Added
13
+ - `bin -> yaml` can now take the zserio type directly via `--type <module.TypeName>` (and optional `--init-args`), so the target YAML no longer has to be pre-created with a `_meta` block. Fully backward compatible: when `--type` is omitted, the existing read-target-`_meta` behavior (including any `transformation_module`) is unchanged. `bin_to_yaml(...)` gains matching optional `schema_module`/`schema_type`/`init_args` parameters. Fixes #30
14
+
10
15
  ## [0.10.1] - 2026-05-07
11
16
 
12
17
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zs-yaml
3
- Version: 0.10.1.dev2
3
+ Version: 0.10.2.dev3
4
4
  Summary: YAML support for zserio
5
5
  Author-email: "Navigation Data Standard e.V." <support@nds-association.org>
6
6
  License-Expression: MIT
@@ -217,6 +217,53 @@ def test_bin_to_yaml():
217
217
  raise
218
218
 
219
219
 
220
+ def test_bin_to_yaml_with_type_arg():
221
+ """Test bin_to_yaml with the type passed directly, no pre-existing target.
222
+
223
+ Regression for ndsev/zs-yaml#30: schema_module/schema_type (CLI --type) allow
224
+ binary -> YAML without a pre-created target file containing _meta.
225
+ """
226
+ print("\nTesting bin_to_yaml with --type (no pre-existing target)...")
227
+
228
+ result = subprocess.run(['zs-yaml', 'team1.yaml', 'test_bin_to_yaml_type.bin'],
229
+ capture_output=True, text=True)
230
+ if result.returncode != 0:
231
+ raise Exception(f"Failed to create binary file: {result.stderr}")
232
+
233
+ out_path = 'test_bin_to_yaml_type_out.yaml'
234
+ cli_out = 'test_bin_to_yaml_type_cli.yaml'
235
+ for p in (out_path, cli_out):
236
+ if os.path.exists(p):
237
+ os.unlink(p)
238
+
239
+ try:
240
+ # API: pass the type directly; the target file does not exist yet.
241
+ bin_to_yaml('test_bin_to_yaml_type.bin', out_path,
242
+ schema_module='team.api', schema_type='Team')
243
+ with open(out_path, 'r') as f:
244
+ output_data = yaml.safe_load(f)
245
+ assert output_data['_meta']['schema_module'] == 'team.api'
246
+ assert output_data['_meta']['schema_type'] == 'Team'
247
+ assert output_data['name'] == "Dream Team", "Team name doesn't match"
248
+ assert 'members' in output_data, "members field missing"
249
+
250
+ # CLI: end-to-end --type path.
251
+ r = subprocess.run(
252
+ ['zs-yaml', 'test_bin_to_yaml_type.bin', cli_out, '--type', 'team.api.Team'],
253
+ capture_output=True, text=True)
254
+ assert r.returncode == 0, f"CLI --type failed: {r.stderr}"
255
+ with open(cli_out, 'r') as f:
256
+ cli_data = yaml.safe_load(f)
257
+ assert cli_data['name'] == "Dream Team"
258
+
259
+ print(" ✓ bin -> yaml via --type successful")
260
+ return True
261
+ finally:
262
+ for p in ('test_bin_to_yaml_type.bin', out_path, cli_out):
263
+ if os.path.exists(p):
264
+ os.unlink(p)
265
+
266
+
220
267
  def test_yaml_to_yaml_with_template_args():
221
268
  """Test yaml_to_yaml with template argument substitution"""
222
269
  print("\nTesting yaml_to_yaml with template arguments...")
@@ -279,6 +326,56 @@ members:
279
326
  raise
280
327
 
281
328
 
329
+ def test_descriptor_cache_keyed_on_object_not_id():
330
+ """Regression: _COMPOUND_CACHE must not alias distinct TypeInfos by id().
331
+
332
+ Previously the cache keyed on ``id(type_info)`` while only storing the
333
+ descriptor. Each call to a generated ``type_info()`` returns a fresh
334
+ ``TypeInfo`` instance, so after GC reclaims one, a later call can land at
335
+ the same memory address and the cache returns a stale descriptor —
336
+ surfacing as ``'X' object has no attribute 'y'`` errors during
337
+ ``bin_to_dict`` / ``bin_to_yaml`` in long-running processes that load
338
+ multiple schemas.
339
+
340
+ We simulate the collision by seeding the cache with an int key matching
341
+ ``id(type_info)`` whose value is an unrelated descriptor, then verify the
342
+ lookup still returns the correct descriptor for that ``type_info``.
343
+ """
344
+ from team.api import Team, Person
345
+ from zs_yaml import convert
346
+
347
+ print("Testing _COMPOUND_CACHE id-reuse regression...")
348
+
349
+ ti_team = Team.type_info()
350
+ ti_person = Person.type_info()
351
+
352
+ convert._COMPOUND_CACHE.clear()
353
+ desc_team = convert._compound_descriptor(ti_team)
354
+ desc_person = convert._compound_descriptor(ti_person)
355
+ assert desc_team is not desc_person, "Distinct TypeInfos must produce distinct descriptors"
356
+
357
+ # Simulate the previous bug: an int key matching id(ti_person) carrying
358
+ # an unrelated descriptor (as if a freed TypeInfo had occupied that
359
+ # address before GC reclaimed it). A correctly-keyed cache (keyed on the
360
+ # TypeInfo object) ignores the stale int-keyed entry; a buggy id-keyed
361
+ # cache would return desc_team for ti_person.
362
+ convert._COMPOUND_CACHE[id(ti_person)] = desc_team
363
+ desc_person_again = convert._compound_descriptor(ti_person)
364
+
365
+ if desc_person_again is not desc_person:
366
+ team_fields = [f.schema_name for f in desc_team.fields]
367
+ person_fields_expected = [f.schema_name for f in desc_person.fields]
368
+ person_fields_actual = [f.schema_name for f in desc_person_again.fields]
369
+ raise AssertionError(
370
+ "_COMPOUND_CACHE returned a stale descriptor — id() reuse regression. "
371
+ f"Expected fields={person_fields_expected}, got={person_fields_actual} "
372
+ f"(matches Team fields={team_fields})."
373
+ )
374
+
375
+ print(" ✓ cache returns correct descriptor under simulated id() collision")
376
+ return True
377
+
378
+
282
379
  if __name__ == "__main__":
283
380
  try:
284
381
  # Run all tests
@@ -286,8 +383,10 @@ if __name__ == "__main__":
286
383
  json_path = test_yaml_to_json()
287
384
  test_json_to_yaml(json_path)
288
385
  test_bin_to_yaml()
386
+ test_bin_to_yaml_with_type_arg()
289
387
  test_yaml_to_yaml_with_template_args()
290
-
388
+ test_descriptor_cache_keyed_on_object_not_id()
389
+
291
390
  print("\n✅ All conversion tests passed!")
292
391
  sys.exit(0)
293
392
  except Exception as e:
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.10.1.dev2'
22
- __version_tuple__ = version_tuple = (0, 10, 1, 'dev2')
21
+ __version__ = version = '0.10.2.dev3'
22
+ __version_tuple__ = version_tuple = (0, 10, 2, 'dev3')
23
23
 
24
- __commit_id__ = commit_id = 'g694552b85'
24
+ __commit_id__ = commit_id = 'g02dbf3d05'
@@ -540,34 +540,58 @@ def bin_to_dict(bin_input, schema_module, schema_type, init_args=None):
540
540
  )
541
541
 
542
542
 
543
- def bin_to_yaml(bin_input_path, yaml_output_path):
543
+ def bin_to_yaml(bin_input_path, yaml_output_path, schema_module=None, schema_type=None, init_args=None):
544
544
  """
545
545
  Converts a binary file to a YAML file using Zserio deserialization.
546
546
 
547
+ The deserialization type can be supplied in two ways:
548
+
549
+ 1. By passing ``schema_module`` and ``schema_type`` directly. In this case the
550
+ output YAML file does not need to exist beforehand; a minimal ``_meta``
551
+ block is synthesized from the provided type.
552
+ 2. By leaving them as ``None`` (the default). Then the target YAML file must
553
+ already exist and contain a ``_meta`` block with ``schema_module`` and
554
+ ``schema_type`` (the original behavior, preserved for backward compatibility).
555
+ Any extra ``_meta`` keys (e.g. ``transformation_module``) are kept.
556
+
547
557
  Args:
548
558
  bin_input_path (str): Path to the input binary file.
549
559
  yaml_output_path (str): Path to the output YAML file.
560
+ schema_module (str, optional): Fully-qualified schema module name.
561
+ schema_type (str, optional): Schema type name.
562
+ init_args (list, optional): Initialization arguments for deserialization.
550
563
 
551
564
  Raises:
552
- ValueError: If schema_module and schema_type are not specified in the _meta
553
- section of the YAML file, or if the specified Zserio type is not found
554
- in the module.
565
+ ValueError: If the type cannot be determined (neither passed directly nor
566
+ available in the target YAML file's ``_meta`` section).
555
567
  """
556
568
  try:
557
- with open(yaml_output_path, 'r') as yaml_file:
558
- meta = yaml.safe_load(yaml_file)
559
-
560
- schema_module = meta.get('_meta', {}).get('schema_module')
561
- schema_type = meta.get('_meta', {}).get('schema_type')
562
- init_args = meta.get('_meta', {}).get('initialization_args', [])
563
-
564
- if not schema_module or not schema_type:
565
- raise ValueError("Error: schema_module and schema_type must be specified in the _meta section of the YAML file")
569
+ if schema_module and schema_type:
570
+ # Type provided directly: no pre-existing target file required.
571
+ meta_block = {'schema_module': schema_module, 'schema_type': schema_type}
572
+ if init_args:
573
+ meta_block['initialization_args'] = init_args
574
+ else:
575
+ # Backward-compatible: read the type from the existing target's _meta.
576
+ with open(yaml_output_path, 'r') as yaml_file:
577
+ meta = yaml.safe_load(yaml_file)
578
+
579
+ meta_block = (meta or {}).get('_meta', {})
580
+ schema_module = meta_block.get('schema_module')
581
+ schema_type = meta_block.get('schema_type')
582
+ init_args = meta_block.get('initialization_args', [])
583
+
584
+ if not schema_module or not schema_type:
585
+ raise ValueError(
586
+ "Error: schema_module and schema_type must be specified in the _meta "
587
+ "section of the target YAML file, or passed via schema_module/schema_type "
588
+ "(CLI: --type)"
589
+ )
566
590
 
567
591
  data, metadata = bin_to_dict(bin_input_path, schema_module, schema_type, init_args)
568
592
 
569
593
  # Create a new dictionary to ensure _meta comes first
570
- final_data = {'_meta': meta['_meta']}
594
+ final_data = {'_meta': meta_block}
571
595
  final_data.update(data)
572
596
 
573
597
  with open(yaml_output_path, 'w') as yaml_file:
@@ -19,8 +19,9 @@ def parse_arguments():
19
19
  description=
20
20
  f'%(prog)s {get_version_info()}\n\n'
21
21
  'Converts between YAML, JSON, and binary formats. '
22
- 'To convert from binary to YAML, the target YAML file must already exist with metadata. '
23
- 'The metadata is required to identify the correct Python type for deserialization.\n\n'
22
+ 'To convert from binary to YAML, identify the type either by passing '
23
+ '--type <module.TypeName>, or via a pre-existing target YAML file that '
24
+ 'already contains the metadata.\n\n'
24
25
  'The minimal metadata content in the target YAML file should be:\n'
25
26
  ' _meta:\n'
26
27
  ' schema_module: <module_name>\n'
@@ -36,6 +37,17 @@ def parse_arguments():
36
37
  )
37
38
  parser.add_argument('input_path', type=str, help='Path to the input file (YAML, JSON, or binary)')
38
39
  parser.add_argument('output_path', type=str, nargs='?', help='Path to the output file (YAML, JSON, or binary)')
40
+ parser.add_argument(
41
+ '--type', dest='qualified_type', type=str, default=None,
42
+ help='Fully-qualified zserio type for binary -> YAML, e.g. '
43
+ 'pkg.module.TypeName. Alternative to a pre-existing target file '
44
+ 'with _meta; when given, the target need not exist beforehand.'
45
+ )
46
+ parser.add_argument(
47
+ '--init-args', dest='init_args', nargs='*', default=None,
48
+ help='Initialization arguments for the zserio type (binary -> YAML). '
49
+ 'Integers (incl. 0x..) are parsed as ints, everything else as strings.'
50
+ )
39
51
  parser.add_argument('--version', action='version', version=f'%(prog)s {get_version_info()}')
40
52
 
41
53
  if len(sys.argv) < 2:
@@ -62,10 +74,33 @@ def process_yaml_input(input_path, output_path):
62
74
 
63
75
  return output_path
64
76
 
65
- def process_binary_input(input_path, output_path):
77
+ def _split_qualified_type(qualified_type):
78
+ """Split 'pkg.module.TypeName' into (schema_module, schema_type)."""
79
+ if '.' not in qualified_type:
80
+ raise ValueError(
81
+ f"--type must be a fully-qualified type 'module.TypeName', got '{qualified_type}'"
82
+ )
83
+ schema_module, schema_type = qualified_type.rsplit('.', 1)
84
+ return schema_module, schema_type
85
+
86
+ def _parse_init_args(raw_args):
87
+ """Parse CLI init-args: ints (incl. 0x..) as ints, otherwise as strings."""
88
+ parsed = []
89
+ for a in raw_args:
90
+ try:
91
+ parsed.append(int(a, 0))
92
+ except ValueError:
93
+ parsed.append(a)
94
+ return parsed
95
+
96
+ def process_binary_input(input_path, output_path, qualified_type=None, init_args=None):
66
97
  if not output_path:
67
98
  raise ValueError("Output path must be specified for binary input")
68
- bin_to_yaml(input_path, output_path)
99
+ schema_module = schema_type = None
100
+ if qualified_type:
101
+ schema_module, schema_type = _split_qualified_type(qualified_type)
102
+ parsed_init_args = _parse_init_args(init_args) if init_args else None
103
+ bin_to_yaml(input_path, output_path, schema_module, schema_type, parsed_init_args)
69
104
  return output_path
70
105
 
71
106
  def process_json_input(input_path, output_path):
@@ -102,7 +137,9 @@ def main():
102
137
  if input_extension == '.yaml':
103
138
  output_path = process_yaml_input(args.input_path, args.output_path)
104
139
  elif input_extension == '.bin' or input_extension == '':
105
- output_path = process_binary_input(args.input_path, args.output_path)
140
+ output_path = process_binary_input(
141
+ args.input_path, args.output_path, args.qualified_type, args.init_args
142
+ )
106
143
  elif input_extension == '.json':
107
144
  output_path = process_json_input(args.input_path, args.output_path)
108
145
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zs-yaml
3
- Version: 0.10.1.dev2
3
+ Version: 0.10.2.dev3
4
4
  Summary: YAML support for zserio
5
5
  Author-email: "Navigation Data Standard e.V." <support@nds-association.org>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes