Skip to content

Commit

Permalink
Relaxed the Max Evaluation Depth from 28 to 30, we were seeing some r…
Browse files Browse the repository at this point in the history
…eal-world cases where meta-shacl was failing on large Shapes Graphs at 28 levels deep.

sh:namespace values can now be xsd:anyURI or xsd:string or "literal string", but now cannot be <URI nodes>.
sh:order can now support xsd:decimal values and xsd:integer values, and can be interchanged at will.
  • Loading branch information
ashleysommer committed Feb 20, 2021
1 parent ce1488c commit 4fcf5d9
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 17 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Python PEP 440 Versioning](https://www.python.org/dev/peps/pep-0440/).

## [0.14.3] - 2021-02-20

## Changed
- Relaxed the Max Evaluation Depth from 28 to 30, we were seeing some real-world cases where meta-shacl was failing on large Shapes Graphs at 28 levels deep.
- sh:namespace values can now be xsd:anyURI or xsd:string or "literal string", but now cannot be <URI nodes>.
- sh:order can now support xsd:decimal values and xsd:integer values, and can be interchanged at will.


## [0.14.2] - 2021-01-02

## Added
Expand All @@ -12,7 +20,7 @@ and this project adheres to [Python PEP 440 Versioning](https://www.python.org/d
## Fixed
- Black and Flake8 issues outstanding from 0.14.1 release.
- Workaround a RDFLib bug trying to import `requests` when requests is not required to be installed.
- This bug will still be observed if you use SPARQLConstraints, SPARQLFunction or JSFunction features, but it can be worked around by simply installing `requests` in your python enviornment.
- This bug will still be observed if you use SPARQLConstraints, SPARQLFunction or JSFunction features, but it can be worked around by simply installing `requests` in your python environment.


## [0.14.1] - 2020-12-23
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "pyshacl"
version = "0.14.2"
version = "0.14.3"
# Don't forget to change the version number in __init__.py along with this one
description = "Python SHACL Validator"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion pyshacl/__init__.py
Expand Up @@ -6,7 +6,7 @@


# version compliant with https://www.python.org/dev/peps/pep-0440/
__version__ = '0.14.2'
__version__ = '0.14.3'
# Don't forget to change the version number in pyproject.toml along with this one

__all__ = ['validate', 'Validator', '__version__', 'Shape', 'ShapesGraph']
17 changes: 7 additions & 10 deletions pyshacl/helper/sparql_query_helper.py
Expand Up @@ -4,8 +4,6 @@
"""
import re

from warnings import warn

import rdflib

from rdflib import RDF, XSD
Expand Down Expand Up @@ -173,14 +171,13 @@ def collect_prefixes(self):
if prefix == "sh" and isinstance(namespace.value, str):
# Known bug in shacl.ttl https://github.com/w3c/data-shapes/issues/125
pass
elif namespace.language is not None or isinstance(namespace.value, str):
warn(
Warning(
"sh:namespace value must be an RDF Literal with type xsd:anyURI.\nLiteral: \"{}\" type={}".format(
namespace.value, namespace.datatype or namespace.language
)
)
)
elif (
namespace.datatype == XSD.string
or namespace.language is not None
or isinstance(namespace.value, str)
):
# Its now possible for namespace to be xsd:string or string literal
pass
else:
raise ConstraintLoadError(
"sh:namespace value must be an RDF Literal with type xsd:anyURI.\nLiteral: {} type={}".format(
Expand Down
21 changes: 19 additions & 2 deletions pyshacl/parameter.py
@@ -1,3 +1,4 @@
from decimal import Decimal
from logging import Logger
from typing import Union

Expand Down Expand Up @@ -60,8 +61,24 @@ def __init__(self, sg, param_node, path=None, logger: Union[Logger, None] = None
"https://www.w3.org/TR/shacl-af/#functions-example",
)
else:
# TODO: check order is a literal with type Int
self.param_order = int(orders[0])
order = orders[0]
if not isinstance(order, Literal):
raise ConstraintLoadError(
"sh:order value must be a literal of type Decimal or Integer",
"https://www.w3.org/TR/shacl-af/#functions-example",
)
if isinstance(order.value, Decimal):
order = order.value
elif isinstance(order.value, int):
order = Decimal(order.value)
elif isinstance(order.value, float):
order = Decimal(str(order.value))
else:
raise ConstraintLoadError(
"sh:order value must be a literal of type Decimal or Integer",
"https://www.w3.org/TR/shacl-af/#functions-example",
)
self.param_order = order
optionals = list(sg.objects(self.node, SH_optional))
if len(optionals) < 1:
self.optional = False
Expand Down
15 changes: 13 additions & 2 deletions pyshacl/shape.py
Expand Up @@ -198,7 +198,17 @@ def order(self):
raise ShapeLoadError(
"A SHACL Shape must be a numeric literal.", "https://www.w3.org/TR/shacl-af/#rules-order"
)
return Decimal(order_node.value)
if isinstance(order_node.value, Decimal):
order = order_node.value
elif isinstance(order_node.value, int):
order = Decimal(order_node.value)
elif isinstance(order_node.value, float):
order = Decimal(str(order_node.value))
else:
raise ShapeLoadError(
"A SHACL Shape must be a numeric literal.", "https://www.w3.org/TR/shacl-af/#rules-order"
)
return order

def target_nodes(self):
return self.sg.graph.objects(self.node, SH_targetNode)
Expand Down Expand Up @@ -519,7 +529,8 @@ def validate(
return True, []
if _evaluation_path is None:
_evaluation_path = []
elif len(_evaluation_path) >= 28: # 27 is the depth required to successfully do the meta-shacl test
elif len(_evaluation_path) >= 30:
# 27 is the depth required to successfully do the meta-shacl test on shacl.ttl
path_str = "->".join((str(e) for e in _evaluation_path))
raise ReportableRuntimeError("Evaluation path too deep!\n{}".format(path_str))
# Lazy import here to avoid an import loop
Expand Down
66 changes: 66 additions & 0 deletions test/issues/bad_test_070.py
@@ -0,0 +1,66 @@
from rdflib import Graph

from pyshacl import validate


"""
https://github.com/RDFLib/pySHACL/issues/70
"""

data_text = """\
@prefix ns0: <http://www.opengis.net/ont/geosparql#> .
@prefix dc: <http://purl.org/dc/terms/> .
@prefix ns1: <http://qudt.org/schema/qudt/> .
@prefix sosa: <http://www.w3.org/ns/sosa/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix geo: <http://www.opengis.net/ont/geosparql#> .
@prefix sf: <http://www.opengis.net/ont/sf#> .
<http://www.w3id.org/afarcloud/pCoord?lat=45.75&amp;long=4.85>
ns0:asWKT "POINT(45.75 4.85)"^^ns0:wktLiteral ;
a <http://www.opengis.net/ont/sf#Point> .
<http://www.w3id.org/afarcloud/poi?lat=45.75&amp;long=4.85>
ns0:hasGeometry <http://www.w3id.org/afarcloud/pCoord?lat=45.75&amp;long=4.85> ;
a ns0:Feature .
<urn:afc:AS09:cropsManagement:TEC:soil:sen0022> a <https://json-ld.org/playground/AfarcloudSensors>, <https://json-ld.org/playground/SoilSensor> .
<urn:afc:AS09:sen0022:obs-1514810172/q1>
dc:identifier "q1" ;
ns1:numericValue "0.27121272683143616" ;
ns1:unit <http://qudt.org/vocab/unit/DEG_C> ;
a ns1:QuantityValue .
<urn:afc:AS09:sen0022:obs-1514810172>
a sosa:Observation ;
sosa:hasFeatureOfInterest <http://www.w3id.org/afarcloud/poi?lat=45.75&amp;long=4.85> ;
sosa:hasResult <urn:afc:AS09:sen0022:obs-1514810172/q1> ;
sosa:madeBySensor <urn:afc:AS09:cropsManagement:TEC:soil:sen0022> ;
sosa:observedProperty <http://www.w3id.org/afarcloud/soil_temperature> ;
sosa:resultTime "2018-01-01T12:36:12Z"^^xsd:dateTime .
"""
shacl_url = """https://raw.githubusercontent.com/rapw3k/DEMETER/master/models/SHACL/demeterAgriProfile-SHACL.ttl"""


def test_070():
g = Graph().parse(data=data_text, format="turtle")
try:
conforms, g, s = validate(
g,
shacl_graph=shacl_url,
shacl_graph_format="turtle",
abort_on_error=False,
meta_shacl=False,
debug=True,
advanced=True,
)
except Exception as e:
print(e)
raise
assert conforms


if __name__ == "__main__":
test_070()

0 comments on commit 4fcf5d9

Please sign in to comment.