From cad5f80934162e39e21881f2f11d3806f6c9b798 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 01/13] [#300]: Add `ChoiceBySchemeDocumentLoader` for scheme-based dispatch --- lib/pyld/documentloader/choice_by_scheme.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/pyld/documentloader/choice_by_scheme.py diff --git a/lib/pyld/documentloader/choice_by_scheme.py b/lib/pyld/documentloader/choice_by_scheme.py new file mode 100644 index 00000000..bfe8cfa8 --- /dev/null +++ b/lib/pyld/documentloader/choice_by_scheme.py @@ -0,0 +1,63 @@ +""" +Scheme-dispatching JSON-LD document loader. + +.. module:: jsonld.documentloader.choice_by_scheme + :synopsis: ChoiceBySchemeDocumentLoader for multi-scheme document loading +""" + +from collections.abc import Callable +from pathlib import Path +from urllib.parse import urlparse + +from pyld.documentloader.base import DocumentLoader, RemoteDocument +from pyld.jsonld import JsonLdError + + +class ChoiceBySchemeDocumentLoader(DocumentLoader): + """Document loader that dispatches to per-scheme loaders. + + Constructed as keyword arguments mapping each URL scheme name to a + `DocumentLoader` instance or plain callable. Non-identifier scheme names + can be passed via ``ChoiceBySchemeDocumentLoader(**{'view-source': loader})``. + + An empty / missing scheme (including a `pathlib.Path` argument) is treated + as `file`. An unregistered scheme raises `JsonLdError` with code + `loading document failed` and details naming the registered schemes. + + :param loaders: keyword mapping of scheme name to document loader. + """ + + def __init__(self, **loaders: DocumentLoader | Callable) -> None: + self.loaders = { + scheme.lower(): loader for scheme, loader in loaders.items() + } + + def __call__( + self, url: str | Path, options: dict | None = None + ) -> RemoteDocument: + """Retrieve the JSON-LD document at `url` via the matching scheme loader. + + :param url: the URL (or `pathlib.Path`) to retrieve. + :param options: loader options forwarded to the chosen loader. + :return: a `RemoteDocument`. + """ + if options is None: + options = {} + + if isinstance(url, Path): + scheme = 'file' + else: + scheme = urlparse(url).scheme or 'file' + + try: + loader = self.loaders[scheme] + except KeyError: + raise JsonLdError( + 'URL could not be dereferenced; no loader is registered for ' + f'the "{scheme}" scheme.', + 'jsonld.InvalidUrl', + {'url': url, 'schemes': sorted(self.loaders)}, + code='loading document failed', + ) from None + + return loader(url, options) From f923081684cc67fc0785e7572d985a417cfc445b Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 02/13] [#300]: Export `ChoiceBySchemeDocumentLoader` from `pyld` --- lib/pyld/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pyld/__init__.py b/lib/pyld/__init__.py index c107b47c..380d35af 100644 --- a/lib/pyld/__init__.py +++ b/lib/pyld/__init__.py @@ -4,6 +4,7 @@ from .context_resolver import ContextResolver from .documentloader.aiohttp import AioHttpDocumentLoader from .documentloader.base import DocumentLoader, RemoteDocument +from .documentloader.choice_by_scheme import ChoiceBySchemeDocumentLoader from .documentloader.file import FileDocumentLoader from .documentloader.frozen import BUNDLED_CONTEXTS, FrozenDocumentLoader from .documentloader.requests import RequestsDocumentLoader @@ -12,6 +13,7 @@ __all__ = [ 'AioHttpDocumentLoader', 'BUNDLED_CONTEXTS', + 'ChoiceBySchemeDocumentLoader', 'ContextResolver', 'DocumentLoader', 'FileDocumentLoader', From 179aa75e97c30599a2136056578a778b82f3ce0f Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 03/13] [#300]: Add tests for `ChoiceBySchemeDocumentLoader` --- .../test_choice_by_scheme_document_loader.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tests/test_choice_by_scheme_document_loader.py diff --git a/tests/test_choice_by_scheme_document_loader.py b/tests/test_choice_by_scheme_document_loader.py new file mode 100644 index 00000000..f2ff7734 --- /dev/null +++ b/tests/test_choice_by_scheme_document_loader.py @@ -0,0 +1,132 @@ +"""Tests for ChoiceBySchemeDocumentLoader.""" + +import json +from pathlib import Path + +import pytest + +from pyld import ( + ChoiceBySchemeDocumentLoader, + FileDocumentLoader, + FrozenDocumentLoader, + jsonld, +) +from pyld.jsonld import JsonLdError + +_CONTEXT_URL = 'https://example.com/context' +_CONTEXT = {'@context': {'name': 'http://schema.org/name'}} +_PERSON = { + '@context': _CONTEXT_URL, + 'name': 'Ada Lovelace', +} + + +def _file_url(path: Path) -> str: + return path.resolve().as_uri() + + +def test_dispatches_file_url(tmp_path): + """A file: URL is dispatched to the file loader.""" + path = tmp_path / 'person.jsonld' + path.write_text(json.dumps(_PERSON), encoding='utf-8') + loader = ChoiceBySchemeDocumentLoader(file=FileDocumentLoader()) + + result = loader(_file_url(path), {}) + + assert result['contentType'] == 'application/ld+json' + assert json.loads(result['document']) == _PERSON + + +def test_dispatches_https_url(): + """An https: URL is dispatched to the https loader.""" + http = FrozenDocumentLoader(documents={_CONTEXT_URL: _CONTEXT}) + loader = ChoiceBySchemeDocumentLoader(https=http) + + result = loader(_CONTEXT_URL, {}) + + assert result['document'] == _CONTEXT + assert result['documentUrl'] == _CONTEXT_URL + + +def test_scheme_less_path_routes_to_file(tmp_path): + """A scheme-less absolute path routes to the file loader.""" + path = tmp_path / 'person.jsonld' + path.write_text(json.dumps(_PERSON), encoding='utf-8') + loader = ChoiceBySchemeDocumentLoader(file=FileDocumentLoader()) + + result = loader(str(path.resolve()), {}) + + assert json.loads(result['document']) == _PERSON + assert result['documentUrl'].startswith('file:') + + +def test_pathlib_path_routes_to_file(tmp_path): + """A pathlib.Path routes to the file loader.""" + path = tmp_path / 'person.jsonld' + path.write_text(json.dumps(_PERSON), encoding='utf-8') + loader = ChoiceBySchemeDocumentLoader(file=FileDocumentLoader()) + + result = loader(path, {}) + + assert json.loads(result['document']) == _PERSON + assert result['documentUrl'] == path.resolve().as_uri() + + +def test_unregistered_scheme_raises(): + """An unregistered scheme raises JsonLdError naming registered schemes.""" + loader = ChoiceBySchemeDocumentLoader(file=FileDocumentLoader()) + + with pytest.raises(JsonLdError) as exc: + loader('https://example.com/person.jsonld', {}) + + assert exc.value.code == 'loading document failed' + assert exc.value.type == 'jsonld.InvalidUrl' + assert exc.value.details['schemes'] == ['file'] + + +def test_scheme_matching_is_case_insensitive(): + """Registered scheme keys match uppercase URL schemes.""" + http = FrozenDocumentLoader(documents={_CONTEXT_URL: _CONTEXT}) + loader = ChoiceBySchemeDocumentLoader(HTTPS=http) + + result = loader(_CONTEXT_URL, {}) + + assert result['document'] == _CONTEXT + + +def test_plain_callable_is_accepted(): + """A plain callable (not a DocumentLoader) is accepted as a scheme loader.""" + remote = { + 'contentType': 'application/ld+json', + 'contextUrl': None, + 'documentUrl': _CONTEXT_URL, + 'document': _CONTEXT, + } + + def stub(url, options): + return remote + + loader = ChoiceBySchemeDocumentLoader(https=stub) + + assert loader(_CONTEXT_URL, {}) is remote + + +def test_expand_local_file_with_remote_context(tmp_path): + """Expand a local file whose @context is an https URL via composed loaders.""" + path = tmp_path / 'person.jsonld' + path.write_text(json.dumps(_PERSON), encoding='utf-8') + http = FrozenDocumentLoader(documents={_CONTEXT_URL: _CONTEXT}) + loader = ChoiceBySchemeDocumentLoader( + file=FileDocumentLoader(), + http=http, + https=http, + ) + + expanded = jsonld.expand( + _file_url(path), + options={'documentLoader': loader}, + ) + + assert expanded == [ + {'http://schema.org/name': [{'@value': 'Ada Lovelace'}]} + ] From 5d72bd7a8aa8c5fff603b2baac2b132044c7b432 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 04/13] [#300]: Add sample person JSON-LD with remote `@context` --- docs/examples/data/person_remote_context.jsonld | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/examples/data/person_remote_context.jsonld diff --git a/docs/examples/data/person_remote_context.jsonld b/docs/examples/data/person_remote_context.jsonld new file mode 100644 index 00000000..a3e21b14 --- /dev/null +++ b/docs/examples/data/person_remote_context.jsonld @@ -0,0 +1,4 @@ +{ + "@context": "https://example.com/context", + "name": "Ada Lovelace" +} From 24c4d9a3c71f4862920ef3b5794b98a22b6aba0d Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 05/13] [#300]: Add `ChoiceBySchemeDocumentLoader` docs example --- .../document_loaders/choice_by_scheme.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/examples/document_loaders/choice_by_scheme.py diff --git a/docs/examples/document_loaders/choice_by_scheme.py b/docs/examples/document_loaders/choice_by_scheme.py new file mode 100644 index 00000000..ef43193f --- /dev/null +++ b/docs/examples/document_loaders/choice_by_scheme.py @@ -0,0 +1,31 @@ +import json +from pathlib import Path + +from pyld import ( + ChoiceBySchemeDocumentLoader, + FileDocumentLoader, + FrozenDocumentLoader, + jsonld, +) + +person = ( + Path(__file__).resolve().parent.parent / 'data' / 'person_remote_context.jsonld' +) + +http = FrozenDocumentLoader( + documents={ + 'https://example.com/context': { + '@context': {'name': 'http://schema.org/name'}, + }, + } +) +loader = ChoiceBySchemeDocumentLoader( + file=FileDocumentLoader(), + http=http, + https=http, +) +result = jsonld.expand( + person.as_uri(), + options={'documentLoader': loader}, +) +print(json.dumps(result, indent=2)) From d38c8450c0f431f3c3b91e2df2c6e85eaaa944ec Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 06/13] [#300]: Document `ChoiceBySchemeDocumentLoader` --- .../document-loaders/choice-by-scheme.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/reference/document-loaders/choice-by-scheme.md diff --git a/docs/reference/document-loaders/choice-by-scheme.md b/docs/reference/document-loaders/choice-by-scheme.md new file mode 100644 index 00000000..c398192d --- /dev/null +++ b/docs/reference/document-loaders/choice-by-scheme.md @@ -0,0 +1,25 @@ +--- +hide: [toc] +--- +# :material-call-split: `ChoiceBySchemeDocumentLoader` + +::: pyld.ChoiceBySchemeDocumentLoader + options: + show_root_heading: false + show_bases: false + heading_level: 3 + members: false + +Compose per-scheme loaders so a local document can resolve a remote `@context`: + +=== "Example" + + {{ example('document_loaders/choice_by_scheme.py', output_syntax='json', indent=4) }} + +=== "person_remote_context.jsonld" + + {{ example_data('data/person_remote_context.jsonld', indent=4) }} + +An empty or missing scheme (including a `pathlib.Path` argument) is treated as +`file`. An unregistered scheme raises `JsonLdError` with code +`loading document failed` and details naming the schemes that are registered. From c3648cc8926a9623369ebc24aa9c72b706f2513e Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 07/13] [#300]: Link `ChoiceBySchemeDocumentLoader` on document loaders index --- docs/reference/document-loaders/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/document-loaders/index.md b/docs/reference/document-loaders/index.md index 383597c5..d8127238 100644 --- a/docs/reference/document-loaders/index.md +++ b/docs/reference/document-loaders/index.md @@ -38,6 +38,12 @@ class-based loaders for common cases and supports custom subclasses of Read local JSON-LD documents from `file:` URLs. +- [:material-call-split:{ .lg .middle } `ChoiceBySchemeDocumentLoader`](choice-by-scheme.md) + + --- + + Delegate to another Document Loader based on the scheme of the URL, for instance, `file:` vs `https://`. + - [:material-code-braces:{ .lg .middle } __Custom Document Loaders__](custom.md) --- From 7c9c105b4a004b5bf5ed8fb486373d1e6486e693 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 08/13] [#300]: Add `example_data` mkdocs macro for sourced example files --- docs_macros.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs_macros.py b/docs_macros.py index fd066217..2f38c897 100644 --- a/docs_macros.py +++ b/docs_macros.py @@ -339,3 +339,23 @@ def example(name, output_syntax=None, indent=0): pad = ' ' * content_indent indented = '\n'.join(f'{pad}{line}' for line in body.splitlines()) return f'!!! example "{title}"\n\n{indented}\n' + + @env.macro + def example_data(name, indent=0): + path = _example_path(name) + source = path.read_text().rstrip('\n') + suffix = path.suffix.lower() + lang = 'json' if suffix in {'.json', '.jsonld'} else ( + suffix.lstrip('.') or 'text' + ) + github_url = _example_github_url(name, env.conf['repo_url']) + title = ( + f'Source' + f':fontawesome-brands-github: [`{path.name}`]({github_url})' + 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' From d5136bebae8df18dc1a3c5bc0c3cdbb38b54178e Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 09/13] [#300]: Show sourced JSON-LD beside `FileDocumentLoader` examples --- docs/reference/document-loaders/file.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/reference/document-loaders/file.md b/docs/reference/document-loaders/file.md index b4f023b3..895a9e2d 100644 --- a/docs/reference/document-loaders/file.md +++ b/docs/reference/document-loaders/file.md @@ -10,7 +10,13 @@ hide: [toc] heading_level: 3 members: false -{{ example('document_loaders/file_basic.py', output_syntax='json') }} +=== "Example" + + {{ example('document_loaders/file_basic.py', output_syntax='json', indent=4) }} + +=== "person.jsonld" + + {{ example_data('data/person.jsonld', indent=4) }} ## Content Types @@ -26,4 +32,10 @@ Pass `root` to refuse paths that resolve outside a directory. Resolved paths and symlink targets are checked, so `..` traversal and symlink escapes are rejected: -{{ example('document_loaders/file_root.py', output_syntax='json') }} +=== "Example" + + {{ example('document_loaders/file_root.py', output_syntax='json', indent=4) }} + +=== "person.jsonld" + + {{ example_data('data/person.jsonld', indent=4) }} From c6fadbf1d10ae0d5afad7365a56f140ef0045b3d Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 11:48:56 +0400 Subject: [PATCH 10/13] [#300]: Note `ChoiceBySchemeDocumentLoader` in the unreleased changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7c8559..32360737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `*Options` TypedDict types and a `Context` type alias in `pyld.options` for JSON-LD API option dicts (typing and documentation). - `pyld.SqliteCacheRequestsDocumentLoader`: a SQLite-backed HTTP cache document loader using `requests-cache`. - `pyld.FileDocumentLoader`: a document loader for local `file:` URLs with optional root confinement. +- `pyld.ChoiceBySchemeDocumentLoader`: a document loader that dispatches to per-scheme loaders. ### Fixed - If value objects contain array values for `@type` during expansion, an error is now raised. Fixes [expand#ter54](https://w3c.github.io/json-ld-api/tests/expand-manifest.html#ter54) and [toRdf#ter54](https://w3c.github.io/json-ld-api/tests/toRdf-manifest.html#ter54). From c0e8ca3d51cb7c648bde745a49419a1d7406fe4f Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 21:26:17 +0400 Subject: [PATCH 11/13] [#300]: Accept only `DocumentLoader` instances and URL strings in `ChoiceBySchemeDocumentLoader` --- .../document-loaders/choice-by-scheme.md | 4 -- lib/pyld/documentloader/choice_by_scheme.py | 26 ++++------- .../test_choice_by_scheme_document_loader.py | 46 +++---------------- 3 files changed, 15 insertions(+), 61 deletions(-) diff --git a/docs/reference/document-loaders/choice-by-scheme.md b/docs/reference/document-loaders/choice-by-scheme.md index c398192d..71f8c540 100644 --- a/docs/reference/document-loaders/choice-by-scheme.md +++ b/docs/reference/document-loaders/choice-by-scheme.md @@ -19,7 +19,3 @@ Compose per-scheme loaders so a local document can resolve a remote `@context`: === "person_remote_context.jsonld" {{ example_data('data/person_remote_context.jsonld', indent=4) }} - -An empty or missing scheme (including a `pathlib.Path` argument) is treated as -`file`. An unregistered scheme raises `JsonLdError` with code -`loading document failed` and details naming the schemes that are registered. diff --git a/lib/pyld/documentloader/choice_by_scheme.py b/lib/pyld/documentloader/choice_by_scheme.py index bfe8cfa8..e8cd4a73 100644 --- a/lib/pyld/documentloader/choice_by_scheme.py +++ b/lib/pyld/documentloader/choice_by_scheme.py @@ -5,8 +5,6 @@ :synopsis: ChoiceBySchemeDocumentLoader for multi-scheme document loading """ -from collections.abc import Callable -from pathlib import Path from urllib.parse import urlparse from pyld.documentloader.base import DocumentLoader, RemoteDocument @@ -17,37 +15,29 @@ class ChoiceBySchemeDocumentLoader(DocumentLoader): """Document loader that dispatches to per-scheme loaders. Constructed as keyword arguments mapping each URL scheme name to a - `DocumentLoader` instance or plain callable. Non-identifier scheme names - can be passed via ``ChoiceBySchemeDocumentLoader(**{'view-source': loader})``. + `DocumentLoader` instance. Non-identifier scheme names can be passed via + ``ChoiceBySchemeDocumentLoader(**{'view-source': loader})``. - An empty / missing scheme (including a `pathlib.Path` argument) is treated - as `file`. An unregistered scheme raises `JsonLdError` with code + An unregistered scheme raises `JsonLdError` with code `loading document failed` and details naming the registered schemes. :param loaders: keyword mapping of scheme name to document loader. """ - def __init__(self, **loaders: DocumentLoader | Callable) -> None: - self.loaders = { - scheme.lower(): loader for scheme, loader in loaders.items() - } + def __init__(self, **loaders: DocumentLoader) -> None: + self.loaders = {scheme.lower(): loader for scheme, loader in loaders.items()} - def __call__( - self, url: str | Path, options: dict | None = None - ) -> RemoteDocument: + def __call__(self, url: str, options: dict | None = None) -> RemoteDocument: """Retrieve the JSON-LD document at `url` via the matching scheme loader. - :param url: the URL (or `pathlib.Path`) to retrieve. + :param url: the URL string to retrieve. :param options: loader options forwarded to the chosen loader. :return: a `RemoteDocument`. """ if options is None: options = {} - if isinstance(url, Path): - scheme = 'file' - else: - scheme = urlparse(url).scheme or 'file' + scheme = urlparse(url).scheme try: loader = self.loaders[scheme] diff --git a/tests/test_choice_by_scheme_document_loader.py b/tests/test_choice_by_scheme_document_loader.py index f2ff7734..d8106221 100644 --- a/tests/test_choice_by_scheme_document_loader.py +++ b/tests/test_choice_by_scheme_document_loader.py @@ -48,28 +48,15 @@ def test_dispatches_https_url(): assert result['documentUrl'] == _CONTEXT_URL -def test_scheme_less_path_routes_to_file(tmp_path): - """A scheme-less absolute path routes to the file loader.""" - path = tmp_path / 'person.jsonld' - path.write_text(json.dumps(_PERSON), encoding='utf-8') - loader = ChoiceBySchemeDocumentLoader(file=FileDocumentLoader()) - - result = loader(str(path.resolve()), {}) - - assert json.loads(result['document']) == _PERSON - assert result['documentUrl'].startswith('file:') - - -def test_pathlib_path_routes_to_file(tmp_path): - """A pathlib.Path routes to the file loader.""" - path = tmp_path / 'person.jsonld' - path.write_text(json.dumps(_PERSON), encoding='utf-8') +def test_scheme_less_url_raises(): + """A scheme-less URL raises when no empty-scheme loader is registered.""" loader = ChoiceBySchemeDocumentLoader(file=FileDocumentLoader()) - result = loader(path, {}) + with pytest.raises(JsonLdError) as exc: + loader('/tmp/person.jsonld', {}) - assert json.loads(result['document']) == _PERSON - assert result['documentUrl'] == path.resolve().as_uri() + assert exc.value.code == 'loading document failed' + assert exc.value.details['schemes'] == ['file'] def test_unregistered_scheme_raises(): @@ -94,23 +81,6 @@ def test_scheme_matching_is_case_insensitive(): assert result['document'] == _CONTEXT -def test_plain_callable_is_accepted(): - """A plain callable (not a DocumentLoader) is accepted as a scheme loader.""" - remote = { - 'contentType': 'application/ld+json', - 'contextUrl': None, - 'documentUrl': _CONTEXT_URL, - 'document': _CONTEXT, - } - - def stub(url, options): - return remote - - loader = ChoiceBySchemeDocumentLoader(https=stub) - - assert loader(_CONTEXT_URL, {}) is remote - - def test_expand_local_file_with_remote_context(tmp_path): """Expand a local file whose @context is an https URL via composed loaders.""" path = tmp_path / 'person.jsonld' @@ -127,6 +97,4 @@ def test_expand_local_file_with_remote_context(tmp_path): options={'documentLoader': loader}, ) - assert expanded == [ - {'http://schema.org/name': [{'@value': 'Ada Lovelace'}]} - ] + assert expanded == [{'http://schema.org/name': [{'@value': 'Ada Lovelace'}]}] From 46af99a4c4690d6a98b38090f99cb79d1bdd69fc Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 21:26:20 +0400 Subject: [PATCH 12/13] [#300]: Require composed document loaders to accept only `DocumentLoader` instances --- AGENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8ef1e48f..fd178e3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,11 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) for code style, linting (e.g. `make lint - For brand-new test files, prefer function-based pytest tests (module-level `def test_...`) over class-based tests. - Use descriptive test names that reflect behavior (e.g. `test_remote_context_via_link_alternate`). +## Document loaders + +- Class-based composed Document Loaders accept only `DocumentLoader` instances — never plain + callables, and never type as `DocumentLoader | Callable`. + ## Documentation - Put docs-specific CSS in `docs/stylesheets/extra.css` and register it via `extra_css` in `mkdocs.yml`. From e306a03b914658e84aa6166c9aa155563b156482 Mon Sep 17 00:00:00 2001 From: Anatoly Scherbakov Date: Sun, 26 Jul 2026 21:26:20 +0400 Subject: [PATCH 13/13] [#300]: Note that `ChoiceBySchemeDocumentLoader` dispatches URL strings --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32360737..b0d2dc01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ - `*Options` TypedDict types and a `Context` type alias in `pyld.options` for JSON-LD API option dicts (typing and documentation). - `pyld.SqliteCacheRequestsDocumentLoader`: a SQLite-backed HTTP cache document loader using `requests-cache`. - `pyld.FileDocumentLoader`: a document loader for local `file:` URLs with optional root confinement. -- `pyld.ChoiceBySchemeDocumentLoader`: a document loader that dispatches to per-scheme loaders. +- `pyld.ChoiceBySchemeDocumentLoader`: a document loader that dispatches URL + strings to per-scheme loaders. ### Fixed - If value objects contain array values for `@type` during expansion, an error is now raised. Fixes [expand#ter54](https://w3c.github.io/json-ld-api/tests/expand-manifest.html#ter54) and [toRdf#ter54](https://w3c.github.io/json-ld-api/tests/toRdf-manifest.html#ter54).