Skip to content

Commit

Permalink
fix: raise an exception when serialization format does not support quads
Browse files Browse the repository at this point in the history
- fixes <#2393>
  • Loading branch information
aucampia committed Jun 8, 2023
1 parent ace4acd commit 1745417
Show file tree
Hide file tree
Showing 19 changed files with 614 additions and 31 deletions.
30 changes: 30 additions & 0 deletions rdflib/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import logging

"""
Some utility functions.
Expand Down Expand Up @@ -614,3 +616,31 @@ def _iri2uri(iri: str) -> str:
uri += "#"

return uri


def _has_non_default_graphs(graph: rdflib.graph.ConjunctiveGraph) -> bool:
"""
Check if the value passed as `graph` contains graphs other than the default
graph.
The intent of this is to detect if the value passed can be serialized using
formats which do not support named graphs like N-Triples and Turtle.
Ideally this function would check if the supplied value contains any named
graphs, but RDFLib assigns a name to the default graph, so the best that can
be done is to check if the supplied graph contains any graphs other than the
default graph.
If the supplied value contains only the default graph and other graphs, this
function will return `False`, otherwise if the value passed contains at
least one graph other than the default graph it will return `True`.
"""
default_context = graph.default_context
logging.debug("default_context.identifier = %s", default_context.identifier)
for context_index, context in enumerate(graph.contexts()):
logging.debug("contexts[%s].identifier = %s", context_index, context.identifier)
if context.identifier != default_context.identifier:
return True
if context_index > 0:
return True
return False
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ exclude =
test/data/suites/, # does not contain python
test/jsonld/1.1/, # does not contain python
test/jsonld/test-suite/, # does not contain python
test/data/variants/, # does not contain python
test/data/translate_algebra/, # does not contain python
docs/rdf_terms.rst, # This file is causing an error on GitHub actions
extend-ignore =
Expand Down
4 changes: 2 additions & 2 deletions test/data.py → test/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rdflib import URIRef
from rdflib.graph import Graph

TEST_DIR = Path(__file__).parent
TEST_DIR = Path(__file__).parent.parent
TEST_DATA_DIR = TEST_DIR / "data"

alice_uri = URIRef("http://example.org/alice")
Expand All @@ -22,7 +22,7 @@
context2 = URIRef("urn:example:context-2")


simple_triple_graph = Graph().add(
SIMPLE_TRIPLE_GRAPH = Graph().add(
(
URIRef("http://example.org/subject"),
URIRef("http://example.org/predicate"),
Expand Down
8 changes: 4 additions & 4 deletions test/data/variants/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# multi variant graphs

This directory containts variants of the same graph encoded in different
This directory contains variants of the same graph encoded in different
formats, or differently in the same format.

The graph that a specific file is a variant of is determined by it's filename.
Files that differ only in file extention but have the same basename are
The graph that a specific file is a variant of is determined by its filename.
Files that differ only in file extensions but have the same basename are
considered variants of the same graph. Additionally, any suffix that matches
`-variant-[^/]*` is excluded when determening the graph key, so the following
`-variant-[^/]*` is excluded when determining the graph key, so the following
files are all considered variants of the same graph:

```
Expand Down
29 changes: 29 additions & 0 deletions test/data/variants/diverse_quads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from test.utils.namespace import EGDC, EGSCHEME, EGURN

from rdflib.graph import ConjunctiveGraph, Graph
from rdflib.namespace import XSD
from rdflib.term import Literal


def populate_graph(graph: Graph) -> None:
assert isinstance(graph, ConjunctiveGraph)

graph.add((EGSCHEME.subject, EGSCHEME.predicate, EGSCHEME.object))
graph.add((EGDC.subject, EGDC.predicate, Literal("typeless")))
graph.add((EGURN.subject, EGURN.predicate, EGURN.object))

egscheme_graph = graph.get_context(EGSCHEME.graph)
egscheme_graph.add((EGDC.subject, EGDC.predicate, Literal("日本語の表記体系", lang="jpx")))
egscheme_graph.add((EGURN.subject, EGSCHEME.predicate, EGSCHEME.subject))
egscheme_graph.add((EGSCHEME.subject, EGSCHEME.predicate, EGSCHEME.object))
egscheme_graph.add((EGSCHEME.subject, EGSCHEME.predicate, Literal(12)))

egurn_graph = graph.get_context(EGURN.graph)
egurn_graph.add((EGSCHEME.subject, EGSCHEME.predicate, EGSCHEME.object))
egurn_graph.add((EGSCHEME.subject, EGDC.predicate, EGDC.object))
egurn_graph.add(
(EGSCHEME.subject, EGDC.predicate, Literal("XSD string", datatype=XSD.string))
)


__all__ = ["populate_graph"]
5 changes: 5 additions & 0 deletions test/data/variants/diverse_triples.nq
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<http://example.com/subject> <http://example.com/predicate> "日本語の表記体系"@jpx .
<urn:example:subject> <example:predicate> <example:subject> .
<example:object> <http://example.com/predicate> "XSD string" .
<example:subject> <example:predicate> <example:object> .
<example:subject> <example:predicate> "12"^^<http://www.w3.org/2001/XMLSchema#integer> .
18 changes: 18 additions & 0 deletions test/data/variants/diverse_triples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from test.utils.namespace import EGDC, EGSCHEME, EGURN

from rdflib.graph import ConjunctiveGraph, Graph
from rdflib.namespace import XSD
from rdflib.term import Literal


def populate_graph(graph: Graph) -> None:
assert isinstance(graph, Graph)

graph.add((EGDC.subject, EGDC.predicate, Literal("日本語の表記体系", lang="jpx")))
graph.add((EGURN.subject, EGSCHEME.predicate, EGSCHEME.subject))

graph.add((EGSCHEME.object, EGDC.predicate, Literal("XSD string")))
graph.add((EGSCHEME.subject, EGSCHEME.predicate, EGSCHEME.object))
graph.add((EGSCHEME.subject, EGSCHEME.predicate, Literal(12)))

__all__ = ["populate_graph"]
15 changes: 15 additions & 0 deletions test/data/variants/diverse_triples.trig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@prefix eghttp: <http://example.com/> .
@prefix egurn: <urn:example:> .
@prefix egschema: <example:> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

{
eghttp:subject eghttp:predicate "日本語の表記体系"@jpx .

egurn:subject egschema:predicate egschema:subject .
}

egschema:object eghttp:predicate "XSD string"^^xsd:string .

egschema:subject egschema:predicate egschema:object,
12 .
13 changes: 13 additions & 0 deletions test/data/variants/simple_quad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from test.utils.namespace import EGDO

from rdflib.graph import ConjunctiveGraph, Graph


def populate_graph(graph: Graph) -> None:
assert isinstance(graph, ConjunctiveGraph)

egdo_graph = graph.get_context(EGDO.graph)
egdo_graph.add((EGDO.subject, EGDO.predicate, EGDO.object))


__all__ = ["populate_graph"]
10 changes: 10 additions & 0 deletions test/data/variants/simple_triple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from test.utils.namespace import EGDO

from rdflib.graph import Graph


def populate_graph(graph: Graph) -> None:
graph.add((EGDO.subject, EGDO.predicate, EGDO.object))


__all__ = ["populate_graph"]
2 changes: 2 additions & 0 deletions test/data/variants/simple_triple.trig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<http://example.org/subject>
<http://example.org/predicate> <http://example.org/object> .
7 changes: 5 additions & 2 deletions test/test_graph/test_graph_redirect.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from test.data import TEST_DATA_DIR, simple_triple_graph
from test.data import TEST_DATA_DIR
from test.utils import GraphHelper
from test.utils.http import MethodName, MockHTTPResponse
from test.utils.httpservermock import ServedBaseHTTPServerMock
from test.utils.variants import load_pyvariant
from typing import Tuple
from urllib.parse import urlparse

from rdflib.graph import Graph

SIMPLE_TRIPLE_GRAPH = load_pyvariant("simple_triple", Graph)


def test_graph_redirect_new_host(
function_httpmocks: Tuple[ServedBaseHTTPServerMock, ServedBaseHTTPServerMock]
Expand Down Expand Up @@ -38,7 +41,7 @@ def test_graph_redirect_new_host(

graph = Graph()
graph.parse(location=f"{mock_a.url}/a/data.ttl")
GraphHelper.assert_sets_equals(graph, simple_triple_graph)
GraphHelper.assert_sets_equals(graph, SIMPLE_TRIPLE_GRAPH)
for mock in function_httpmocks:
assert 1 == len(mock.requests[MethodName.GET])
for request in mock.requests[MethodName.GET]:
Expand Down
36 changes: 33 additions & 3 deletions test/test_graph/test_variants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import os
import re
from dataclasses import dataclass, field
from importlib import import_module
from pathlib import Path, PurePath
from test.data import TEST_DATA_DIR
from test.utils import GraphHelper
from test.utils.graph import load_sources
from typing import (
ClassVar,
Collection,
Expand All @@ -27,11 +29,13 @@

import rdflib.compare
import rdflib.util
from rdflib.graph import ConjunctiveGraph
from rdflib.graph import ConjunctiveGraph, Graph
from rdflib.namespace import XSD
from rdflib.term import URIRef
from rdflib.util import guess_format

MODULE_PATH = Path(__file__).parent

TEST_DIR = Path(__file__).parent.parent.absolute()
VARIANTS_DIR = TEST_DATA_DIR / "variants"

Expand Down Expand Up @@ -86,11 +90,27 @@ class GraphVariants:
key: str
variants: Dict[str, Path] = field(default_factory=OrderedDict)
asserts: GraphAsserts = field(default_factory=lambda: GraphAsserts())
pyvariant: Optional[Path] = None

_variant_regex: ClassVar[Pattern[str]] = re.compile(
r"^(.*?)(|[-]variant-[^/]+|[-]asserts)$"
)

def parse_pyvariant(self, graph: Graph) -> None:
"""
Load a graph from a Python file.
"""
assert self.pyvariant is not None

relative_path = self.pyvariant.relative_to(MODULE_PATH.parent.parent)

logging.debug("relative_path = %s", relative_path)
module_name = ".".join([*relative_path.parts[:-1], relative_path.stem])
logging.debug("module_name = %s", module_name)
module = import_module(module_name)
logging.debug("module = %s", module)
module.populate_graph(graph)

def pytest_param(
self,
marks: Optional[
Expand Down Expand Up @@ -123,13 +143,14 @@ def for_files(
for file_path in file_paths:
logging.debug("file_path = %s", file_path)
file_key, variant_key = cls._decompose_path(file_path, basedir)
# file_key = f"{file_path.parent / stem}"
if file_key not in graph_varaint_dict:
graph_variant = graph_varaint_dict[file_key] = GraphVariants(file_key)
else:
graph_variant = graph_varaint_dict[file_key]
if variant_key.endswith("-asserts.json"):
graph_variant.asserts = GraphAsserts.from_path(file_path)
elif variant_key.endswith(".py"):
graph_variant.pyvariant = file_path
else:
graph_variant.variants[variant_key] = file_path
return graph_varaint_dict
Expand All @@ -139,7 +160,9 @@ def for_directory(
cls, directory: Path, basedir: Optional[Path] = None
) -> Dict[str, "GraphVariants"]:
file_paths = []
for file_path in directory.glob("**/*"):
logging.debug("directory = %s, basedir = %s", directory, basedir)
for file_path in directory.glob("*"):
logging.debug("file_path = %s", file_path)
if not file_path.is_file():
continue
if file_path.name.endswith(".md"):
Expand Down Expand Up @@ -227,6 +250,13 @@ def test_variants(graph_variant: GraphVariants) -> None:
first_path: Optional[Path] = None
logging.debug("graph_variant.asserts = %s", graph_variant.asserts)

if graph_variant.pyvariant is not None:
first_path = graph_variant.pyvariant
logging.debug("loading pyvariant = %s", graph_variant.pyvariant)
first_graph = ConjunctiveGraph(identifier=public_id)
# graph_variant.parse_pyvariant(first_graph)
load_sources(first_path, graph=first_graph)

for variant_key, variant_path in graph_variant.variants.items():
logging.debug("variant_path = %s", variant_path)
format = guess_format(variant_path.name, fmap=SUFFIX_FORMAT_MAP)
Expand Down
53 changes: 53 additions & 0 deletions test/test_serializers/test_serialize_context_aware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import itertools
import logging
from test.utils.graph import GRAPH_FORMATS, GraphType
from test.utils.variants import load_pyvariant
from typing import Dict, Iterable, Type

import pytest
from _pytest.mark.structures import ParameterSet
from _pytest.outcomes import Failed

from rdflib.graph import ConjunctiveGraph, Dataset, Graph


def make_quads_in_triples_cases() -> Iterable[ParameterSet]:
"""
Generate test cases for serializing named graphs (i.e. quads) into a format
that does not support named graphs.
"""
triple_only_formats = [
graph_format
for graph_format in GRAPH_FORMATS
if graph_format.info.graph_types == {GraphType.TRIPLE}
]
for graph_type, graph_format in itertools.product(
(ConjunctiveGraph, Dataset), triple_only_formats
):
for serializer in graph_format.info.serializers:
yield pytest.param(
graph_type, serializer, marks=pytest.mark.xfail(raises=Failed)
)


CONJUNCTIVE_GRAPH_WITH_QUADS = load_pyvariant("diverse_quads", ConjunctiveGraph)
DATASET_WITH_QUADS = load_pyvariant("diverse_quads", Dataset)

GRAPHS: Dict[Type[Graph], Graph] = {
ConjunctiveGraph: CONJUNCTIVE_GRAPH_WITH_QUADS,
Dataset: DATASET_WITH_QUADS,
}


@pytest.mark.parametrize(["graph_type", "serializer"], make_quads_in_triples_cases())
def test_quads_in_triples(graph_type: Type[ConjunctiveGraph], serializer: str) -> None:
"""
Serializing named graphs (i.e. quads) inside a `ConjunctiveGraph` into a
format that does not support named graphs should result in an exception.
"""
graph = GRAPHS[graph_type]
assert type(graph) is graph_type
with pytest.raises(Exception) as caught:
graph.serialize(format=serializer)

logging.debug("caught.value = %r", caught.value, exc_info=caught.value)
Loading

0 comments on commit 1745417

Please sign in to comment.