diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 43ad16fa..bb4d6f8d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -69,6 +69,7 @@ jobs: pytest tests/test_manifests.py --tests=./specifications/json-ld-api/tests --loader=${{ matrix.loader }} 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/yaml-ld/tests --loader=${{ matrix.loader }} pytest --ignore ./tests/test_manifests.py env: LOADER: ${{ matrix.loader }} 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 diff --git a/.gitmodules b/.gitmodules index f4e5432a..6c7ecd1d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "specifications/normalization"] path = specifications/normalization url = https://github.com/json-ld/normalization.git +[submodule "specifications/yaml-ld"] + path = specifications/yaml-ld + url = https://github.com/w3c/yaml-ld.git diff --git a/README.md b/README.md index d38ad939..cd3fd58f 100644 --- a/README.md +++ b/README.md @@ -331,13 +331,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, 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 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 diff --git a/docs/project/index.md b/docs/project/index.md index e42be8e9..aeeeb62c 100644 --- a/docs/project/index.md +++ b/docs/project/index.md @@ -12,6 +12,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 diff --git a/docs_macros.py b/docs_macros.py index 1a3c3643..99b84d22 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): @@ -319,7 +364,19 @@ 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 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) diff --git a/mkdocs.yml b/mkdocs.yml index 4d2a59db..340dedad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,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 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 diff --git a/tests/runtests.py b/tests/runtests.py index 255dc8f9..26b12cb2 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -91,12 +91,14 @@ '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/yaml-ld/tests', ] SPEC_DIRS = [ '../specifications/json-ld-api/tests/', '../specifications/json-ld-framing/tests/', '../specifications/normalization/tests/', + '../specifications/yaml-ld/tests/', ] # NOTE: The following TestRunner class can be removed because pytest now @@ -718,6 +720,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'): @@ -918,7 +922,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'], - 'idRegex': [], + # 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': [ @@ -950,7 +958,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'], - 'idRegex': [], + # 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()], @@ -961,7 +1012,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'], - 'idRegex': [], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#flatten$', + ], }, 'fn': 'flatten', 'params': [ @@ -976,7 +1030,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'], - 'idRegex': [], + # YAML-LD suite: only currently failing cases (passing cases stay active) + 'idRegex': [ + '.*yaml-ld/tests/manifest#frame-t0001$', + ], }, 'fn': 'frame', 'params': [ @@ -1026,7 +1083,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'], - 'idRegex': [], + # 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': [