Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- `pyld.FileDocumentLoader`: a document loader for local `file:` URLs with optional root confinement.
- `pyld.SchemeDirectedDocumentLoader`: a document loader that dispatches URL
strings to per-scheme loaders.
- `pyld.TypeDirectedDocumentLoader`: a document loader that dispatches by Python
input type (e.g. `pathlib.Path` vs `str`).

## 3.2.0 - 2026-08-17

Expand Down
42 changes: 42 additions & 0 deletions docs/examples/document_loaders/type_directed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import json
from pathlib import Path

from pyld import (
FileDocumentLoader,
FrozenDocumentLoader,
SchemeDirectedDocumentLoader,
TypeDirectedDocumentLoader,
jsonld,
)

person = (
Path(__file__).resolve().parent.parent / 'data' / 'person_remote_context.jsonld'
)

file_loader = FileDocumentLoader()
http_loader = FrozenDocumentLoader(
documents={
'https://example.com/context': {
'@context': {'name': 'http://schema.org/name'},
},
}
)
loader = TypeDirectedDocumentLoader(
Comment thread
anatoly-scherbakov marked this conversation as resolved.
{
Path: file_loader,
str: SchemeDirectedDocumentLoader(
file=file_loader,
http=http_loader,
https=http_loader,
),
}
)
remote = jsonld.load_document(person, options={'documentLoader': loader})
result = jsonld.expand(
remote['document'],
options={
'documentLoader': loader,
'base': remote['documentUrl'],
},
)
print(json.dumps(result, indent=2))
6 changes: 6 additions & 0 deletions docs/reference/document-loaders/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ class-based loaders for common cases and supports custom subclasses of

Delegate to another Document Loader based on the scheme of the URL, for instance, `file:` vs `https://`.

- [:material-shape-outline:{ .lg .middle } `TypeDirectedDocumentLoader`](type-directed.md)

---

Delegate by Python input type, for instance `pathlib.Path` vs URL `str`.

- [:material-code-braces:{ .lg .middle } __Custom Document Loaders__](custom.md)

---
Expand Down
23 changes: 23 additions & 0 deletions docs/reference/document-loaders/type-directed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
hide: [toc]
---
# :material-shape-outline: `TypeDirectedDocumentLoader`

::: pyld.TypeDirectedDocumentLoader
options:
show_root_heading: false
show_bases: false
heading_level: 3
members: false

`TypeDirectedDocumentLoader` dispatches a value to the loader registered for its Python type. Applications can register any types that represent distinct document locations or loading policies.

This example registers `Path` for local documents and `str` for URL values. A local document is loaded through `FileDocumentLoader`; its remote `@context` is a URL string, so the `str` registration delegates to a nested `SchemeDirectedDocumentLoader`.

=== "Example"

{{ example('document_loaders/type_directed.py', output_syntax='json', indent=4) }}

=== "person_remote_context.jsonld"

{{ example_data('data/person_remote_context.jsonld', indent=4) }}
2 changes: 2 additions & 0 deletions lib/pyld/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
from .documentloader.requests import RequestsDocumentLoader
from .documentloader.requests_sqlite_cache import SqliteCacheRequestsDocumentLoader
from .documentloader.scheme_directed import SchemeDirectedDocumentLoader
from .documentloader.type_directed import TypeDirectedDocumentLoader

__all__ = [
'AioHttpDocumentLoader',
'BUNDLED_CONTEXTS',
'SchemeDirectedDocumentLoader',
'TypeDirectedDocumentLoader',
'ContextResolver',
'DocumentLoader',
'FileDocumentLoader',
Expand Down
56 changes: 56 additions & 0 deletions lib/pyld/documentloader/type_directed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Type-dispatching JSON-LD document loader.

.. module:: jsonld.documentloader.type_directed
:synopsis: TypeDirectedDocumentLoader for type-based document loading
"""

from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any

from pyld.documentloader.base import DocumentLoader, RemoteDocument
from pyld.jsonld import JsonLdError


@dataclass
class TypeDirectedDocumentLoader(DocumentLoader):
"""Document loader that dispatches to per-type loaders.

Constructed with a mapping from Python types to `DocumentLoader` instances.
Dispatch uses `isinstance`, so a `pathlib.Path` registration matches
`pathlib.PosixPath` / `WindowsPath`. The first matching entry in insertion
order wins.

An unregistered input type raises `JsonLdError` with code
`loading document failed` and details naming the registered types.

:param loaders: mapping of type to document loader.
"""

loaders: Mapping[type, DocumentLoader]

def __call__(self, url: Any, options: dict | None = None) -> RemoteDocument:
"""Retrieve the JSON-LD document at `url` via the matching type loader.

:param url: the URL, path, or other location to retrieve.
:param options: loader options forwarded to the chosen loader.
:return: a `RemoteDocument`.
"""
if options is None:
options = {}

for typ, loader in self.loaders.items():
if isinstance(url, typ):
return loader(url, options)

raise JsonLdError(
'URL could not be dereferenced; no loader is registered for '
f'type "{type(url).__name__}".',
'jsonld.InvalidUrl',
{
'url': url,
'types': [typ.__name__ for typ in self.loaders],
},
code='loading document failed',
)
116 changes: 116 additions & 0 deletions tests/test_type_directed_document_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Tests for TypeDirectedDocumentLoader."""

import json
from pathlib import Path

import pytest

from pyld import (
FileDocumentLoader,
FrozenDocumentLoader,
SchemeDirectedDocumentLoader,
TypeDirectedDocumentLoader,
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 test_dispatches_path_to_file_loader(tmp_path):
"""A pathlib.Path is dispatched to the Path loader."""
path = tmp_path / 'person.jsonld'
path.write_text(json.dumps(_PERSON), encoding='utf-8')
loader = TypeDirectedDocumentLoader(
{
Path: FileDocumentLoader(),
}
)

result = loader(path, {})

assert result['contentType'] == 'application/ld+json'
assert json.loads(result['document']) == _PERSON
assert result['documentUrl'] == path.resolve().as_uri()


def test_path_subclass_matches_path_registration(tmp_path):
"""A concrete Path subclass matches a Path registration via isinstance."""
path = tmp_path / 'person.jsonld'
path.write_text(json.dumps(_PERSON), encoding='utf-8')
assert type(path) is not Path
assert isinstance(path, Path)
loader = TypeDirectedDocumentLoader(
{
Path: FileDocumentLoader(),
}
)

result = loader(path, {})

assert json.loads(result['document']) == _PERSON


def test_dispatches_str_to_nested_scheme_loader():
"""A str URL is dispatched to the str loader (e.g. by-scheme)."""
http = FrozenDocumentLoader(documents={_CONTEXT_URL: _CONTEXT})
loader = TypeDirectedDocumentLoader(
{
str: SchemeDirectedDocumentLoader(https=http),
}
)

result = loader(_CONTEXT_URL, {})

assert result['document'] == _CONTEXT
assert result['documentUrl'] == _CONTEXT_URL


def test_unregistered_type_raises():
"""An unregistered input type raises JsonLdError naming registered types."""
loader = TypeDirectedDocumentLoader(
{
Path: 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['types'] == ['Path']


def test_load_path_with_remote_context(tmp_path):
"""Load a Path whose @context is an https URL via composed loaders."""
path = tmp_path / 'person.jsonld'
path.write_text(json.dumps(_PERSON), encoding='utf-8')
file_loader = FileDocumentLoader()
http = FrozenDocumentLoader(documents={_CONTEXT_URL: _CONTEXT})
loader = TypeDirectedDocumentLoader(
Comment thread
anatoly-scherbakov marked this conversation as resolved.
{
Path: file_loader,
str: SchemeDirectedDocumentLoader(
file=file_loader,
http=http,
https=http,
),
}
)

remote = jsonld.load_document(path, options={'documentLoader': loader})
expanded = jsonld.expand(
remote['document'],
options={
'documentLoader': loader,
'base': remote['documentUrl'],
},
)

assert expanded == [{'http://schema.org/name': [{'@value': 'Ada Lovelace'}]}]
Loading