Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
at `pyld.identifier_issuer`.
- **BREAKING**: The classes `URDNA2015` and `URGNA2012` were moved to `canon.py`. They are now available
at `pyld.canon`.
- `jsonld.expand()` now accepts a `on_key_dropped` parameter which is a handler called on every ignored key.

## 2.0.4 - 2024-02-16

Expand Down
15 changes: 15 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ If Requests_ is not available, the loader is set to aiohttp_. The fallback
document loader is a dummy document loader that raises an exception on every
invocation.

Handling ignored keys during JSON-LD expansion
----------------------------------------------

If a key in a JSON-LD document does not map to an absolute IRI then it is ignored and logged by default.
You can customize this behaviour by passing a customizable handler to `on_key_dropped` parameter of `jsonld.expand()`.

For example, you can introduce a strict mode by raising a ValueError on every dropped key:

```python
def raise_this(value):
raise ValueError(value)
jsonld.expand(doc, None, on_key_dropped=raise_this)
```

Commercial Support
------------------

Expand Down
57 changes: 48 additions & 9 deletions lib/pyld/jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

import copy
import json
import logging
import re
import sys
from urllib.parse import urlparse
import warnings
import uuid

from typing import Optional, Callable
from pyld.canon import URDNA2015, URGNA2012, UnknownFormatError
from pyld.nquads import ParserError, parse_nquads, to_nquad, to_nquads
from pyld.identifier_issuer import IdentifierIssuer
Expand All @@ -34,6 +36,8 @@
from pyld.__about__ import (__copyright__, __license__, __version__)
from .iri_resolver import resolve, unresolve

logger = logging.getLogger('pyld.jsonld')

__all__ = [
'__copyright__', '__license__', '__version__',
'compact', 'expand', 'flatten', 'frame', 'link', 'from_rdf', 'to_rdf',
Expand Down Expand Up @@ -116,6 +120,17 @@
# Initial contexts, defined on first access
INITIAL_CONTEXTS = {}

# Handler to call if a key was dropped during expansion
OnKeyDropped = Callable[[Optional[str]], ...]


def log_on_key_dropped(key: Optional[str]):
"""Default behavior on ignored JSON-LD keys is to log them."""
logger.debug(
'Key `%s` was not mapped to an absolute IRI and was ignored.',
key,
)

def compact(input_, ctx, options=None):
"""
Performs JSON-LD compaction.
Expand All @@ -141,7 +156,7 @@ def compact(input_, ctx, options=None):
return JsonLdProcessor().compact(input_, ctx, options)


def expand(input_, options=None):
def expand(input_, options=None, on_key_dropped: OnKeyDropped = log_on_key_dropped):
"""
Performs JSON-LD expansion.

Expand All @@ -159,7 +174,9 @@ def expand(input_, options=None):

:return: the expanded JSON-LD output.
"""
return JsonLdProcessor().expand(input_, options)
return JsonLdProcessor(
on_key_dropped=on_key_dropped
).expand(input_, options)


def flatten(input_, ctx=None, options=None):
Expand Down Expand Up @@ -442,17 +459,18 @@ def unregister_rdf_parser(content_type):
del _rdf_parsers[content_type]


class JsonLdProcessor(object):
class JsonLdProcessor:
"""
A JSON-LD processor.
"""

def __init__(self):
def __init__(self, on_key_dropped: OnKeyDropped = log_on_key_dropped):
"""
Initialize the JSON-LD processor.
"""
# processor-specific RDF parsers
self.rdf_parsers = None
self.on_key_dropped = on_key_dropped

def compact(self, input_, ctx, options):
"""
Expand Down Expand Up @@ -2076,6 +2094,7 @@ def _expand_object(
not (
_is_absolute_iri(expanded_property) or
_is_keyword(expanded_property))):
self.on_key_dropped(expanded_property)
continue

if _is_keyword(expanded_property):
Expand Down Expand Up @@ -3266,18 +3285,31 @@ def _object_to_rdf(self, item, issuer, triples, rdfDirection):
datatype = item.get('@type')

# convert to XSD datatypes as appropriate
if item.get('@type') == '@json':
if datatype == '@json':
object['value'] = canonicalize(value).decode('UTF-8')
object['datatype'] = RDF_JSON_LITERAL
elif _is_bool(value):
object['value'] = 'true' if value else 'false'
object['datatype'] = datatype or XSD_BOOLEAN
elif _is_double(value) or datatype == XSD_DOUBLE:
elif _is_double(value):
# canonical double representation
object['value'] = re.sub(
r'(\d)0*E\+?0*(\d)', r'\1E\2',
('%1.15E' % value))
object['value'] = _canonicalize_double(value)
object['datatype'] = datatype or XSD_DOUBLE
return object
elif datatype == XSD_DOUBLE:
# Since the previous branch did not activate, we know that `value` is not a float number.
try:
float_value = float(value)
except (ValueError, TypeError):
# If `value` is not convertible to float, we will return it as-is.
object['value'] = value
object['datatype'] = XSD_DOUBLE
return object
else:
# We have a float, and canonicalization may proceed.
object['value'] = _canonicalize_double(float_value)
object['datatype'] = XSD_DOUBLE
return object
elif _is_integer(value):
object['value'] = str(value)
object['datatype'] = datatype or XSD_INTEGER
Expand Down Expand Up @@ -5382,6 +5414,13 @@ def _is_double(v):
return not isinstance(v, Integral) and isinstance(v, Real)


def _canonicalize_double(value: float) -> str:
"""Convert a float value to canonical lexical form of `xsd:double`."""
return re.sub(
r'(\d)0*E\+?0*(\d)', r'\1E\2',
('%1.15E' % value))


def _is_numeric(v):
"""
Returns True if the given value is numeric.
Expand Down
Loading
Loading