gcf-python 2.2.0__tar.gz → 2.2.2__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.
- {gcf_python-2.2.0 → gcf_python-2.2.2}/CHANGELOG.md +23 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/PKG-INFO +11 -7
- {gcf_python-2.2.0 → gcf_python-2.2.2}/README.md +9 -5
- {gcf_python-2.2.0 → gcf_python-2.2.2}/pyproject.toml +2 -2
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/__init__.py +3 -2
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/decode_generic.py +19 -10
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/generic.py +92 -34
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_conformance_v2.py +2 -2
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_generic.py +51 -1
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_roundtrip_v2.py +98 -22
- gcf_python-2.2.2/uv.lock +8 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/.github/FUNDING.yml +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/.github/workflows/ci.yml +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/.github/workflows/publish.yml +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/.gitignore +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/LICENSE +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/assets/gcf-python-diagram.png +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/__main__.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/cli.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/constants.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/decode.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/delta.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/encode.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/scalar.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/session.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/stream.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/stream_generic.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/src/gcf/types.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/__init__.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_decode.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_delta.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_encode.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_roundtrip.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_session.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_stream.py +0 -0
- {gcf_python-2.2.0 → gcf_python-2.2.2}/tests/test_stream_generic.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.2.2 (2026-07-10)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **Losslessness (nested null):** a nested object that is null at an intermediate level (e.g. `{"meta": {"owner": None}}`) is no longer flattened. Previously its leaves encoded as absent (`~`) and unflattened to a missing key, silently dropping the null. Such fields now fall back to the attachment mechanism; a top-level `None` still flattens losslessly (emits `-`, reconstructs via the all-null rule). Enforced by the shared conformance fixtures `flatten/017`–`019`. Prototype pollution does not affect Python (dicts have no mutable prototype).
|
|
8
|
+
|
|
9
|
+
### Tests
|
|
10
|
+
|
|
11
|
+
- `test_flatten_roundtrip`: aligned arrays whose shared fields are fixed-shape nested objects with a field or an intermediate nested level sometimes null/absent — the shape the prior scalar-only generator never produced, leaving the flatten/unflatten path unexercised. Verified to fail on the pre-fix encoder and pass on the fix.
|
|
12
|
+
|
|
13
|
+
## v2.2.1 (2026-06-23)
|
|
14
|
+
|
|
15
|
+
### Flatten Opt-Out
|
|
16
|
+
|
|
17
|
+
- Added `GenericOptions` dataclass with `no_flatten` field to disable nested object flattening
|
|
18
|
+
- `encode_generic(data, GenericOptions(no_flatten=True))` produces attachment syntax instead of path columns
|
|
19
|
+
- Backward compatible: `encode_generic(data)` behavior unchanged (flatten on by default)
|
|
20
|
+
- Fixed: field names containing `>` no longer appear as tabular columns (spec rule 7.4.6.1.4)
|
|
21
|
+
- Fixed: field names containing `>` no longer eligible for flattening analysis
|
|
22
|
+
- Fixed: decoder no longer treats literal `>` in key names as a path separator
|
|
23
|
+
- Fixed: decoder accepts orphan attachments (fields excluded from column list)
|
|
24
|
+
- 12 targeted edge case tests for `>` in field names
|
|
25
|
+
|
|
3
26
|
## v2.2.0 (2026-06-22)
|
|
4
27
|
|
|
5
28
|
### Spec v3.2: Nested Object Flattening
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gcf-python
|
|
3
|
-
Version: 2.2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.2.2
|
|
4
|
+
Summary: The AI-native wire format for structured data. 50-92% fewer tokens than JSON. 100% comprehension on every frontier model. Zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
|
|
6
6
|
Project-URL: Documentation, https://blackwell-systems.github.io/gcf/
|
|
7
7
|
Project-URL: Specification, https://github.com/blackwell-systems/gcf
|
|
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
Python implementation of [GCF](https://gcformat.com/) — the most token-efficient wire format for LLMs. A drop-in alternative to JSON and TOON for any structured data.
|
|
34
34
|
|
|
35
|
-
**100% comprehension on every frontier model tested.
|
|
35
|
+
**100% comprehension on every frontier model tested. 29% fewer tokens than TOON, 56% fewer than JSON across 16 datasets. 91.2% on structurally complex code graphs (vs TOON 68.8%, JSON 54.1%). 2,400+ LLM evaluations. Zero training.**
|
|
36
36
|
|
|
37
37
|
Docs: [gcformat.com](https://gcformat.com/) · [Playground](https://gcformat.com/playground.html) · [GCF vs TOON](https://gcformat.com/guide/vs-toon.html)
|
|
38
38
|
|
|
@@ -206,16 +206,16 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
|
|
|
206
206
|
|
|
207
207
|
## Benchmarks
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
2,400+ LLM evaluations across 10 models, 3 providers, and 51 independent test runs.
|
|
210
210
|
|
|
211
211
|
| | GCF | TOON | JSON |
|
|
212
212
|
|---|---|---|---|
|
|
213
|
-
| **Comprehension** (23 runs, 10 models) | **
|
|
213
|
+
| **Comprehension** (23 runs, 10 models) | **91.2%** | 68.8% | 54.1% |
|
|
214
214
|
| **Generation** (28 runs, 9 models) | **5/5** | 1.0/5 | 5.0/5 |
|
|
215
215
|
| **Input tokens** (500 symbols) | **11,090** | 16,378 | 53,341 |
|
|
216
216
|
| **Output tokens** (100 symbols) | **5,976** | 8,937 | 16,121 |
|
|
217
217
|
|
|
218
|
-
GCF wins
|
|
218
|
+
GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://github.com/blackwell-systems/toon/tree/gcf-comparison). Full results: [gcformat.com/guide/benchmarks](https://gcformat.com/guide/benchmarks.html)
|
|
219
219
|
|
|
220
220
|
## Implementations
|
|
221
221
|
|
|
@@ -236,7 +236,11 @@ GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://git
|
|
|
236
236
|
|
|
237
237
|
**Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
238
238
|
|
|
239
|
-
**Specification:** [SPEC v3.
|
|
239
|
+
**Specification:** [SPEC v3.2 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 174 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.2.1+ (Go v1.3.1). Cross-language 6x6 matrix verified.
|
|
240
|
+
|
|
241
|
+
## Adopted by
|
|
242
|
+
|
|
243
|
+
[Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) (46K stars, Google Chrome DevTools team) · [Speakeasy](https://speakeasy.com) (API tooling, customers include Google, Verizon, Mistral AI, DocuSign, Vercel) · [OmniRoute](https://omniroute.online) (6.1K stars) · [NetClaw](https://github.com/automateyournetwork/netclaw) (556 stars) · [ctx](https://github.com/stevesolun/ctx) (510 stars) · [NeuroNest](https://neuronest.cc) · [Open Data Products SDK](https://opendataproducts.org/sdk/) (Linux Foundation) · [Raycast](https://raycast.com/blackwell-systems/json-to-gcf-converter) · [and more](https://gcformat.com/ecosystem/adopters.html)
|
|
240
244
|
|
|
241
245
|
## License
|
|
242
246
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Python implementation of [GCF](https://gcformat.com/) — the most token-efficient wire format for LLMs. A drop-in alternative to JSON and TOON for any structured data.
|
|
9
9
|
|
|
10
|
-
**100% comprehension on every frontier model tested.
|
|
10
|
+
**100% comprehension on every frontier model tested. 29% fewer tokens than TOON, 56% fewer than JSON across 16 datasets. 91.2% on structurally complex code graphs (vs TOON 68.8%, JSON 54.1%). 2,400+ LLM evaluations. Zero training.**
|
|
11
11
|
|
|
12
12
|
Docs: [gcformat.com](https://gcformat.com/) · [Playground](https://gcformat.com/playground.html) · [GCF vs TOON](https://gcformat.com/guide/vs-toon.html)
|
|
13
13
|
|
|
@@ -181,16 +181,16 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
|
|
|
181
181
|
|
|
182
182
|
## Benchmarks
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
2,400+ LLM evaluations across 10 models, 3 providers, and 51 independent test runs.
|
|
185
185
|
|
|
186
186
|
| | GCF | TOON | JSON |
|
|
187
187
|
|---|---|---|---|
|
|
188
|
-
| **Comprehension** (23 runs, 10 models) | **
|
|
188
|
+
| **Comprehension** (23 runs, 10 models) | **91.2%** | 68.8% | 54.1% |
|
|
189
189
|
| **Generation** (28 runs, 9 models) | **5/5** | 1.0/5 | 5.0/5 |
|
|
190
190
|
| **Input tokens** (500 symbols) | **11,090** | 16,378 | 53,341 |
|
|
191
191
|
| **Output tokens** (100 symbols) | **5,976** | 8,937 | 16,121 |
|
|
192
192
|
|
|
193
|
-
GCF wins
|
|
193
|
+
GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://github.com/blackwell-systems/toon/tree/gcf-comparison). Full results: [gcformat.com/guide/benchmarks](https://gcformat.com/guide/benchmarks.html)
|
|
194
194
|
|
|
195
195
|
## Implementations
|
|
196
196
|
|
|
@@ -211,7 +211,11 @@ GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://git
|
|
|
211
211
|
|
|
212
212
|
**Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
213
213
|
|
|
214
|
-
**Specification:** [SPEC v3.
|
|
214
|
+
**Specification:** [SPEC v3.2 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 174 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.2.1+ (Go v1.3.1). Cross-language 6x6 matrix verified.
|
|
215
|
+
|
|
216
|
+
## Adopted by
|
|
217
|
+
|
|
218
|
+
[Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) (46K stars, Google Chrome DevTools team) · [Speakeasy](https://speakeasy.com) (API tooling, customers include Google, Verizon, Mistral AI, DocuSign, Vercel) · [OmniRoute](https://omniroute.online) (6.1K stars) · [NetClaw](https://github.com/automateyournetwork/netclaw) (556 stars) · [ctx](https://github.com/stevesolun/ctx) (510 stars) · [NeuroNest](https://neuronest.cc) · [Open Data Products SDK](https://opendataproducts.org/sdk/) (Linux Foundation) · [Raycast](https://raycast.com/blackwell-systems/json-to-gcf-converter) · [and more](https://gcformat.com/ecosystem/adopters.html)
|
|
215
219
|
|
|
216
220
|
## License
|
|
217
221
|
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gcf-python"
|
|
7
|
-
version = "2.2.
|
|
8
|
-
description = "
|
|
7
|
+
version = "2.2.2"
|
|
8
|
+
description = "The AI-native wire format for structured data. 50-92% fewer tokens than JSON. 100% comprehension on every frontier model. Zero dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -38,7 +38,7 @@ from .constants import KIND_ABBREV, KIND_EXPAND
|
|
|
38
38
|
from .decode import DecodeError, decode
|
|
39
39
|
from .delta import encode_delta
|
|
40
40
|
from .encode import encode
|
|
41
|
-
from .generic import encode_generic
|
|
41
|
+
from .generic import encode_generic, GenericOptions
|
|
42
42
|
from .session import Session, encode_with_session
|
|
43
43
|
from .decode_generic import decode_generic
|
|
44
44
|
from .stream import StreamEncoder
|
|
@@ -62,7 +62,8 @@ __all__ = [
|
|
|
62
62
|
"encode",
|
|
63
63
|
"encode_delta",
|
|
64
64
|
"encode_generic",
|
|
65
|
+
"GenericOptions",
|
|
65
66
|
"encode_with_session",
|
|
66
67
|
]
|
|
67
68
|
|
|
68
|
-
__version__ = "
|
|
69
|
+
__version__ = "2.2.2"
|
|
@@ -360,6 +360,14 @@ def _parse_attachment(
|
|
|
360
360
|
arr, consumed = _parse_array_from_header(lines, line_idx, depth, after_name)
|
|
361
361
|
return name, arr, consumed, None
|
|
362
362
|
|
|
363
|
+
# Scalar: =value (field names containing ">" excluded from tabular columns).
|
|
364
|
+
if after_name.startswith("="):
|
|
365
|
+
val_str = after_name[1:]
|
|
366
|
+
parsed = parse_scalar(val_str, tabular_context=True)
|
|
367
|
+
if parsed is MISSING:
|
|
368
|
+
return name, None, 1, None
|
|
369
|
+
return name, parsed, 1, None
|
|
370
|
+
|
|
363
371
|
raise ValueError(f"invalid attachment form: {after_name}")
|
|
364
372
|
|
|
365
373
|
|
|
@@ -423,7 +431,11 @@ def _parse_tabular_body(
|
|
|
423
431
|
path_column_map: dict[str, list[str]] = {}
|
|
424
432
|
for f in fields:
|
|
425
433
|
if ">" in f:
|
|
426
|
-
|
|
434
|
+
parts = f.split(">")
|
|
435
|
+
# Only treat as a path column if all segments are non-empty.
|
|
436
|
+
# A literal key like ">" would split into ["", ""].
|
|
437
|
+
if all(p for p in parts):
|
|
438
|
+
path_column_map[f] = parts
|
|
427
439
|
|
|
428
440
|
# Track inline schemas and shared array schemas.
|
|
429
441
|
inline_schemas: dict[str, list[str]] = {}
|
|
@@ -506,10 +518,10 @@ def _parse_tabular_body(
|
|
|
506
518
|
all_att_fields = traditional_att_fields + inline_att_fields
|
|
507
519
|
attachment_values: dict[str, Any] = {}
|
|
508
520
|
|
|
509
|
-
if row_has_id
|
|
521
|
+
if row_has_id:
|
|
510
522
|
inline_idx = 0
|
|
511
523
|
|
|
512
|
-
while i < len(lines)
|
|
524
|
+
while i < len(lines):
|
|
513
525
|
a_line = lines[i]
|
|
514
526
|
a_content: str | None = None
|
|
515
527
|
if depth == 0 or a_line.startswith(ind):
|
|
@@ -601,13 +613,6 @@ def _parse_tabular_body(
|
|
|
601
613
|
if extra_name in attachment_values:
|
|
602
614
|
raise ValueError(f"duplicate_attachment: {extra_name}")
|
|
603
615
|
|
|
604
|
-
if not row_has_id or not all_att_fields:
|
|
605
|
-
att_indent = ind + " "
|
|
606
|
-
if i < len(lines) and lines[i].startswith(att_indent):
|
|
607
|
-
peek = lines[i][len(att_indent):]
|
|
608
|
-
if peek.startswith("."):
|
|
609
|
-
raise ValueError(f"orphan_attachment: {peek}")
|
|
610
|
-
|
|
611
616
|
row: dict[str, Any] = {}
|
|
612
617
|
for f in fields:
|
|
613
618
|
if f in missing_fields:
|
|
@@ -616,6 +621,10 @@ def _parse_tabular_body(
|
|
|
616
621
|
row[f] = cell_values[f]
|
|
617
622
|
elif f in attachment_values:
|
|
618
623
|
row[f] = attachment_values[f]
|
|
624
|
+
# Also add any orphan attachment values (fields excluded from column list, e.g. ">" fields).
|
|
625
|
+
for k, v in attachment_values.items():
|
|
626
|
+
if k not in row:
|
|
627
|
+
row[k] = v
|
|
619
628
|
# Unflatten path columns into nested objects.
|
|
620
629
|
if path_column_map:
|
|
621
630
|
nested = _unflatten_paths(path_column_map, flat_values, flat_absent)
|
|
@@ -2,42 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from .scalar import format_scalar, format_key
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
@dataclass
|
|
12
|
+
class GenericOptions:
|
|
13
|
+
"""Options for controlling generic encoding behavior."""
|
|
14
|
+
no_flatten: bool = False
|
|
15
|
+
"""When True, disables promotion of fixed-shape nested objects to path
|
|
16
|
+
columns (e.g. "customer>name"). Nested objects use attachment syntax
|
|
17
|
+
instead. Open-weight models currently comprehend the expanded form
|
|
18
|
+
better; this gap is expected to close."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def encode_generic(data: Any, opts: GenericOptions | None = None) -> str:
|
|
22
|
+
if opts is None:
|
|
23
|
+
opts = GenericOptions()
|
|
11
24
|
out: list[str] = ["GCF profile=generic"]
|
|
12
|
-
_encode_root_value(data, out)
|
|
25
|
+
_encode_root_value(data, out, opts)
|
|
13
26
|
return "\n".join(out) + "\n"
|
|
14
27
|
|
|
15
28
|
|
|
16
|
-
def _encode_root_value(v: Any, out: list[str]) -> None:
|
|
29
|
+
def _encode_root_value(v: Any, out: list[str], opts: GenericOptions) -> None:
|
|
17
30
|
if v is None:
|
|
18
31
|
out.append("=-")
|
|
19
32
|
elif isinstance(v, dict):
|
|
20
|
-
_encode_object(v, out, 0)
|
|
33
|
+
_encode_object(v, out, 0, opts)
|
|
21
34
|
elif isinstance(v, list):
|
|
22
|
-
_encode_root_array(v, out)
|
|
35
|
+
_encode_root_array(v, out, opts)
|
|
23
36
|
else:
|
|
24
37
|
out.append(f"={format_scalar(v)}")
|
|
25
38
|
|
|
26
39
|
|
|
27
|
-
def _encode_object(d: dict, out: list[str], depth: int) -> None:
|
|
40
|
+
def _encode_object(d: dict, out: list[str], depth: int, opts: GenericOptions) -> None:
|
|
28
41
|
prefix = _indent(depth)
|
|
29
42
|
for key, value in d.items():
|
|
30
43
|
fk = format_key(key)
|
|
31
44
|
if isinstance(value, dict):
|
|
32
45
|
out.append(f"{prefix}## {fk}")
|
|
33
|
-
_encode_object(value, out, depth + 1)
|
|
46
|
+
_encode_object(value, out, depth + 1, opts)
|
|
34
47
|
elif isinstance(value, list):
|
|
35
|
-
_encode_named_array(fk, value, out, depth)
|
|
48
|
+
_encode_named_array(fk, value, out, depth, opts)
|
|
36
49
|
else:
|
|
37
50
|
out.append(f"{prefix}{fk}={format_scalar(value)}")
|
|
38
51
|
|
|
39
52
|
|
|
40
|
-
def _encode_root_array(arr: list, out: list[str]) -> None:
|
|
53
|
+
def _encode_root_array(arr: list, out: list[str], opts: GenericOptions) -> None:
|
|
41
54
|
if not arr:
|
|
42
55
|
out.append("## [0]")
|
|
43
56
|
return
|
|
@@ -47,12 +60,12 @@ def _encode_root_array(arr: list, out: list[str]) -> None:
|
|
|
47
60
|
return
|
|
48
61
|
fields = _tabular_fields(arr)
|
|
49
62
|
if fields is not None:
|
|
50
|
-
_encode_tabular("## ", arr, fields, out, 0)
|
|
63
|
+
_encode_tabular("## ", arr, fields, out, 0, opts)
|
|
51
64
|
return
|
|
52
|
-
_encode_expanded("## ", arr, out, 0)
|
|
65
|
+
_encode_expanded("## ", arr, out, 0, opts)
|
|
53
66
|
|
|
54
67
|
|
|
55
|
-
def _encode_named_array(name: str, arr: list, out: list[str], depth: int) -> None:
|
|
68
|
+
def _encode_named_array(name: str, arr: list, out: list[str], depth: int, opts: GenericOptions) -> None:
|
|
56
69
|
prefix = _indent(depth)
|
|
57
70
|
if not arr:
|
|
58
71
|
out.append(f"{prefix}## {name} [0]")
|
|
@@ -63,9 +76,9 @@ def _encode_named_array(name: str, arr: list, out: list[str], depth: int) -> Non
|
|
|
63
76
|
return
|
|
64
77
|
fields = _tabular_fields(arr)
|
|
65
78
|
if fields is not None:
|
|
66
|
-
_encode_tabular(f"{prefix}## {name} ", arr, fields, out, depth)
|
|
79
|
+
_encode_tabular(f"{prefix}## {name} ", arr, fields, out, depth, opts)
|
|
67
80
|
return
|
|
68
|
-
_encode_expanded(f"{prefix}## {name} ", arr, out, depth)
|
|
81
|
+
_encode_expanded(f"{prefix}## {name} ", arr, out, depth, opts)
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
def _tabular_fields(arr: list) -> list[str] | None:
|
|
@@ -152,10 +165,21 @@ def _analyze_flattenable(
|
|
|
152
165
|
arr: list[dict], field_name: str, parent_path: str
|
|
153
166
|
) -> list[dict] | None:
|
|
154
167
|
"""Analyze whether a field can be flattened. Returns list of leaf descriptors or None."""
|
|
168
|
+
# Field names containing ">" cannot be flattened (would create ambiguous paths).
|
|
169
|
+
if ">" in field_name:
|
|
170
|
+
return None
|
|
155
171
|
canonical_shape: dict[str, str] | None = None # key -> "scalar" | "nested"
|
|
156
172
|
|
|
157
173
|
for item in arr:
|
|
158
|
-
if field_name not in item
|
|
174
|
+
if field_name not in item:
|
|
175
|
+
continue
|
|
176
|
+
# A nested (non-top-level) null cannot be flattened losslessly: its leaves
|
|
177
|
+
# would encode as absent ("~") and unflatten back to a missing key, not None.
|
|
178
|
+
# Bail to the attachment path. A top-level None is fine (emits "-" and
|
|
179
|
+
# reconstructs via the all-null rule), so just skip the row from shape analysis.
|
|
180
|
+
if item[field_name] is None:
|
|
181
|
+
if parent_path != "":
|
|
182
|
+
return None
|
|
159
183
|
continue
|
|
160
184
|
v = item[field_name]
|
|
161
185
|
if not isinstance(v, dict):
|
|
@@ -249,26 +273,39 @@ def _resolve_key_chain(item: Any, keys: list[str]) -> tuple[Any, bool]:
|
|
|
249
273
|
|
|
250
274
|
|
|
251
275
|
def _encode_tabular(
|
|
252
|
-
header_prefix: str, arr: list[dict], fields: list[str], out: list[str], depth: int
|
|
276
|
+
header_prefix: str, arr: list[dict], fields: list[str], out: list[str], depth: int, opts: GenericOptions
|
|
253
277
|
) -> None:
|
|
254
278
|
prefix = _indent(depth)
|
|
255
279
|
|
|
256
280
|
# Phase 0: Analyze fields for flattening.
|
|
257
281
|
flatten_map: dict[str, list[dict]] = {}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
if not opts.no_flatten:
|
|
283
|
+
for f in fields:
|
|
284
|
+
leaves = _analyze_flattenable(arr, f, "")
|
|
285
|
+
if leaves and len(leaves) > 0:
|
|
286
|
+
flatten_map[f] = leaves
|
|
287
|
+
|
|
288
|
+
# Fields whose names contain ">" must not appear as tabular columns
|
|
289
|
+
# because the decoder would interpret them as flattened path columns.
|
|
290
|
+
# Track them for per-row attachment emission (spec rule 7.4.6.1.4).
|
|
291
|
+
gt_fields = {f for f in fields if f not in flatten_map and ">" in f}
|
|
262
292
|
|
|
263
293
|
# Build expanded column list.
|
|
264
294
|
columns: list[dict] = []
|
|
265
295
|
for f in fields:
|
|
296
|
+
if f in gt_fields:
|
|
297
|
+
continue
|
|
266
298
|
if f in flatten_map:
|
|
267
299
|
for leaf in flatten_map[f]:
|
|
268
300
|
columns.append({"header": format_key(leaf["path"]), "type": "flat", "field": f, "keys": leaf["keys"]})
|
|
269
301
|
else:
|
|
270
302
|
columns.append({"header": format_key(f), "type": "original", "field": f, "keys": []})
|
|
271
303
|
|
|
304
|
+
# If all fields were excluded (all contain ">"), fall back to expanded.
|
|
305
|
+
if not columns:
|
|
306
|
+
_encode_expanded(header_prefix, arr, out, depth, opts)
|
|
307
|
+
return
|
|
308
|
+
|
|
272
309
|
# Pre-compute inline schemas and shared array schemas (skip flattened fields).
|
|
273
310
|
inline_schemas: dict[str, list[str]] = {}
|
|
274
311
|
shared_arr_schemas: dict[str, list[str]] = {}
|
|
@@ -333,6 +370,15 @@ def _encode_tabular(
|
|
|
333
370
|
else:
|
|
334
371
|
cells.append(format_scalar(v, "|"))
|
|
335
372
|
|
|
373
|
+
# Emit fields with ">" in their names as per-row attachments.
|
|
374
|
+
for f in fields:
|
|
375
|
+
if f not in gt_fields:
|
|
376
|
+
continue
|
|
377
|
+
if f not in item:
|
|
378
|
+
continue
|
|
379
|
+
row_has_attachment = True
|
|
380
|
+
attachments.append((f, item[f], False, None))
|
|
381
|
+
|
|
336
382
|
row = "|".join(cells)
|
|
337
383
|
if row_has_attachment:
|
|
338
384
|
out.append(f"{prefix}@{i} {row}")
|
|
@@ -351,17 +397,25 @@ def _encode_tabular(
|
|
|
351
397
|
elif isinstance(att_val, list):
|
|
352
398
|
sas = shared_arr_schemas.get(att_name)
|
|
353
399
|
if sas and i > 0:
|
|
354
|
-
_encode_attachment_array_shared(prefix, fk, att_val, out, depth + 2, sas)
|
|
400
|
+
_encode_attachment_array_shared(prefix, fk, att_val, out, depth + 2, sas, opts)
|
|
355
401
|
else:
|
|
356
|
-
_encode_attachment_array(prefix, fk, att_val, out, depth + 2)
|
|
402
|
+
_encode_attachment_array(prefix, fk, att_val, out, depth + 2, opts)
|
|
357
403
|
elif isinstance(att_val, dict):
|
|
358
404
|
out.append(f"{prefix}.{fk} {{}}")
|
|
359
|
-
_encode_object(att_val, out, depth + 2)
|
|
405
|
+
_encode_object(att_val, out, depth + 2, opts)
|
|
406
|
+
else:
|
|
407
|
+
# Scalar attachment (e.g. field names containing ">").
|
|
408
|
+
if att_val is None:
|
|
409
|
+
out.append(f"{prefix}.{fk} =-")
|
|
410
|
+
else:
|
|
411
|
+
out.append(f"{prefix}.{fk} ={format_scalar(att_val)}")
|
|
360
412
|
|
|
361
413
|
|
|
362
414
|
def _encode_attachment_array(
|
|
363
|
-
att_prefix: str, fk: str, arr: list, out: list[str], depth: int
|
|
415
|
+
att_prefix: str, fk: str, arr: list, out: list[str], depth: int, opts: GenericOptions | None = None
|
|
364
416
|
) -> None:
|
|
417
|
+
if opts is None:
|
|
418
|
+
opts = GenericOptions()
|
|
365
419
|
if not arr:
|
|
366
420
|
out.append(f"{att_prefix}.{fk} [0]")
|
|
367
421
|
elif _all_primitives(arr):
|
|
@@ -370,13 +424,13 @@ def _encode_attachment_array(
|
|
|
370
424
|
else:
|
|
371
425
|
fields = _tabular_fields(arr)
|
|
372
426
|
if fields is not None:
|
|
373
|
-
_encode_tabular(f"{att_prefix}.{fk} ", arr, fields, out, depth)
|
|
427
|
+
_encode_tabular(f"{att_prefix}.{fk} ", arr, fields, out, depth, opts)
|
|
374
428
|
else:
|
|
375
|
-
_encode_expanded(f"{att_prefix}.{fk} ", arr, out, depth)
|
|
429
|
+
_encode_expanded(f"{att_prefix}.{fk} ", arr, out, depth, opts)
|
|
376
430
|
|
|
377
431
|
|
|
378
432
|
def _encode_attachment_array_shared(
|
|
379
|
-
att_prefix: str, fk: str, arr: list, out: list[str], depth: int, shared_fields: list[str]
|
|
433
|
+
att_prefix: str, fk: str, arr: list, out: list[str], depth: int, shared_fields: list[str], opts: GenericOptions | None = None
|
|
380
434
|
) -> None:
|
|
381
435
|
if not arr:
|
|
382
436
|
out.append(f"{att_prefix}.{fk} [0]")
|
|
@@ -403,25 +457,29 @@ def _encode_attachment_array_shared(
|
|
|
403
457
|
out.append(f"{prefix}{'|'.join(cells)}")
|
|
404
458
|
else:
|
|
405
459
|
# Fields don't match: fall back to full encoding.
|
|
406
|
-
_encode_attachment_array(att_prefix, fk, arr, out, depth)
|
|
460
|
+
_encode_attachment_array(att_prefix, fk, arr, out, depth, opts)
|
|
407
461
|
|
|
408
462
|
|
|
409
|
-
def _encode_expanded(header_prefix: str, arr: list, out: list[str], depth: int) -> None:
|
|
463
|
+
def _encode_expanded(header_prefix: str, arr: list, out: list[str], depth: int, opts: GenericOptions | None = None) -> None:
|
|
464
|
+
if opts is None:
|
|
465
|
+
opts = GenericOptions()
|
|
410
466
|
prefix = _indent(depth)
|
|
411
467
|
out.append(f"{header_prefix}[{len(arr)}]")
|
|
412
468
|
for i, item in enumerate(arr):
|
|
413
469
|
if isinstance(item, dict):
|
|
414
470
|
out.append(f"{prefix}@{i} {{}}")
|
|
415
|
-
_encode_object(item, out, depth + 1)
|
|
471
|
+
_encode_object(item, out, depth + 1, opts)
|
|
416
472
|
elif isinstance(item, list):
|
|
417
|
-
_encode_expanded_array_item(prefix, i, item, out, depth)
|
|
473
|
+
_encode_expanded_array_item(prefix, i, item, out, depth, opts)
|
|
418
474
|
else:
|
|
419
475
|
out.append(f"{prefix}@{i} ={format_scalar(item)}")
|
|
420
476
|
|
|
421
477
|
|
|
422
478
|
def _encode_expanded_array_item(
|
|
423
|
-
prefix: str, idx: int, arr: list, out: list[str], depth: int
|
|
479
|
+
prefix: str, idx: int, arr: list, out: list[str], depth: int, opts: GenericOptions | None = None
|
|
424
480
|
) -> None:
|
|
481
|
+
if opts is None:
|
|
482
|
+
opts = GenericOptions()
|
|
425
483
|
if not arr:
|
|
426
484
|
out.append(f"{prefix}@{idx} [0]")
|
|
427
485
|
elif _all_primitives(arr):
|
|
@@ -430,9 +488,9 @@ def _encode_expanded_array_item(
|
|
|
430
488
|
else:
|
|
431
489
|
fields = _tabular_fields(arr)
|
|
432
490
|
if fields is not None:
|
|
433
|
-
_encode_tabular(f"{prefix}@{idx} ", arr, fields, out, depth + 1)
|
|
491
|
+
_encode_tabular(f"{prefix}@{idx} ", arr, fields, out, depth + 1, opts)
|
|
434
492
|
else:
|
|
435
|
-
_encode_expanded(f"{prefix}@{idx} ", arr, out, depth + 1)
|
|
493
|
+
_encode_expanded(f"{prefix}@{idx} ", arr, out, depth + 1, opts)
|
|
436
494
|
|
|
437
495
|
|
|
438
496
|
def _all_primitives(arr: list) -> bool:
|
|
@@ -98,9 +98,9 @@ def test_conformance(rel_path, data):
|
|
|
98
98
|
), f"decode mismatch:\n got: {got}\n exp: {data['expected']}"
|
|
99
99
|
|
|
100
100
|
elif op == "roundtrip":
|
|
101
|
-
# Encode, verify GCF output matches expected, then decode and verify round-trip.
|
|
101
|
+
# Encode, verify GCF output matches expected (if provided), then decode and verify round-trip.
|
|
102
102
|
got = encode_generic(data["input"])
|
|
103
|
-
if isinstance(data["expected"], str):
|
|
103
|
+
if "expected" in data and isinstance(data["expected"], str):
|
|
104
104
|
assert got == data["expected"], f"encode mismatch:\n got: {got!r}\n exp: {data['expected']!r}"
|
|
105
105
|
decoded = decode_generic(got)
|
|
106
106
|
assert _structural_equal(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tests for GCF generic encoding."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import pytest
|
|
4
|
+
from gcf import encode_generic, decode_generic, GenericOptions
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def test_encode_flat_tabular_list():
|
|
@@ -164,3 +165,52 @@ def test_encode_string_with_pipe():
|
|
|
164
165
|
data = {"val": "a|b"}
|
|
165
166
|
output = encode_generic(data)
|
|
166
167
|
assert 'val="a|b"' in output
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_no_flatten_option():
|
|
171
|
+
"""noFlatten produces attachment syntax instead of path columns."""
|
|
172
|
+
data = {
|
|
173
|
+
"orders": [
|
|
174
|
+
{"id": "ORD-1", "customer": {"name": "Alice", "email": "alice@co.com"}, "total": 99.99},
|
|
175
|
+
{"id": "ORD-2", "customer": {"name": "Bob", "email": "bob@co.com"}, "total": 49.99},
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
with_flatten = encode_generic(data)
|
|
179
|
+
assert "customer>" in with_flatten
|
|
180
|
+
|
|
181
|
+
no_flatten = encode_generic(data, GenericOptions(no_flatten=True))
|
|
182
|
+
assert "customer>" not in no_flatten
|
|
183
|
+
assert ".customer" in no_flatten
|
|
184
|
+
|
|
185
|
+
# Both round-trip.
|
|
186
|
+
assert decode_generic(with_flatten) == data
|
|
187
|
+
assert decode_generic(no_flatten) == data
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
GT_EDGE_CASES = [
|
|
191
|
+
("literal > key", [{">": 1}, {">": 2}]),
|
|
192
|
+
("> at start", [{">foo": "a", "id": 1}, {">foo": "b", "id": 2}]),
|
|
193
|
+
("> at end", [{"foo>": "a", "id": 1}, {"foo>": "b", "id": 2}]),
|
|
194
|
+
("double >>", [{"a>>b": "x"}, {"a>>b": "y"}]),
|
|
195
|
+
("multiple > in key", [{"a>b>c": "x"}, {"a>b>c": "y"}]),
|
|
196
|
+
("> field with null", [{"a>b": None, "id": 1}, {"a>b": "hello", "id": 2}]),
|
|
197
|
+
("> field with object", [{"a>b": {"x": 1}, "id": 1}, {"a>b": {"x": 2}, "id": 2}]),
|
|
198
|
+
("> field with array", [{"a>b": [1, 2], "id": 1}, {"a>b": [3], "id": 2}]),
|
|
199
|
+
("all fields have >", [{">": 1, "a>b": 2}, {">": 3, "a>b": 4}]),
|
|
200
|
+
("mix of > literal and flattened", [
|
|
201
|
+
{"id": 1, "x>y": "lit", "nested": {"a": "v1", "b": "v2"}},
|
|
202
|
+
{"id": 2, "x>y": "lit2", "nested": {"a": "v3", "b": "v4"}},
|
|
203
|
+
]),
|
|
204
|
+
("> field absent in some rows", [{"id": 1, "a>b": "present"}, {"id": 2}]),
|
|
205
|
+
("key looks like flattened path", [{"id": 1, "customer>name": "Alice"}, {"id": 2, "customer>name": "Bob"}]),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@pytest.mark.parametrize("name,data", GT_EDGE_CASES)
|
|
210
|
+
def test_gt_field_edge_cases(name, data):
|
|
211
|
+
"""Field names containing > round-trip correctly in both flatten modes."""
|
|
212
|
+
for no_flatten in (False, True):
|
|
213
|
+
opts = GenericOptions(no_flatten=no_flatten)
|
|
214
|
+
encoded = encode_generic(data, opts)
|
|
215
|
+
decoded = decode_generic(encoded)
|
|
216
|
+
assert decoded == data, f"{name} (no_flatten={no_flatten}): round-trip mismatch\n gcf: {encoded!r}"
|
|
@@ -7,7 +7,7 @@ import string
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from gcf import encode_generic, decode_generic
|
|
10
|
+
from gcf import encode_generic, decode_generic, GenericOptions
|
|
11
11
|
|
|
12
12
|
ITERATIONS = int(os.environ.get("GCF_ITERATIONS", "100000"))
|
|
13
13
|
|
|
@@ -55,7 +55,7 @@ def _gen_number(r):
|
|
|
55
55
|
])()
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
SPECIAL = ' |,="\\#@\n\t
|
|
58
|
+
SPECIAL = ' |,="\\#@\n\t~^+-.>'
|
|
59
59
|
BARE = string.ascii_letters + string.digits
|
|
60
60
|
|
|
61
61
|
|
|
@@ -214,33 +214,109 @@ def _structural_equal(a, b):
|
|
|
214
214
|
return a == b
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
def _gen_flat_shape(r, depth, max_depth):
|
|
218
|
+
"""A fixed nested schema: the string "scalar" or a dict of named sub-shapes."""
|
|
219
|
+
if depth >= max_depth or r.random() < 0.45:
|
|
220
|
+
return "scalar"
|
|
221
|
+
shape = {}
|
|
222
|
+
for _ in range(1 + r.randint(0, 2)):
|
|
223
|
+
shape[_gen_bare_key(r)] = _gen_flat_shape(r, depth + 1, max_depth)
|
|
224
|
+
return shape if shape else "scalar"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _materialize_flat_shape(r, shape):
|
|
228
|
+
if shape == "scalar":
|
|
229
|
+
return _gen_scalar(r)
|
|
230
|
+
obj = {}
|
|
231
|
+
for k, s in shape.items():
|
|
232
|
+
# A nested sub-object is sometimes None (intermediate null — the case the
|
|
233
|
+
# pre-fix encoder dropped) instead of a full object.
|
|
234
|
+
obj[k] = None if s != "scalar" and r.random() < 0.3 else _materialize_flat_shape(r, s)
|
|
235
|
+
return obj
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _gen_flattenable_array(r):
|
|
239
|
+
schema = {"id": "scalar"}
|
|
240
|
+
order = ["id"]
|
|
241
|
+
has_nested = False
|
|
242
|
+
for _ in range(1 + r.randint(0, 2)):
|
|
243
|
+
k = _gen_bare_key(r)
|
|
244
|
+
if k in schema:
|
|
245
|
+
continue
|
|
246
|
+
s = _gen_flat_shape(r, 1, 3)
|
|
247
|
+
schema[k] = s
|
|
248
|
+
order.append(k)
|
|
249
|
+
if s != "scalar":
|
|
250
|
+
has_nested = True
|
|
251
|
+
if not has_nested:
|
|
252
|
+
k = _gen_bare_key(r)
|
|
253
|
+
schema[k] = {_gen_bare_key(r): {_gen_bare_key(r): "scalar"}}
|
|
254
|
+
order.append(k)
|
|
255
|
+
arr = []
|
|
256
|
+
for _ in range(2 + r.randint(0, 5)):
|
|
257
|
+
row = {}
|
|
258
|
+
for f in order:
|
|
259
|
+
x = r.random()
|
|
260
|
+
if x < 0.12:
|
|
261
|
+
continue # field absent this row
|
|
262
|
+
elif x < 0.24:
|
|
263
|
+
row[f] = None # field present-null (top-level null)
|
|
264
|
+
else:
|
|
265
|
+
row[f] = _materialize_flat_shape(r, schema[f])
|
|
266
|
+
arr.append(row)
|
|
267
|
+
return arr
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_flatten_roundtrip():
|
|
271
|
+
"""Aligned arrays whose shared fields are fixed-shape nested objects with a
|
|
272
|
+
field or an intermediate nested level sometimes null/absent — the v3.2 flatten
|
|
273
|
+
path the scalar-only generator never produces, so flatten/unflatten and its
|
|
274
|
+
null-at-depth losslessness edge would otherwise be unexercised."""
|
|
275
|
+
r = _rng(7)
|
|
276
|
+
for i in range(ITERATIONS):
|
|
277
|
+
val = _gen_flattenable_array(r)
|
|
278
|
+
for no_flatten in (False, True):
|
|
279
|
+
gcf = encode_generic(val, GenericOptions(no_flatten=no_flatten))
|
|
280
|
+
decoded = decode_generic(gcf)
|
|
281
|
+
a = _json_norm(val)
|
|
282
|
+
b = _json_norm(decoded)
|
|
283
|
+
assert _structural_equal(a, b), (
|
|
284
|
+
f"iteration {i} no_flatten={no_flatten}: round-trip mismatch\n"
|
|
285
|
+
f" input: {json.dumps(val)}\n"
|
|
286
|
+
f" decoded: {json.dumps(decoded)}\n"
|
|
287
|
+
f" gcf: {gcf!r}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
217
291
|
def test_random_roundtrip():
|
|
218
292
|
r = _rng(42)
|
|
219
293
|
for i in range(ITERATIONS):
|
|
220
294
|
val = _gen_value(r, 0, 4)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
295
|
+
for no_flatten in (False, True):
|
|
296
|
+
gcf = encode_generic(val, GenericOptions(no_flatten=no_flatten))
|
|
297
|
+
decoded = decode_generic(gcf)
|
|
298
|
+
a = _json_norm(val)
|
|
299
|
+
b = _json_norm(decoded)
|
|
300
|
+
assert _structural_equal(a, b), (
|
|
301
|
+
f"iteration {i} no_flatten={no_flatten}: round-trip mismatch\n"
|
|
302
|
+
f" input: {json.dumps(val)}\n"
|
|
303
|
+
f" decoded: {json.dumps(decoded)}\n"
|
|
304
|
+
f" gcf: {gcf!r}"
|
|
305
|
+
)
|
|
231
306
|
|
|
232
307
|
|
|
233
308
|
def test_adversarial_roundtrip():
|
|
234
309
|
r = _rng(99)
|
|
235
310
|
for i in range(ITERATIONS):
|
|
236
311
|
val = _gen_adversarial_value(r, 0, 3)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
312
|
+
for no_flatten in (False, True):
|
|
313
|
+
gcf = encode_generic(val, GenericOptions(no_flatten=no_flatten))
|
|
314
|
+
decoded = decode_generic(gcf)
|
|
315
|
+
a = _json_norm(val)
|
|
316
|
+
b = _json_norm(decoded)
|
|
317
|
+
assert _structural_equal(a, b), (
|
|
318
|
+
f"iteration {i} no_flatten={no_flatten}: round-trip mismatch\n"
|
|
319
|
+
f" input: {json.dumps(val)}\n"
|
|
320
|
+
f" decoded: {json.dumps(decoded)}\n"
|
|
321
|
+
f" gcf: {gcf!r}"
|
|
322
|
+
)
|
gcf_python-2.2.2/uv.lock
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|