Gen3SchemaDev 2.5.0__tar.gz → 2.6.1__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. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/PKG-INFO +3 -10
  2. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/README.md +2 -9
  3. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/pyproject.toml +1 -1
  4. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/cli.py +28 -58
  5. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/converter.py +8 -13
  6. gen3schemadev-2.6.1/src/gen3schemadev/refs.py +74 -0
  7. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/program.yaml +1 -2
  8. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/project.yaml +2 -4
  9. gen3schemadev-2.5.0/src/gen3schemadev/refs.py +0 -204
  10. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/LICENSE +0 -0
  11. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/__init__.py +0 -0
  12. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/ddvis.py +0 -0
  13. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/__init__.py +0 -0
  14. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/gen3_template.py +0 -0
  15. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/input_schema.py +0 -0
  16. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/_definitions.yaml +0 -0
  17. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/_settings.yaml +0 -0
  18. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/_terms.yaml +0 -0
  19. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/core_metadata_collection.yaml +0 -0
  20. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/schema/schema_templates/gen3_metaschema.yml +0 -0
  21. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/utils.py +0 -0
  22. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/validators/input_validator.py +0 -0
  23. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/validators/metaschema_validator.py +0 -0
  24. {gen3schemadev-2.5.0 → gen3schemadev-2.6.1}/src/gen3schemadev/validators/rule_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Gen3SchemaDev
3
- Version: 2.5.0
3
+ Version: 2.6.1
4
4
  Summary: Tool for data modelling in Gen3
5
5
  License: Apache 2.0
6
6
  License-File: LICENSE
@@ -35,16 +35,9 @@ This repository aims to provide the documentation, learning materials, and softw
35
35
  - [Guide to creating your first dictionary](docs/gen3schemadev/first_dictionary.md)
36
36
  - [Troubleshooting](docs/gen3schemadev/troubleshooting.md)
37
37
 
38
- ### Fixing `$ref` properties in an existing dictionary
38
+ ### Null description placeholders
39
39
 
40
- JSON Schema draft-04 (which Gen3 uses) ignores any keyword sitting as a direct sibling of `$ref`, so properties written as `{description: ..., $ref: _definitions.yaml#/X}` lose their description in the data-dictionary viewer. The `fix-refs` command rewrites such properties in place, moving the `$ref` into an `allOf` list so annotations survive resolution:
41
-
42
- ```bash
43
- gen3schemadev fix-refs -y path/to/dictionary --dry-run # preview changes
44
- gen3schemadev fix-refs -y path/to/dictionary # rewrite in place
45
- ```
46
-
47
- The command is idempotent and never touches bare refs, refs already inside `allOf`/`anyOf`/`oneOf`, the `properties: {$ref: ...}` merge construct, or `_definitions.yaml`/`_terms.yaml`/`_settings.yaml`.
40
+ `gen3schemadev validate` warns about `description: null` placeholders anywhere in the dictionary (commonly in `_definitions.yaml`'s enum definitions). The Gen3 metaschema requires descriptions to be strings; null placeholders cause "No Description" in the data-dictionary viewer and metaschema validation failures that surface on resolved node schemas, far from the offending definition. Remove the null `description` keys to resolve the warning.
48
41
 
49
42
 
50
43
  ## Deep dive into Gen3 Data Modelling
@@ -12,16 +12,9 @@ This repository aims to provide the documentation, learning materials, and softw
12
12
  - [Guide to creating your first dictionary](docs/gen3schemadev/first_dictionary.md)
13
13
  - [Troubleshooting](docs/gen3schemadev/troubleshooting.md)
14
14
 
15
- ### Fixing `$ref` properties in an existing dictionary
15
+ ### Null description placeholders
16
16
 
17
- JSON Schema draft-04 (which Gen3 uses) ignores any keyword sitting as a direct sibling of `$ref`, so properties written as `{description: ..., $ref: _definitions.yaml#/X}` lose their description in the data-dictionary viewer. The `fix-refs` command rewrites such properties in place, moving the `$ref` into an `allOf` list so annotations survive resolution:
18
-
19
- ```bash
20
- gen3schemadev fix-refs -y path/to/dictionary --dry-run # preview changes
21
- gen3schemadev fix-refs -y path/to/dictionary # rewrite in place
22
- ```
23
-
24
- The command is idempotent and never touches bare refs, refs already inside `allOf`/`anyOf`/`oneOf`, the `properties: {$ref: ...}` merge construct, or `_definitions.yaml`/`_terms.yaml`/`_settings.yaml`.
17
+ `gen3schemadev validate` warns about `description: null` placeholders anywhere in the dictionary (commonly in `_definitions.yaml`'s enum definitions). The Gen3 metaschema requires descriptions to be strings; null placeholders cause "No Description" in the data-dictionary viewer and metaschema validation failures that surface on resolved node schemas, far from the offending definition. Remove the null `description` keys to resolve the warning.
25
18
 
26
19
 
27
20
  ## Deep dive into Gen3 Data Modelling
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Gen3SchemaDev"
3
- version = "2.5.0"
3
+ version = "2.6.1"
4
4
  description = "Tool for data modelling in Gen3"
5
5
  authors = [
6
6
  {name = "JoshuaHarris391",email = "harjo391@gmail.com"}
@@ -20,7 +20,25 @@ from gen3schemadev.validators.metaschema_validator import validate_schema_with_m
20
20
  from importlib.metadata import version
21
21
  from gen3schemadev.ddvis import visualise_with_docker
22
22
  from gen3schemadev.validators.rule_validator import RuleValidator
23
- from gen3schemadev.refs import WRAPPED, SKIP_REASONS, fix_yaml_dir
23
+ from gen3schemadev.refs import find_null_descriptions
24
+
25
+
26
+ def print_null_description_warning(hits):
27
+ """
28
+ Print a warning block listing every null-valued 'description' key found
29
+ in the dictionary ("file: dotted.path" strings). No-op when hits is empty.
30
+ """
31
+ if not hits:
32
+ return
33
+ print(
34
+ "\nWARNING: found 'description: null' placeholders. The Gen3 metaschema "
35
+ "requires 'description' to be a string, and null placeholders in shared "
36
+ "definitions fail metaschema validation once exposed through bare or "
37
+ "allOf-wrapped $refs. Remove the null 'description' keys (removing adds "
38
+ "no shared description, so nothing leaks onto referencing properties):"
39
+ )
40
+ for hit in hits:
41
+ print(f" - {hit}")
24
42
 
25
43
 
26
44
  def main():
@@ -144,27 +162,6 @@ def main():
144
162
  help="Set logging level to DEBUG"
145
163
  )
146
164
 
147
- # Create 'fix-refs' subcommand
148
- fixrefs_parser = subparsers.add_parser(
149
- "fix-refs",
150
- help="Rewrite properties with '$ref' siblings into draft-04-safe allOf form (in place)"
151
- )
152
- fixrefs_parser.add_argument(
153
- "-y", "--yamldir",
154
- required=True,
155
- help="Directory of Gen3 YAML files to rewrite in place (recursive)"
156
- )
157
- fixrefs_parser.add_argument(
158
- "--dry-run",
159
- action="store_true",
160
- help="Report what would change without writing files"
161
- )
162
- fixrefs_parser.add_argument(
163
- "--debug",
164
- action="store_true",
165
- help="Set logging level to DEBUG"
166
- )
167
-
168
165
  args = parser.parse_args()
169
166
 
170
167
  # Handle case where no command is provided
@@ -242,6 +239,15 @@ def main():
242
239
  elif args.yamls:
243
240
  schema_dict = bundle_yamls(args.yamls)
244
241
 
242
+ # Pre-resolution diagnostic: report every null 'description' up front,
243
+ # because the metaschema stage fails on the first resolved node schema,
244
+ # far away from the definition that carries the null.
245
+ null_hits = []
246
+ for schema_name, schema in schema_dict.items():
247
+ for hit in find_null_descriptions(schema):
248
+ null_hits.append(f"{schema_name}: {hit}")
249
+ print_null_description_warning(null_hits)
250
+
245
251
  for schema_name, schema in schema_dict.items():
246
252
 
247
253
  if '.' in schema_name:
@@ -328,41 +334,5 @@ def main():
328
334
  output_path = args.output or "input_example.yaml"
329
335
  write_yaml(init_yaml, output_path)
330
336
 
331
- elif args.command == "fix-refs":
332
- if not os.path.isdir(args.yamldir):
333
- print(f"ERROR: Directory not found: {args.yamldir}", file=sys.stderr)
334
- sys.exit(1)
335
-
336
- if args.dry_run:
337
- print(f"Dry run: scanning {args.yamldir} (no files will be written)")
338
- else:
339
- print(f"Fixing $ref sibling annotations in: {args.yamldir}")
340
-
341
- reports = fix_yaml_dir(args.yamldir, dry_run=args.dry_run)
342
-
343
- total_wrapped = 0
344
- files_changed = 0
345
- for report in reports:
346
- print(report["path"])
347
- if report["skipped_file"]:
348
- print(f" skipped file ({report['skipped_file']})")
349
- continue
350
- if not report["changes"]:
351
- print(" no $ref properties found")
352
- continue
353
- for prop_name, action, preserved in report["changes"]:
354
- if action == WRAPPED:
355
- total_wrapped += 1
356
- print(f" wrapped: {prop_name} (preserved: {', '.join(preserved)})")
357
- else:
358
- print(f" skipped ({SKIP_REASONS[action]}): {prop_name}")
359
- if report["rewritten"] or (args.dry_run and any(c[1] == WRAPPED for c in report["changes"])):
360
- files_changed += 1
361
-
362
- files_unchanged = len(reports) - files_changed
363
- verb = "would be wrapped" if args.dry_run else "wrapped"
364
- print(f"\nSummary: {total_wrapped} properties {verb} in {files_changed} files; "
365
- f"{files_unchanged} files unchanged.")
366
-
367
337
  if __name__ == "__main__":
368
338
  main()
@@ -17,8 +17,6 @@ from dataclasses import dataclass, asdict
17
17
  from typing import Protocol, runtime_checkable, Dict, Any, List
18
18
  import logging
19
19
 
20
- from gen3schemadev.refs import wrap_ref_siblings
21
-
22
20
 
23
21
  logger = logging.getLogger(__name__)
24
22
 
@@ -490,11 +488,11 @@ def format_datetime(prop_dict: dict) -> dict:
490
488
  Formats a property dictionary with a 'type' of 'datetime' to use a $ref
491
489
  to the Gen3 _definitions.yaml#/datetime definition.
492
490
 
493
- Annotations such as 'description' are preserved by wrapping the $ref in
494
- an allOf list: under JSON Schema draft-04, keywords sitting as direct
495
- siblings of $ref are ignored during resolution, so they must live beside
496
- allOf instead. A datetime property with no annotations becomes a bare
497
- $ref. If the property is not of type 'datetime', it is returned unchanged.
491
+ Annotations such as 'description' are preserved as siblings of the $ref:
492
+ Gen3's resolver merges sibling keys over the referenced definition, so
493
+ the property's own description survives resolution. A datetime property
494
+ with no annotations becomes a bare $ref. If the property is not of type
495
+ 'datetime', it is returned unchanged.
498
496
 
499
497
  Example input:
500
498
  {
@@ -508,9 +506,7 @@ def format_datetime(prop_dict: dict) -> dict:
508
506
  {
509
507
  "collection_date": {
510
508
  "description": "Date and time of collection (datetime)",
511
- "allOf": [
512
- {"$ref": "_definitions.yaml#/datetime"}
513
- ]
509
+ "$ref": "_definitions.yaml#/datetime"
514
510
  }
515
511
  }
516
512
 
@@ -532,9 +528,8 @@ def format_datetime(prop_dict: dict) -> dict:
532
528
  value = prop_dict[first_key]
533
529
 
534
530
  if isinstance(value, dict) and value.get('type') == 'datetime':
535
- ref_prop = {k: v for k, v in value.items() if k != 'type'}
536
- ref_prop['$ref'] = "_definitions.yaml#/datetime"
537
- formatted_props, _ = wrap_ref_siblings(ref_prop, first_key)
531
+ formatted_props = {k: v for k, v in value.items() if k != 'type'}
532
+ formatted_props['$ref'] = "_definitions.yaml#/datetime"
538
533
  else:
539
534
  formatted_props = value
540
535
 
@@ -0,0 +1,74 @@
1
+ """Diagnostics for ``$ref`` usage in Gen3 data dictionaries.
2
+
3
+ Gen3's resolver merges a property's sibling keys over the referenced
4
+ definition, so writing annotations next to a ``$ref`` is the normal,
5
+ working form::
6
+
7
+ atrial_fibrillation:
8
+ description: "Self-reported atrial fibrillation."
9
+ $ref: "_definitions.yaml#/enum_yes_no"
10
+
11
+ The real hazard is a ``description: null`` placeholder inside a shared
12
+ definition (commonly the enum definitions in ``_definitions.yaml``): the
13
+ Gen3 metaschema requires ``description`` to be a string, and the null shows
14
+ up as "No Description" in the data-dictionary viewer and fails metaschema
15
+ validation on resolved node schemas — far from the definition that carries
16
+ it. ``find_null_descriptions`` reports every such placeholder by path so
17
+ the root cause is named up front.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _COMBINATORS = ("allOf", "anyOf", "oneOf")
27
+
28
+
29
+ def has_ref(prop) -> bool:
30
+ """
31
+ Return True if a property definition references another schema, either
32
+ via a top-level ``$ref`` or via a ``$ref`` inside an allOf/anyOf/oneOf
33
+ list item.
34
+ """
35
+ if not isinstance(prop, dict):
36
+ return False
37
+ if "$ref" in prop:
38
+ return True
39
+ for combinator in _COMBINATORS:
40
+ items = prop.get(combinator)
41
+ if isinstance(items, list):
42
+ if any(isinstance(item, dict) and "$ref" in item for item in items):
43
+ return True
44
+ return False
45
+
46
+
47
+ def find_null_descriptions(node, path: str = "") -> list:
48
+ """
49
+ Recursively find every ``description`` key whose value is null.
50
+
51
+ The Gen3 metaschema requires ``description`` to be a string, so a
52
+ ``description: null`` placeholder (common in generated shared
53
+ definitions) is invalid. Because the property's own description merges
54
+ over the definition's during resolution, the null often stays hidden
55
+ until it surfaces on some resolved node schema — where the resulting
56
+ metaschema failure points far away from the definition that caused it.
57
+
58
+ Returns a list of dotted paths to each offender, with list items shown
59
+ as ``[i]`` (e.g. ``"enum_yes_no.description"``,
60
+ ``"properties.status.anyOf[0].description"``). Non-dict/list input
61
+ yields an empty list.
62
+ """
63
+ hits = []
64
+ if isinstance(node, dict):
65
+ for key, value in node.items():
66
+ key_path = f"{path}.{key}" if path else str(key)
67
+ if key == "description" and value is None:
68
+ hits.append(key_path)
69
+ else:
70
+ hits.extend(find_null_descriptions(value, key_path))
71
+ elif isinstance(node, list):
72
+ for i, item in enumerate(node):
73
+ hits.extend(find_null_descriptions(item, f"{path}[{i}]"))
74
+ return hits
@@ -24,9 +24,8 @@ properties:
24
24
  type:
25
25
  type: string
26
26
  id:
27
+ $ref: _definitions.yaml#/UUID
27
28
  systemAlias: node_id
28
- allOf:
29
- - $ref: _definitions.yaml#/UUID
30
29
  name:
31
30
  type: string
32
31
  description: Full name/title of the program.
@@ -35,10 +35,9 @@ properties:
35
35
  type:
36
36
  type: string
37
37
  id:
38
+ $ref: _definitions.yaml#/UUID
38
39
  systemAlias: node_id
39
40
  description: UUID for the project.
40
- allOf:
41
- - $ref: _definitions.yaml#/UUID
42
41
  name:
43
42
  type: string
44
43
  description: Display name/brief description for the project.
@@ -63,11 +62,10 @@ properties:
63
62
  description: The ID of the source providing support/grant resources.
64
63
  type: string
65
64
  programs:
65
+ $ref: _definitions.yaml#/to_one
66
66
  description: 'Indicates that the Study is logically part of the indicated Study.
67
67
 
68
68
  '
69
- allOf:
70
- - $ref: _definitions.yaml#/to_one
71
69
  state:
72
70
  description: 'The possible states a project can be in. All but `open` are
73
71
 
@@ -1,204 +0,0 @@
1
- """Utilities for making ``$ref`` properties safe under JSON Schema draft-04.
2
-
3
- Gen3 compiles data dictionaries with draft-04 semantics, where any keyword
4
- sitting as a direct sibling of ``$ref`` is ignored during resolution. A
5
- property written as::
6
-
7
- atrial_fibrillation:
8
- description: "Self-reported atrial fibrillation."
9
- $ref: "_definitions.yaml#/enum_yes_no"
10
-
11
- loses its ``description`` (and ``termDef``, ``term``, etc.) when resolved by
12
- strict tooling such as dictionaryutils, so the data-dictionary viewer shows
13
- "No Description". The fix is structural: keep the annotations at the top
14
- level and move the ``$ref`` into an ``allOf`` list::
15
-
16
- atrial_fibrillation:
17
- description: "Self-reported atrial fibrillation."
18
- allOf:
19
- - $ref: "_definitions.yaml#/enum_yes_no"
20
-
21
- This module is the single source of truth for that transformation. It is
22
- used by the schema generator (``converter.format_datetime``), the rule
23
- validator, and the ``gen3schemadev fix-refs`` CLI command.
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- import fnmatch
29
- import logging
30
- import os
31
-
32
- logger = logging.getLogger(__name__)
33
-
34
- # Actions recorded for each property examined by the transform.
35
- WRAPPED = "wrapped"
36
- BARE_REF = "bare-ref"
37
- ALREADY_WRAPPED = "already-wrapped"
38
- NO_REF = "no-ref"
39
- PROPERTIES_MERGE = "properties-merge"
40
-
41
- # Human-readable reasons for skipped properties, keyed by action.
42
- SKIP_REASONS = {
43
- BARE_REF: "bare $ref, no siblings to preserve",
44
- ALREADY_WRAPPED: "already wrapped in allOf/anyOf/oneOf",
45
- PROPERTIES_MERGE: "$ref key in 'properties' is a Gen3 properties-merge",
46
- }
47
-
48
- _COMBINATORS = ("allOf", "anyOf", "oneOf")
49
-
50
-
51
- def has_ref(prop) -> bool:
52
- """
53
- Return True if a property definition references another schema, either
54
- via a top-level ``$ref`` or via a ``$ref`` inside an allOf/anyOf/oneOf
55
- list item.
56
- """
57
- if not isinstance(prop, dict):
58
- return False
59
- if "$ref" in prop:
60
- return True
61
- for combinator in _COMBINATORS:
62
- items = prop.get(combinator)
63
- if isinstance(items, list):
64
- if any(isinstance(item, dict) and "$ref" in item for item in items):
65
- return True
66
- return False
67
-
68
-
69
- def wrap_ref_siblings(prop, prop_name: str = "") -> "tuple":
70
- """
71
- Rewrite a single property definition so sibling annotations survive
72
- draft-04 ``$ref`` resolution.
73
-
74
- Returns a ``(new_prop, action)`` tuple. The transformation only fires
75
- when the property dict has a top-level ``$ref`` alongside at least one
76
- other key; in that case the siblings keep their original order and the
77
- ``$ref`` moves into an ``allOf`` list appended last. All other shapes
78
- are returned unchanged:
79
-
80
- - not a dict, or no ``$ref`` anywhere -> ``NO_REF``
81
- - ``$ref`` only inside allOf/anyOf/oneOf -> ``ALREADY_WRAPPED`` (idempotent)
82
- - ``{"$ref": ...}`` with no siblings -> ``BARE_REF`` (nothing is lost)
83
- """
84
- if not isinstance(prop, dict) or not has_ref(prop):
85
- return prop, NO_REF
86
-
87
- if "$ref" not in prop:
88
- return prop, ALREADY_WRAPPED
89
-
90
- if len(prop) == 1:
91
- return prop, BARE_REF
92
-
93
- new_prop = {k: v for k, v in prop.items() if k != "$ref"}
94
- if isinstance(new_prop.get("allOf"), list):
95
- # Rare edge: the property already has an allOf next to the $ref.
96
- # Append rather than overwrite so existing constraints are kept.
97
- new_prop["allOf"] = new_prop["allOf"] + [{"$ref": prop["$ref"]}]
98
- else:
99
- new_prop["allOf"] = [{"$ref": prop["$ref"]}]
100
- logger.debug(f"Wrapped $ref of property '{prop_name}' in allOf.")
101
- return new_prop, WRAPPED
102
-
103
-
104
- def fix_schema(schema: dict) -> "tuple":
105
- """
106
- Apply ``wrap_ref_siblings`` to every property of a node schema.
107
-
108
- Only the direct values of ``schema["properties"]`` are examined:
109
- sibling-``$ref`` annotations occur at the property level in Gen3 node
110
- schemas, and a shallow walk cannot accidentally rewrite the Gen3
111
- properties-merge construct (a ``$ref`` *key* directly inside the
112
- ``properties`` block), which is recorded as ``PROPERTIES_MERGE`` and
113
- left untouched.
114
-
115
- Returns ``(new_schema, changes)`` where changes is a list of
116
- ``(prop_name, action, preserved_keys)`` tuples. ``preserved_keys`` names
117
- the sibling annotation keys kept for ``WRAPPED`` entries and is an empty
118
- tuple otherwise.
119
- """
120
- changes = []
121
- props = schema.get("properties") if isinstance(schema, dict) else None
122
- if not isinstance(props, dict):
123
- return schema, changes
124
-
125
- new_props = {}
126
- for prop_name, prop in props.items():
127
- if prop_name == "$ref":
128
- new_props[prop_name] = prop
129
- changes.append((prop_name, PROPERTIES_MERGE, ()))
130
- continue
131
- new_prop, action = wrap_ref_siblings(prop, prop_name)
132
- new_props[prop_name] = new_prop
133
- preserved = ()
134
- if action == WRAPPED:
135
- preserved = tuple(k for k in prop.keys() if k != "$ref")
136
- if action != NO_REF:
137
- changes.append((prop_name, action, preserved))
138
-
139
- new_schema = dict(schema)
140
- new_schema["properties"] = new_props
141
- return new_schema, changes
142
-
143
-
144
- def fix_yaml_dir(yaml_dir: str, dry_run: bool = False) -> list:
145
- """
146
- Rewrite every YAML node schema under ``yaml_dir`` (recursively) so that
147
- ``$ref`` sibling annotations survive draft-04 resolution.
148
-
149
- Files whose basename starts with an underscore (``_definitions.yaml``,
150
- ``_terms.yaml``, ``_settings.yaml``) are skipped entirely: adding
151
- structure or descriptions to shared definitions would force them onto
152
- every referencing property. Files with no changes are never rewritten,
153
- which keeps the operation idempotent. With ``dry_run=True`` nothing is
154
- written at all.
155
-
156
- Returns a list of per-file report dicts::
157
-
158
- {"path": <relative path>, "skipped_file": <reason or None>,
159
- "changes": [(prop_name, action, preserved_keys), ...],
160
- "rewritten": <bool>}
161
- """
162
- from gen3schemadev.utils import load_yaml, write_yaml
163
-
164
- reports = []
165
- for root, _dirs, files in os.walk(yaml_dir):
166
- for fname in sorted(files):
167
- if not (fnmatch.fnmatch(fname, "*.yaml") or fnmatch.fnmatch(fname, "*.yml")):
168
- continue
169
- path = os.path.join(root, fname)
170
- rel_path = os.path.relpath(path, yaml_dir)
171
-
172
- if fname.startswith("_"):
173
- reports.append({
174
- "path": rel_path,
175
- "skipped_file": "definitions/terms/settings file",
176
- "changes": [],
177
- "rewritten": False,
178
- })
179
- continue
180
-
181
- schema = load_yaml(path)
182
- if not isinstance(schema, dict):
183
- reports.append({
184
- "path": rel_path,
185
- "skipped_file": "not a mapping, no properties to fix",
186
- "changes": [],
187
- "rewritten": False,
188
- })
189
- continue
190
-
191
- new_schema, changes = fix_schema(schema)
192
- wrapped = [c for c in changes if c[1] == WRAPPED]
193
- rewritten = False
194
- if wrapped and not dry_run:
195
- write_yaml(new_schema, path)
196
- rewritten = True
197
-
198
- reports.append({
199
- "path": rel_path,
200
- "skipped_file": None,
201
- "changes": changes,
202
- "rewritten": rewritten,
203
- })
204
- return reports
File without changes