From 80b7b3c10015f06516be77e9bff0f81281345a0c Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 01/14] Add yaml-ld git submodule entry in .gitmodules --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitmodules b/.gitmodules index 87e7ce95..824bc216 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "specifications/rdf-canon"] path = specifications/rdf-canon url = https://github.com/w3c/rdf-canon.git +[submodule "specifications/yaml-ld"] + path = specifications/yaml-ld + url = https://github.com/w3c/yaml-ld.git From fadbdf4273d7d232cbd86190a014b3a3a5d3d5f7 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 02/14] Add w3c/yaml-ld specification submodule --- specifications/yaml-ld | 1 + 1 file changed, 1 insertion(+) create mode 160000 specifications/yaml-ld diff --git a/specifications/yaml-ld b/specifications/yaml-ld new file mode 160000 index 00000000..af4c9c8e --- /dev/null +++ b/specifications/yaml-ld @@ -0,0 +1 @@ +Subproject commit af4c9c8eb77454645a0b16e69ec16b28a322ff06 From 991f55e2fdb9cd8c6344ceca60f0d8e38c947756 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 03/14] Wire YAML-LD suite into test harness with baseline skips --- tests/runtests.py | 71 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/tests/runtests.py b/tests/runtests.py index 10b1ea18..1f195c43 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -91,14 +91,16 @@ 'https://w3c.github.io/json-ld-api/tests', 'https://w3c.github.io/json-ld-framing/tests', 'https://github.com/json-ld/normalization/tests', - 'https://w3c.github.io/rdf-canon/tests/vocab#' + 'https://w3c.github.io/rdf-canon/tests/vocab#', + 'https://w3c.github.io/yaml-ld/tests', ] SPEC_DIRS = [ '../specifications/json-ld-api/tests/', '../specifications/json-ld-framing/tests/', '../specifications/normalization/tests/', - '../specifications/rdf-canon/tests/' + '../specifications/rdf-canon/tests/', + '../specifications/yaml-ld/tests/', ] # NOTE: The following TestRunner class can be removed because pytest now @@ -751,6 +753,8 @@ def load_locally(url): url_no_frag = strip_fragment(url) if not content_type and url_no_frag.endswith('.jsonld'): content_type = 'application/ld+json' + if not content_type and url_no_frag.endswith('.yamlld'): + content_type = 'application/ld+yaml' if not content_type and url_no_frag.endswith('.json'): content_type = 'application/json' if not content_type and url_no_frag.endswith('.html'): @@ -953,6 +957,11 @@ def write(self, filename): # skip tests where behavior changed for a 1.1 processor # see JSON-LD 1.0 Errata 'specVersion': ['json-ld-1.0'], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#compact-local-json-ld-context$', + '.*yaml-ld/tests/manifest#compact-local-yaml-ld-context$', + ], }, 'fn': 'compact', 'params': [ @@ -984,6 +993,50 @@ def write(self, filename): # skip tests where behavior changed for a 1.1 processor # see JSON-LD 1.0 Errata 'specVersion': ['json-ld-1.0'], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#cir-mapping-key-1-negative$', + '.*yaml-ld/tests/manifest#cir-mapping-key-2-negative$', + '.*yaml-ld/tests/manifest#cir-mapping-key-3-negative$', + '.*yaml-ld/tests/manifest#cir-mapping-key-4-negative$', + '.*yaml-ld/tests/manifest#cir-mapping-key-5-negative$', + '.*yaml-ld/tests/manifest#cir-scalar-core-1-positive$', + '.*yaml-ld/tests/manifest#cir-scalar-i18n-1-positive$', + '.*yaml-ld/tests/manifest#cir-scalar-other-1-positive$', + '.*yaml-ld/tests/manifest#cr-comments-1-positive$', + '.*yaml-ld/tests/manifest#cr-utf8-1-positive$', + '.*yaml-ld/tests/manifest#cr-utf8-2-negative$', + '.*yaml-ld/tests/manifest#cr-well-formed-1-positive$', + '.*yaml-ld/tests/manifest#aa-cycles-1-positive$', + '.*yaml-ld/tests/manifest#aa-cycles-3-positive$', + '.*yaml-ld/tests/manifest#html-and-yaml-streams$', + '.*yaml-ld/tests/manifest#mixed-script-types$', + '.*yaml-ld/tests/manifest#html-dedent-needed$', + '.*yaml-ld/tests/manifest#html-dedent-not-needed$', + '.*yaml-ld/tests/manifest#two-documents-from-stream$', + '.*yaml-ld/tests/manifest#one-document-from-stream$', + '.*yaml-ld/tests/manifest#local-json-ld-context$', + '.*yaml-ld/tests/manifest#local-yaml-ld-context$', + '.*yaml-ld/tests/manifest#core-bool-true$', + '.*yaml-ld/tests/manifest#core-bool-false$', + '.*yaml-ld/tests/manifest#core-bool-true-title$', + '.*yaml-ld/tests/manifest#core-bool-false-upper$', + '.*yaml-ld/tests/manifest#core-null-null$', + '.*yaml-ld/tests/manifest#core-null-tilde$', + '.*yaml-ld/tests/manifest#core-null-upper$', + '.*yaml-ld/tests/manifest#core-int-decimal$', + '.*yaml-ld/tests/manifest#core-int-octal$', + '.*yaml-ld/tests/manifest#core-int-hex$', + '.*yaml-ld/tests/manifest#core-float-basic$', + '.*yaml-ld/tests/manifest#core-float-scientific$', + '.*yaml-ld/tests/manifest#core-date-ymd$', + '.*yaml-ld/tests/manifest#core-yaml11-yes$', + '.*yaml-ld/tests/manifest#core-yaml11-no$', + '.*yaml-ld/tests/manifest#core-yaml11-on$', + '.*yaml-ld/tests/manifest#core-yaml11-off$', + '.*yaml-ld/tests/manifest#core-yaml11-yes-title$', + '.*yaml-ld/tests/manifest#core-yaml11-yes-upper$', + ], }, 'fn': 'expand', 'params': [read_test_url('input'), create_test_options()], @@ -994,6 +1047,10 @@ def write(self, filename): # skip tests where behavior changed for a 1.1 processor # see JSON-LD 1.0 Errata 'specVersion': ['json-ld-1.0'], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#flatten$', + ], }, 'fn': 'flatten', 'params': [ @@ -1008,6 +1065,10 @@ def write(self, filename): # skip tests where behavior changed for a 1.1 processor # see JSON-LD 1.0 Errata 'specVersion': ['json-ld-1.0'], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#frame-t0001$', + ], }, 'fn': 'frame', 'params': [ @@ -1040,6 +1101,12 @@ def write(self, filename): # skip tests where behavior changed for a 1.1 processor # see JSON-LD 1.0 Errata 'specVersion': ['json-ld-1.0'], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#aa-information-1-positive$', + '.*yaml-ld/tests/manifest#cir-scalar-core-2-positive$', + '.*yaml-ld/tests/manifest#cir-scalar-other-2-positive$', + ], }, 'fn': 'to_rdf', 'params': [ From f183fbe1e0d40d576c1ce591003bb3d4bedf4307 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 04/14] Run YAML-LD suite in CI --- .github/workflows/main.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5d06dc51..79dacb3e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -70,6 +70,7 @@ jobs: pytest tests/test_manifests.py --tests=./specifications/json-ld-framing/tests --loader=${{ matrix.loader }} pytest tests/test_manifests.py --tests=./specifications/normalization/tests --loader=${{ matrix.loader }} pytest tests/test_manifests.py --tests=./specifications/rdf-canon/tests --loader=${{ matrix.loader }} + pytest tests/test_manifests.py --tests=./specifications/yaml-ld/tests --loader=${{ matrix.loader }} pytest --ignore ./tests/test_manifests.py env: LOADER: ${{ matrix.loader }} @@ -106,4 +107,4 @@ jobs: uses: MishaKav/pytest-coverage-comment@ae0e8a539a3f310aefb3bfb6a2209778a21fa42b with: pytest-coverage-path: ./pytest-coverage.txt - junitxml-path: ./pytest.xml \ No newline at end of file + junitxml-path: ./pytest.xml From 883750fe5b168e4580788176e740457cb887bcc5 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 05/14] Document cloning the yaml-ld specification suite --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fca45e7f..805f2f59 100644 --- a/README.md +++ b/README.md @@ -388,13 +388,14 @@ git submodule update #### Cloning manually You can also avoid using git submodules by manually cloning the `json-ld-api`, -`json-ld-framing`, and `normalization` repositories hosted on GitHub using the -following commands: +`json-ld-framing`, `normalization`, and `yaml-ld` repositories hosted on GitHub +using the following commands: ```bash git clone https://github.com/w3c/json-ld-api ./specifications/json-ld-api git clone https://github.com/w3c/json-ld-framing ./specifications/json-ld-framing git clone https://github.com/json-ld/normalization ./specifications/normalization +git clone https://github.com/w3c/yaml-ld ./specifications/yaml-ld ``` Note that you can clone these repositories into any location you wish; however, From c35a8c2b2431b45107bc321ad4b67c691467b1b3 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 06/14] Add `source_file` macro and YAML-LD skip-link support --- docs_macros.py | 86 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/docs_macros.py b/docs_macros.py index fade8e17..a6aaa2e8 100644 --- a/docs_macros.py +++ b/docs_macros.py @@ -11,14 +11,25 @@ from yaml import SafeLoader ROOT_DIR = Path(__file__).resolve().parent -EXAMPLES_DIR = ROOT_DIR / 'docs' / 'examples' +DOCS_DIR = ROOT_DIR / 'docs' +EXAMPLES_DIR = DOCS_DIR / 'examples' sys.path.insert(0, str(ROOT_DIR / 'lib')) sys.path.insert(0, str(ROOT_DIR / 'tests')) +_SOURCE_LANG = { + '.yamlld': 'yaml', + '.yaml': 'yaml', + '.yml': 'yaml', + '.jsonld': 'json', + '.json': 'json', + '.py': 'python', +} + MANIFEST_BASES = { 'frame-manifest': 'https://w3c.github.io/json-ld-framing/tests', 'manifest-urgna2012': 'https://w3c.github.io/rdf-canon/tests', 'manifest-urdna2015': 'https://w3c.github.io/rdf-canon/tests', + 'manifest': 'https://w3c.github.io/yaml-ld/tests', } DEFAULT_TEST_BASE = 'https://w3c.github.io/json-ld-api/tests' @@ -26,6 +37,7 @@ _MANIFEST_PATHS = ( ROOT_DIR / 'specifications' / 'json-ld-api' / 'tests', ROOT_DIR / 'specifications' / 'json-ld-framing' / 'tests', + ROOT_DIR / 'specifications' / 'yaml-ld' / 'tests', ) @@ -33,12 +45,19 @@ def _parse_skip_id_regex(pattern): match = _SKIP_ID_PATTERN.fullmatch(pattern) if not match: return None - return match.group('manifest'), match.group('test_id') + # Suite-scoped patterns may include a path prefix, e.g. + # .*yaml-ld/tests/manifest#two-documents-from-stream$ + manifest = match.group('manifest') + if '/' in manifest: + manifest = manifest.rsplit('/', 1)[-1] + return manifest, match.group('test_id') def _test_url(manifest, test_id): - base = MANIFEST_BASES.get(manifest, DEFAULT_TEST_BASE) - return f'{base}/{manifest}.html#{test_id}' + # Basename only — suite-scoped regex captures may still contain '/'. + manifest_name = manifest.rsplit('/', 1)[-1] + base = MANIFEST_BASES.get(manifest_name, DEFAULT_TEST_BASE) + return f'{base}/{manifest_name}.html#{test_id}' def _jsonld_values(data, key): @@ -59,7 +78,11 @@ def _manifest_entries(): for manifest_dir in _MANIFEST_PATHS: if not manifest_dir.exists(): continue - for path in sorted(manifest_dir.glob('*-manifest.jsonld')): + paths = { + *manifest_dir.glob('*-manifest.jsonld'), + *manifest_dir.glob('manifest.jsonld'), + } + for path in sorted(paths): data = json.loads(path.read_text()) manifest = path.stem for entry in _jsonld_values(data, 'sequence'): @@ -82,6 +105,11 @@ def _skip_reason(test_type, skip, test): for pattern in skip.get('idRegex', []): if re.match(pattern, test_id): return f'Explicit skip (`{test_type}`)' + # Suite-scoped patterns (e.g. .*yaml-ld/tests/manifest#id$) still + # refer to the same short manifest#id used in local entries. + parsed = _parse_skip_id_regex(pattern) + if parsed and f'{parsed[0]}#{parsed[1]}' == test_id: + return f'Explicit skip (`{test_type}`)' for pattern in skip.get('descriptionRegex', []): if re.match(pattern, entry.get('description', '')): @@ -114,6 +142,13 @@ def _example_path(name): return path +def _docs_path(rel): + path = (DOCS_DIR / rel).resolve() + if not path.is_relative_to(DOCS_DIR.resolve()): + raise ValueError(f'Invalid docs path: {rel}') + return path + + def _github_branch(): branch = os.environ.get('GITHUB_REF_NAME') if branch: @@ -129,10 +164,20 @@ def _github_branch(): return 'master' -def _example_github_url(name, repo_url): - rel_path = Path('docs/examples') / name +def _docs_github_url(rel, repo_url): branch = 'master' - return f'{repo_url.rstrip("/")}/blob/{branch}/{rel_path.as_posix()}' + return f'{repo_url.rstrip("/")}/blob/{branch}/docs/{Path(rel).as_posix()}' + + +def _example_github_url(name, repo_url): + return _docs_github_url(Path('examples') / name, repo_url) + + +def _example_admonition(title, body, indent=0): + content_indent = indent + 4 + pad = ' ' * content_indent + indented = '\n'.join(f'{pad}{line}' for line in body.splitlines()) + return f'!!! example "{title}"\n\n{indented}\n' def _human_date(value): @@ -335,10 +380,7 @@ def example(name, output_syntax=None, indent=0): f'```python\n{source}```\n\n' f'```{output_lang} title="Output"\n{result.stdout}```' ) - content_indent = indent + 4 - pad = ' ' * content_indent - indented = '\n'.join(f'{pad}{line}' for line in body.splitlines()) - return f'!!! example "{title}"\n\n{indented}\n' + return _example_admonition(title, body, indent=indent) @env.macro def example_data(name, indent=0): @@ -355,10 +397,7 @@ def example_data(name, indent=0): f'' ) body = f'```{lang}\n{source}\n```' - content_indent = indent + 4 - pad = ' ' * content_indent - indented = '\n'.join(f'{pad}{line}' for line in body.splitlines()) - return f'!!! example "{title}"\n\n{indented}\n' + return _example_admonition(title, body, indent=indent) @env.macro def terminal(command, title='pyld', indent=0): @@ -393,3 +432,18 @@ def terminal(command, title='pyld', indent=0): return lines[0] + '\n' + '\n'.join( f'{pad}{line}' if line else line for line in lines[1:] ) + + @env.macro + def source_file(rel, indent=0): + """Include a docs file in an example admonition with a GitHub source link.""" + path = _docs_path(rel) + source = path.read_text() + github_url = _docs_github_url(rel, env.conf['repo_url']) + lang = _SOURCE_LANG.get(path.suffix.lower(), 'text') + title = ( + f'Example' + f':fontawesome-brands-github: [`{path.name}`]({github_url})' + f'' + ) + body = f'```{lang}\n{source}```' + return _example_admonition(title, body, indent=indent) From 06e5d4445ecf91f62a06855dac22cef8a2e2a5f4 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 07/14] Enable pymdownx.snippets for docs includes --- mkdocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 589c2c78..104ef81b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,10 @@ markdown_extensions: emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: anchor_linenums: true + - pymdownx.snippets: + base_path: + - docs + check_paths: true - pymdownx.superfences: custom_fences: - name: mermaid From 932091a6056fa88b4cac8a7accd7039e021ccb7c Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 08/14] Add YAML-LD intro example for the packaging ADR --- docs/project/decisions/examples/intro.yamlld | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/project/decisions/examples/intro.yamlld diff --git a/docs/project/decisions/examples/intro.yamlld b/docs/project/decisions/examples/intro.yamlld new file mode 100644 index 00000000..e7f685d0 --- /dev/null +++ b/docs/project/decisions/examples/intro.yamlld @@ -0,0 +1,19 @@ +"@context": + - https://json-ld.org/contexts/dollar-convenience.jsonld + - schema: https://schema.org/ + dbo: http://dbpedia.org/ontology/ + dbp: http://dbpedia.org/property/ + dbr: http://dbpedia.org/resource/ + xsd: http://www.w3.org/2001/XMLSchema# + dbp:discovered: + "@type": xsd:date + dbp:star: + "@type": "@id" + +$id: dbr:Proxima_Centauri_b +$type: dbo:Planet +schema:description: >- + The closest known exoplanet to Earth, + orbiting in Proxima Centauri's habitable zone. +dbp:discovered: 2016-08-24 +dbp:star: dbr:Proxima_Centauri From b8af67a6f5d4493ff40f50bc9ed4be2d5656a378 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 09/14] Add undecided ADR comparing YAML-LD packaging homes --- .../choose-where-to-host-yaml-ld-support.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/project/decisions/choose-where-to-host-yaml-ld-support.md diff --git a/docs/project/decisions/choose-where-to-host-yaml-ld-support.md b/docs/project/decisions/choose-where-to-host-yaml-ld-support.md new file mode 100644 index 00000000..9851a5a4 --- /dev/null +++ b/docs/project/decisions/choose-where-to-host-yaml-ld-support.md @@ -0,0 +1,81 @@ +--- +title: Treat YAML-LD support as … +status: undecided +date: 2026-07-19 +author: Anatoly Scherbakov +tags: [decision] +hide: [toc] +--- + +# Treat YAML-LD support as … + +{{ adr_metadata(date, status) }} + +## :material-text-box-outline: Context + +At the moment of writing this document, the [JSON-LD Working Group](https://www.w3.org/groups/wg/json-ld/) is preparing a new specification, [YAML-LD](https://www.w3.org/TR/yaml-ld-10/), for the status of [Recommendation](https://www.w3.org/standards/types/#x2-5-recommendation). It [leverages the readability and conciseness of YAML, but relies on the JSON-LD data model and API](https://www.w3.org/TR/yaml-ld-10/). Here is an example: + +{{ source_file('project/decisions/examples/intro.yamlld') }} + +The specification is already stable in the normative sense, and the Working Group is inviting implementations for the new standard. + +Should we provide such an implementation for Python within [:fontawesome-brands-github: `digitalbazaar/pyld`](https://github.com/digitalbazaar/pyld)? + +## :material-arrow-decision-outline: Decision + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PyLD implements YAML-LD`python-yaml-ld` implements YAML-LDPyLD is a façade on top of `python-yaml-ld`
Install`pip install 'PyLD[yaml-ld]'``pip install yaml-ld``pip install 'PyLD[yaml-ld]'`
DependenciesYAML parser, for instance: `ruamel.yaml` or `pyyaml`[PyLD](https://github.com/iolanta-tech/python-yaml-ld/blob/master/pyproject.toml)`PyLD` → `yaml-ld` → `PyLD`
Test suitePyLD (`specifications/yaml-ld`)`yaml-ld``yaml-ld`
YAML parsing`PyLD``yaml-ld``yaml-ld`
How to expand()`pyld.jsonld.expand()``yaml_ld.expand()``pyld.jsonld.expand()`
Decision:question::question::x: Circular dependency (`PyLD` → `yaml-ld` → `PyLD`)
+ +## :material-arrow-right-bold-outline: Consequences + +- Docs, optional extras, and ownership of YAML-LD conformance must match the chosen packaging home. +- Dependency surface (YAML parser and any optional extras) will either grow in PyLD or stay in a separate package. +- Release cadence for YAML-LD support fixes will either couple to PyLD releases or remain with a separate package. + +#### Implementation Steps + +- [ ] Record the Decision row outcome in this ADR (flip status to decided) +- [ ] Align suite location, YAML parsing ownership, `expand()` entry point, and install surface with the chosen column +- [ ] Update project index / user docs if needed From 65fd43a9f055a81fd2111f66650f17cc8609cc4a Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 10/14] Link YAML-LD packaging ADR from the project index --- docs/project/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/project/index.md b/docs/project/index.md index 1c579c95..b7923dff 100644 --- a/docs/project/index.md +++ b/docs/project/index.md @@ -13,6 +13,12 @@ Architecture Decision Records (ADRs) document the technical choices taken during
+!!! warning inline "[Treat YAML-LD support as …](decisions/choose-where-to-host-yaml-ld-support/)" + :material-calendar-clock: 19 July 2026 + + Bundle into PyLD, keep a separate package (with suite under `specifications/`), + expose a thin PyLD facade, or treat YAML-LD as out of scope (no suite either). + !!! success inline "[Use `requests-cache` for persistent HTTP caching in synchronous Python code](decisions/use-requests-cache-for-sync-http-caching-in-document-loaders/)" :material-calendar-clock: 29 June 2026 From b6817d01f56d47a1ab6dfb742713e7248508b00a Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 19 Jul 2026 21:27:12 +0400 Subject: [PATCH 11/14] Ignore local uv.lock --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 820ee873..9ffb4bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ tests/data/test_caching.json # Local version file for pyenv .python-version + +# Local lock file for uv +uv.lock From 5ed3a4cbf275bd61357c4c7a718a75d775bdc635 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Tue, 1 Sep 2026 00:36:41 +0400 Subject: [PATCH 12/14] Restructure YAML-LD packaging ADR and decide expand via pyld.jsonld.expand() Co-authored-by: Cursor --- .../choose-where-to-host-yaml-ld-support.md | 81 ------------- .../implement-yaml-ld-in-pyld.md | 112 ++++++++++++++++++ .../index.md | 71 +++++++++++ .../provide-yaml-ld-through-a-pyld-facade.md | 93 +++++++++++++++ .../use-pyld-jsonld-expand-for-yaml-ld.md | 49 ++++++++ docs/project/index.md | 6 +- docs/stylesheets/extra.css | 15 +++ docs_macros.py | 19 ++- mkdocs.yml | 1 + 9 files changed, 360 insertions(+), 87 deletions(-) delete mode 100644 docs/project/decisions/choose-where-to-host-yaml-ld-support.md create mode 100644 docs/project/decisions/choose-where-to-host-yaml-ld-support/implement-yaml-ld-in-pyld.md create mode 100644 docs/project/decisions/choose-where-to-host-yaml-ld-support/index.md create mode 100644 docs/project/decisions/choose-where-to-host-yaml-ld-support/provide-yaml-ld-through-a-pyld-facade.md create mode 100644 docs/project/decisions/use-pyld-jsonld-expand-for-yaml-ld.md diff --git a/docs/project/decisions/choose-where-to-host-yaml-ld-support.md b/docs/project/decisions/choose-where-to-host-yaml-ld-support.md deleted file mode 100644 index 9851a5a4..00000000 --- a/docs/project/decisions/choose-where-to-host-yaml-ld-support.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Treat YAML-LD support as … -status: undecided -date: 2026-07-19 -author: Anatoly Scherbakov -tags: [decision] -hide: [toc] ---- - -# Treat YAML-LD support as … - -{{ adr_metadata(date, status) }} - -## :material-text-box-outline: Context - -At the moment of writing this document, the [JSON-LD Working Group](https://www.w3.org/groups/wg/json-ld/) is preparing a new specification, [YAML-LD](https://www.w3.org/TR/yaml-ld-10/), for the status of [Recommendation](https://www.w3.org/standards/types/#x2-5-recommendation). It [leverages the readability and conciseness of YAML, but relies on the JSON-LD data model and API](https://www.w3.org/TR/yaml-ld-10/). Here is an example: - -{{ source_file('project/decisions/examples/intro.yamlld') }} - -The specification is already stable in the normative sense, and the Working Group is inviting implementations for the new standard. - -Should we provide such an implementation for Python within [:fontawesome-brands-github: `digitalbazaar/pyld`](https://github.com/digitalbazaar/pyld)? - -## :material-arrow-decision-outline: Decision - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PyLD implements YAML-LD`python-yaml-ld` implements YAML-LDPyLD is a façade on top of `python-yaml-ld`
Install`pip install 'PyLD[yaml-ld]'``pip install yaml-ld``pip install 'PyLD[yaml-ld]'`
DependenciesYAML parser, for instance: `ruamel.yaml` or `pyyaml`[PyLD](https://github.com/iolanta-tech/python-yaml-ld/blob/master/pyproject.toml)`PyLD` → `yaml-ld` → `PyLD`
Test suitePyLD (`specifications/yaml-ld`)`yaml-ld``yaml-ld`
YAML parsing`PyLD``yaml-ld``yaml-ld`
How to expand()`pyld.jsonld.expand()``yaml_ld.expand()``pyld.jsonld.expand()`
Decision:question::question::x: Circular dependency (`PyLD` → `yaml-ld` → `PyLD`)
- -## :material-arrow-right-bold-outline: Consequences - -- Docs, optional extras, and ownership of YAML-LD conformance must match the chosen packaging home. -- Dependency surface (YAML parser and any optional extras) will either grow in PyLD or stay in a separate package. -- Release cadence for YAML-LD support fixes will either couple to PyLD releases or remain with a separate package. - -#### Implementation Steps - -- [ ] Record the Decision row outcome in this ADR (flip status to decided) -- [ ] Align suite location, YAML parsing ownership, `expand()` entry point, and install surface with the chosen column -- [ ] Update project index / user docs if needed diff --git a/docs/project/decisions/choose-where-to-host-yaml-ld-support/implement-yaml-ld-in-pyld.md b/docs/project/decisions/choose-where-to-host-yaml-ld-support/implement-yaml-ld-in-pyld.md new file mode 100644 index 00000000..6e3b56ad --- /dev/null +++ b/docs/project/decisions/choose-where-to-host-yaml-ld-support/implement-yaml-ld-in-pyld.md @@ -0,0 +1,112 @@ +# :material-package-variant: Implement YAML-LD in PyLD + +!!! info "Conditional roadmap" + + This is the implementation universe in which [:fontawesome-brands-github: `digitalbazaar/pyld`](https://github.com/digitalbazaar/pyld) owns YAML-LD support. It applies only if the parent decision selects this alternative. + +## :material-target: Delivered contract + +`PyLD[yaml-ld]` adds YAML-LD document loading to the existing PyLD API; `PyLD[cli,yaml-ld]` adds that capability to the CLI. The implementation supports the YAML-LD JSON profile only: YAML 1.2 Core-schema values that produce JSON values. It does not implement the YAML-LD extended profile: `processingMode='yaml-ld-extended'`, or a profile token exactly `http://www.w3.org/ns/json-ld#extended`, fails with `jsonld.LoadDocumentError`, code `profile-error`. Ignore unknown profile tokens; do not reject an arbitrary `profile` parameter. YAML tags outside the Core schema are discarded when constructing the JSON representation, as required by the JSON profile. A missing `ruamel.yaml` import fails only when a YAML document is actually parsed, with an actionable `yaml-ld` extra message. + +Raw YAML `str` and `bytes` are not new public inputs to `expand`, `compact`, `flatten`, `frame`, `to_rdf`, or `normalize`: a string remains a document URL and a mapping/list remains an already-parsed JSON-compatible document. YAML enters those APIs through a document URL and its document loader. `from_rdf` remains raw RDF input only. + +Do not add `pyld.yaml_ld.expand()` or sibling transformation functions. A +`pyld.yaml_ld` namespace may expose parsing helpers such as `loads()` and +`loads_all()`, while `pyld.jsonld.*` remains the sole transformation API. + +## :material-source-branch: Implementation baseline + +Build this work on [:fontawesome-brands-github: `306-complete-pyld-cli`](https://github.com/digitalbazaar/pyld/tree/306-complete-pyld-cli), which adds [`lib/pyld/cli/input.py`](https://github.com/digitalbazaar/pyld/blob/306-complete-pyld-cli/lib/pyld/cli/input.py) and the command test contract. Either rebase after that branch merges, or create this as a stacked PR with `306-complete-pyld-cli` as its base. Do not reimplement the CLI in this change. + +## :material-clipboard-check-outline: Architectural decisions + +### One media parser at the PyLD loading boundary + +Add `lib/pyld/documentloader/media.py`, with a single idempotent entry point used by [`jsonld.load_document`](https://github.com/digitalbazaar/pyld/blob/master/lib/pyld/jsonld.py): `parse_remote_document(remote_doc, options, profile) -> RemoteDocument`. + +1. Normalize `contentType` by lowercasing the media type and removing parameters for dispatch, while retaining the parameter map to inspect `profile`. Recognize `application/ld+yaml`, `application/yaml`, `application/x-yaml`, and `*+yaml`; recognize JSON as `application/ld+json`, `application/json`, and `*+json`; retain the existing HTML types. +2. If `remote_doc['document']` is a mapping or list, return it unchanged. This makes the boundary idempotent and keeps custom loaders which already return parsed JSON compatible. +3. Built-in loaders always supply raw bytes. A custom loader may supply text, which the boundary first encodes as UTF-8 and then decodes using the same optional-BOM path; an unencodable string or malformed byte sequence is `invalid-encoding`. Any other value is a `loading document failed` error. Dispatch YAML and JSON to the media parser; dispatch HTML to the PyLD HTML folding algorithm below. The parser replaces `remote_doc['document']` with only ordinary Python `dict`, `list`, `str`, `int`, `float`, `bool`, or `None` values. +4. Call this helper exactly once, immediately after `options['documentLoader'](url, options)` in `load_document`, before the null-document check and before processing API code observes the result. It owns parsing, HTML traversal, base handling, profile selection, and fragment selection. Built-in and custom loader output therefore follows the same path. + +Change [`FileDocumentLoader`](https://github.com/digitalbazaar/pyld/blob/master/lib/pyld/documentloader/file.py), [`RequestsDocumentLoader`](https://github.com/digitalbazaar/pyld/blob/master/lib/pyld/documentloader/requests.py), and [`AioHttpDocumentLoader`](https://github.com/digitalbazaar/pyld/blob/master/lib/pyld/documentloader/aiohttp.py) to return raw bytes in `document`, plus `contentType`, `contextUrl`, and final `documentUrl`. In particular, replace Requests’ `response.json()` and aiohttp’s `response.json(content_type=None)` with raw-body reads. `SqliteCacheRequestsDocumentLoader` continues to inherit the Requests behavior. Do not change the documented `RemoteDocument` fields or require third-party loaders to change: their parsed mapping/list output is the idempotent case. + +### YAML parsing and errors + +Use `ruamel.yaml >=0.19`, configured for YAML 1.2 and the Core schema. Decode once, safely compose/load every document in a stream, and recursively convert it to JSON values: keys must be strings; aliases are copied; undefined aliases and cyclic aliases fail; `.inf` and `.nan` are rejected because they are not JSON numbers. A top-level scalar fails and an empty stream fails. With `extractAllScripts=False`, a YAML stream returns its first document; with `extractAllScripts=True`, it returns an array of all stream documents, including an array of one document. This rule applies equally to direct YAML and YAML script bodies. + +Production errors use the current YAML-LD vocabulary: decode errors are `invalid-encoding`; non-string mapping keys are `mapping-key-error`; and the extended profile is `profile-error`. Syntax errors, scalar roots, empty streams, undefined/cyclic aliases, non-JSON floats, and unsupported document shapes are `loading document failed`. JSON is parsed only by the JSON dispatch branch and YAML only by YAML dispatch: do not describe or implement a “valid JSON but invalid YAML” fallback. + +### HTTP, files, links, and negotiation + +Extend `CONTENT_TYPES` with `.yamlld` → `application/ld+yaml` and `.yaml` → `application/yaml`. The exact default `Accept` header, unless the caller supplied `options['headers']`, is: + +```text +application/ld+yaml, application/ld+json;q=0.9, application/yaml;q=0.8, application/x-yaml;q=0.8, application/json;q=0.7, text/html;q=0.5, application/xhtml+xml;q=0.5 +``` + +This intentional YAML-first ordering implements the YAML-LD alternative’s preference; test the literal value in both HTTP loaders. When `requestProfile` is set, prepend the RFC-quoted `application/ld+json;profile="", `, preserving the remaining order. + +Normalize a response `Content-Type` before link decisions. Continue to reject multiple JSON-LD context links. A `rel=alternate` is followed only if its normalized `type` is a supported YAML or JSON media type and the response is neither a supported YAML nor JSON media type; resolve it against the original request URL and recurse through the same loader with the same options. A context link is retained only when the response is neither `application/ld+json` nor `application/ld+yaml`; do not synthesize a context link from a YAML document. Preserve final URLs, redirect behavior, secure-mode enforcement, supplied headers, cache keys, and cache hit/miss behavior. + +### HTML folding, owned by PyLD + +`parse_remote_document` calls a refactored `load_html` that takes the raw HTML text, `documentUrl`, `profile`, and `options`; no transport loader parses HTML or script bodies. + +1. Parse HTML and resolve the first `` against `options['base']` or `documentUrl`; store the resolved base in `options['base']` and replace the remote document URL as current `load_document` does. +2. If `documentUrl` has a fragment, select exactly the script whose `id` equals that fragment. It must have a supported JSON or YAML media type; otherwise raise `loading document failed`. Ignore `extractAllScripts` in this case and return only that script’s parsed document(s). +3. Otherwise select `