diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f7bfa..bedb58a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ 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/). +## [Unreleased] + +- Nothing yet + + +## [0.17.1] - 2021-10-11 + +## Fixes +- Handle transitive subclasses when evaluating sh:targetClass - @gtfierro + - Fixes #96 +- Improve detection of RDF/XML files when loading unknown content + - Fixes #98 +- Imported type stubs and resolved ALL MyPy issues! (this was a big effort) +- Logic fixes in the dataset loader (thanks to inconsistencies exposed by MyPy) + +## Changed +- Add special cases to sh:dataclass constraint, when the given shape uses rdfs:Literal or rdfs:Dataclass as the dataclass to match on + - Fixes #71 + +## Added +- Add datashapes.org/schema as a built-in graph + - Fixes #98 +- Added ability to pass a TextIO or TextIOWrapper object into the dataset loader + ## [0.17.0.post1] - 2021-09-15 ## Notice @@ -828,7 +852,8 @@ just leaves the files open. Now it is up to the command-line client to close the - Initial version, limited functionality -[Unreleased]: https://github.com/RDFLib/pySHACL/compare/v0.17.0.post1...HEAD +[Unreleased]: https://github.com/RDFLib/pySHACL/compare/v0.17.1...HEAD +[0.17.1]: https://github.com/RDFLib/pySHACL/compare/v0.17.0.post1...v0.17.1 [0.17.0.post1]: https://github.com/RDFLib/pySHACL/compare/v0.17.0...v0.17.0.post1 [0.17.0]: https://github.com/RDFLib/pySHACL/compare/v0.16.2...v0.17.0 [0.16.2]: https://github.com/RDFLib/pySHACL/compare/v0.16.1...v0.16.2 diff --git a/CITATION.cff b/CITATION.cff index 2048cdc..0d64e37 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,8 +8,8 @@ authors: given-names: "Nicholas" orcid: "http://orcid.org/0000-0002-8742-7730" title: "pySHACL" -version: 0.17.0 +version: 0.17.1 doi: 10.5281/zenodo.4750840 license: Apache-2.0 -date-released: 2021-07-20 +date-released: 2021-10-11 url: "https://github.com/RDFLib/pySHACL" diff --git a/poetry.lock b/poetry.lock index 2c5f9da..52abfff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,7 +22,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "black" -version = "21.8b0" +version = "21.9b0" description = "The uncompromising code formatter." category = "main" optional = true @@ -50,7 +50,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "click" -version = "8.0.1" +version = "8.0.3" description = "Composable command line interface toolkit" category = "main" optional = true @@ -150,7 +150,7 @@ python-versions = "*" [[package]] name = "more-itertools" -version = "8.9.0" +version = "8.10.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false @@ -212,7 +212,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "2.3.0" +version = "2.4.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" optional = true @@ -254,7 +254,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyduktape2" -version = "0.4.1" +version = "0.4.2" description = "Python integration for the Duktape Javascript interpreter" category = "main" optional = true @@ -319,7 +319,7 @@ testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", [[package]] name = "rdflib" -version = "6.0.0" +version = "6.0.2" description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." category = "main" optional = false @@ -332,7 +332,7 @@ pyparsing = "*" [package.extras] docs = ["sphinx (<5)", "sphinxcontrib-apidoc"] html = ["html5lib"] -tests = ["html5lib", "networkx", "nose (==1.3.7)", "nose-timer", "coverage", "black (==21.7b0)", "flake8", "doctest-ignore-unicode (==0.1.2)"] +tests = ["html5lib", "networkx", "nose (==1.3.7)", "nose-timer", "coverage", "black (==21.6b0)", "flake8", "doctest-ignore-unicode (==0.1.2)"] [[package]] name = "rdflib-jsonld" @@ -347,7 +347,7 @@ rdflib = ">=4.2.2" [[package]] name = "regex" -version = "2021.8.28" +version = "2021.10.8" description = "Alternative regular expression module, to replace re." category = "main" optional = true @@ -377,6 +377,14 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "types-setuptools" +version = "57.4.0" +description = "Typing stubs for setuptools" +category = "dev" +optional = true +python-versions = "*" + [[package]] name = "typing-extensions" version = "3.10.0.2" @@ -395,7 +403,7 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.5.0" +version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -414,7 +422,7 @@ jsonld = ["rdflib-jsonld"] [metadata] lock-version = "1.1" python-versions = "^3.7.0" # Compatible python versions must be declared here -content-hash = "5bf14955f590c9a669d7423a337be3ceb41f901de145d48b5e400fee9ab2fd79" +content-hash = "77a0cbd9785ec0d0cd6a51bcec4eb379de29ad57e0e8183e2eb58f5d820b6433" [metadata.files] atomicwrites = [ @@ -426,12 +434,12 @@ attrs = [ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] black = [ - {file = "black-21.8b0-py3-none-any.whl", hash = "sha256:2a0f9a8c2b2a60dbcf1ccb058842fb22bdbbcb2f32c6cc02d9578f90b92ce8b7"}, - {file = "black-21.8b0.tar.gz", hash = "sha256:570608d28aa3af1792b98c4a337dbac6367877b47b12b88ab42095cfc1a627c2"}, + {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, + {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -533,8 +541,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.9.0.tar.gz", hash = "sha256:8c746e0d09871661520da4f1241ba6b908dc903839733c8203b552cffaf173bd"}, - {file = "more_itertools-8.9.0-py3-none-any.whl", hash = "sha256:70401259e46e216056367a0a6034ee3d3f95e0bf59d3aa6a4eb77837171ed996"}, + {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"}, + {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, ] mypy = [ {file = "mypy-0.800-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e1c84c65ff6d69fb42958ece5b1255394714e0aac4df5ffe151bc4fe19c7600a"}, @@ -577,8 +585,8 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, - {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -593,7 +601,7 @@ pycodestyle = [ {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pyduktape2 = [ - {file = "pyduktape2-0.4.1.tar.gz", hash = "sha256:7f07bdf2f1f198f4f5f240f6053898610125b3d654ad3070014c1f7348657614"}, + {file = "pyduktape2-0.4.2.tar.gz", hash = "sha256:4f6cbf7d251d6603651c24bed2f9455febafe27ca04da1fb0d49fb675e22d8af"}, ] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, @@ -612,54 +620,54 @@ pytest-cov = [ {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, ] rdflib = [ - {file = "rdflib-6.0.0-py3-none-any.whl", hash = "sha256:bb24f0058070d5843503e15b37c597bc3858d328d11acd9476efad3aa62f555d"}, - {file = "rdflib-6.0.0.tar.gz", hash = "sha256:7ce4d757eb26f4dd43205ec340d8c097f29e5adfe45d6ea20238c731dc679879"}, + {file = "rdflib-6.0.2-py3-none-any.whl", hash = "sha256:b7642daac8cdad1ba157fecb236f5d1b2aa1de64e714dcee80d65e2b794d88a6"}, + {file = "rdflib-6.0.2.tar.gz", hash = "sha256:6136ae056001474ee2aff5fc5b956e62a11c3a9c66bb0f3d9c0aaa5fbb56854e"}, ] rdflib-jsonld = [ {file = "rdflib-jsonld-0.5.0.tar.gz", hash = "sha256:4f7d55326405071c7bce9acf5484643bcb984eadb84a6503053367da207105ed"}, ] regex = [ - {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, - {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, - {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, - {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, - {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, - {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, - {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, - {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, - {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, - {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, - {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, - {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, - {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, - {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, - {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, - {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, + {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"}, + {file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"}, + {file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"}, + {file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"}, + {file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"}, + {file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"}, + {file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"}, + {file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"}, + {file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"}, + {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"}, + {file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"}, + {file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"}, + {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"}, + {file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"}, + {file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"}, + {file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -701,6 +709,10 @@ typed-ast = [ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] +types-setuptools = [ + {file = "types-setuptools-57.4.0.tar.gz", hash = "sha256:5034f81b237429c1dc0ad84d3e9015e74730400c4db2b4db40daba216d39289b"}, + {file = "types_setuptools-57.4.0-py3-none-any.whl", hash = "sha256:986630532705e8c77740b6d3cd86aaafd1e0ba6b04119c73c39dc4a67ceae579"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, @@ -711,6 +723,6 @@ wcwidth = [ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/pyproject.toml b/pyproject.toml index 26c316d..8026202 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyshacl" -version = "0.17.0.post2" +version = "0.17.1" # Don't forget to change the version number in __init__.py and CITATION.cff along with this one description = "Python SHACL Validator" license = "Apache-2.0" @@ -39,6 +39,7 @@ packages = [ include = [ { path = "pyshacl/assets/*.ttl", format = "sdist" }, + { path = "pyshacl/assets/*.py", format = "sdist" }, { path = "hooks/*", format = "sdist" }, { path = "MANIFEST.in", format = "sdist" }, { path = "pyproject.toml", format = "sdist" }, @@ -54,12 +55,14 @@ include = [ [tool.poetry.dependencies] python = "^3.7.0" # Compatible python versions must be declared here rdflib = ">=6.0.0,<7" -owlrl = "^5.2.3" +owlrl = "^5.2.3,<7" rdflib-jsonld = { version=">=0.4.0,<0.6", optional=true} pyduktape2 = {version="^0.4.1", optional=true} flake8 = {version="^3.8.0", optional=true} isort = {version="^5.7.0", optional=true} -black = {version="21.8b0", optional=true} +black = {version="21.9b0", optional=true} +mypy = {version="^0.800", optional=true} +types-setuptools = {version="*", optional=true} [tool.poetry.dev-dependencies] coverage = "^4.5" @@ -67,15 +70,16 @@ pytest = "^5.0" pytest-cov = "^2.8.1" flake8 = {version="^3.8.0", optional=true} isort = {version="^5.7.0", optional=true} -black = {version="21.8b0", optional=true} +black = {version="21.9b0", optional=true} mypy = {version="^0.800", optional=true} +types-setuptools = {version="*", optional=true} [tool.poetry.extras] jsonld = ["rdflib-jsonld"] js = ["pyduktape2"] dev-lint = ["isort", "black", "flake8"] -dev-type-checking = ["mypy"] +dev-type-checking = ["mypy", "types-setuptools"] [tool.poetry.scripts] pyshacl = "pyshacl.cli:main" @@ -85,7 +89,7 @@ from = {format = "poetry", path = "pyproject.toml"} to = {format = "setuppy", path = "setup.py"} [tool.black] -required-version = "21.8b0" +required-version = "21.9b0" line-length = "119" skip-string-normalization = true target-version = ['py37'] @@ -163,7 +167,7 @@ commands = [testenv:type-checking] commands_pre = poetry install -vv -n --no-root --extras "dev-type-checking" - poetry run pip3 install "mypy>=0.800" + poetry run pip3 install "mypy>=0.800" "types-setuptools" commands = - poetry show poetry run python3 -m mypy --ignore-missing-imports pyshacl diff --git a/pyshacl/__init__.py b/pyshacl/__init__.py index a0c252e..33819ac 100644 --- a/pyshacl/__init__.py +++ b/pyshacl/__init__.py @@ -6,7 +6,7 @@ # version compliant with https://www.python.org/dev/peps/pep-0440/ -__version__ = '0.17.0.post2' +__version__ = '0.17.1' # Don't forget to change the version number in pyproject.toml and CITATION.cff along with this one __all__ = ['validate', 'Validator', '__version__', 'Shape', 'ShapesGraph'] diff --git a/pyshacl/helper/expression_helper.py b/pyshacl/helper/expression_helper.py index b2e6b0b..3ba049f 100644 --- a/pyshacl/helper/expression_helper.py +++ b/pyshacl/helper/expression_helper.py @@ -134,7 +134,7 @@ def value_nodes_from_path(sg, focus, path_val, target_graph, recursion=0): def nodes_from_node_expression( expr, focus_node, data_graph: 'GraphLike', sg: 'ShapesGraph', recurse_depth=0 -) -> Union[Set['RDFNode'], List['RDFNode']]: +) -> Union[Set[Union['RDFNode', None]], List[Union['RDFNode', None]]]: # https://www.w3.org/TR/shacl-af/#node-expressions if expr == SH_this: return [focus_node] @@ -151,7 +151,7 @@ def nodes_from_node_expression( if len(unions): union_list = next(iter(unions)) parts = list(sg.graph.items(union_list)) - all_nodes: Set['RDFNode'] = set() + all_nodes: Set[Union['RDFNode', None]] = set() for p in parts: new_parts = nodes_from_node_expression(p, focus_node, data_graph, sg, recurse_depth=recurse_depth + 1) all_nodes = all_nodes.union(new_parts) @@ -159,7 +159,7 @@ def nodes_from_node_expression( if len(intersections): inter_list = next(iter(intersections)) parts = list(data_graph.items(inter_list)) - inter_nodes: Set[RDFNode] = set() + inter_nodes: Set[Union['RDFNode', None]] = set() new = True for p in parts: new_parts = nodes_from_node_expression(p, focus_node, data_graph, sg, recurse_depth=recurse_depth + 1) @@ -221,7 +221,7 @@ def nodes_from_node_expression( "The SHACLFunction {} was not defined in this SHACL Shapes file.".format(fnexpr) ) argslist_parts = list(sg.graph.items(fnargslist)) - args_sets = [ + args_sets: List[Union[List[Union['RDFNode', None]], Set[Union['RDFNode', None]]]] = [ nodes_from_node_expression(p, focus_node, data_graph, sg, recurse_depth=recurse_depth + 1) for p in argslist_parts ] diff --git a/pyshacl/monkey/__init__.py b/pyshacl/monkey/__init__.py index 0771c57..72242aa 100644 --- a/pyshacl/monkey/__init__.py +++ b/pyshacl/monkey/__init__.py @@ -11,6 +11,7 @@ RDFLIB_421 = LooseVersion("4.2.1") RDFLIB_500 = LooseVersion("5.0.0") RDFLIB_600 = LooseVersion("6.0.0") +RDFLIB_602 = LooseVersion("6.0.2") def rdflib_bool_patch(): @@ -55,6 +56,12 @@ def __ge__(term, other): setattr(rdflib.term.Literal, "__le__", __le__) +def empty_iterator(): + if False: + # noinspection PyUnreachableCode + yield None # type: ignore[unreachable] + + def apply_patches(): if RDFLIB_421 >= RDFLIB_VERSION: rdflib_term_ge_le_patch() @@ -63,4 +70,7 @@ def apply_patches(): if RDFLIB_421 <= RDFLIB_VERSION < RDFLIB_600: # RDFLib 6.0.0+ comes with its own Memory2 store (called "Memory") by default plugin.register("default", store.Store, "pyshacl.monkey.memory2", "Memory2") + if RDFLIB_602 == RDFLIB_VERSION: + # Fixes https://github.com/RDFLib/rdflib/pull/1432 + setattr(store.Store, "namespaces", empty_iterator) return True diff --git a/pyshacl/rdfutil/clone.py b/pyshacl/rdfutil/clone.py index 0f367e4..45bf741 100644 --- a/pyshacl/rdfutil/clone.py +++ b/pyshacl/rdfutil/clone.py @@ -80,9 +80,11 @@ def mix_datasets( base_named_graphs = list(base_ds.contexts()) if target_ds is None: target_ds = rdflib.Dataset(default_union=default_union) + elif isinstance(target_ds, rdflib.ConjunctiveGraph): + raise RuntimeError("Cannot mix new graphs into a ConjunctiveGraph, use Dataset instead.") elif target_ds == "inplace": pass # do nothing here - elif not isinstance(target_ds, (rdflib.Dataset, rdflib.ConjunctiveGraph)): + elif not isinstance(target_ds, rdflib.Dataset): raise RuntimeError("Cannot mix datasets if target_ds passed in is not a Dataset itself.") if isinstance(extra_ds, (rdflib.Dataset, rdflib.ConjunctiveGraph)): mixin_graphs = list(extra_ds.contexts()) @@ -126,7 +128,9 @@ def mix_graphs(base_graph: GraphLike, extra_graph: GraphLike, target_graph: Opti :return: The cloned graph with mixed in triples from extra_graph :rtype: rdflib.Graph """ - if isinstance(base_graph, (rdflib.ConjunctiveGraph, rdflib.Dataset)): + if isinstance(base_graph, (rdflib.ConjunctiveGraph, rdflib.Dataset)) and isinstance( + target_graph, (rdflib.ConjunctiveGraph, rdflib.Dataset) + ): return mix_datasets(base_graph, extra_graph, target_ds=target_graph) if target_graph is None: g = clone_graph(base_graph, target_graph=None, identifier=base_graph.identifier) diff --git a/pyshacl/rdfutil/load.py b/pyshacl/rdfutil/load.py index ba70788..5eb4da0 100644 --- a/pyshacl/rdfutil/load.py +++ b/pyshacl/rdfutil/load.py @@ -3,7 +3,7 @@ import pickle import platform -from io import BytesIO, IOBase, UnsupportedOperation +from io import BufferedIOBase, BytesIO, TextIOBase, UnsupportedOperation from pathlib import Path from typing import BinaryIO, List, Optional, Union from urllib import request @@ -47,13 +47,16 @@ def get_rdf_from_web(url: Union[rdflib.URIRef, str]): else: return g, None + # Ask for everything we know about headers = {'Accept': 'text/turtle, application/rdf+xml, application/ld+json, application/n-triples, text/plain'} + known_format = None + r = request.Request(url, headers=headers) resp = request.urlopen(r) code = resp.getcode() if not (200 <= code <= 210): raise RuntimeError("Cannot pull RDF URL from the web: {}, code: {}".format(url, str(code))) - known_format = None + content_type = resp.headers.get('Content-Type', None) if content_type: if content_type.startswith("text/turtle"): @@ -70,7 +73,7 @@ def get_rdf_from_web(url: Union[rdflib.URIRef, str]): def load_from_source( - source: Union[GraphLike, BinaryIO, Union[str, bytes]], + source: Union[GraphLike, BufferedIOBase, TextIOBase, BinaryIO, Union[str, bytes]], g: Optional[GraphLike] = None, rdf_format: Optional[str] = None, multigraph: bool = False, @@ -81,7 +84,7 @@ def load_from_source( :param source: :param g: - :type g: rdflib.Graph + :type g: rdflib.Graph | None :param rdf_format: :type rdf_format: str :param multigraph: @@ -93,10 +96,11 @@ def load_from_source( :return: """ source_is_graph = False - source_is_open = False - source_was_open = False - source_is_file = False - source_is_bytes = False + open_source: Optional[Union[BufferedIOBase, BinaryIO]] = None + source_was_open: bool = False + source_as_file: Optional[Union[BufferedIOBase, BinaryIO]] = None + source_as_filename: Optional[str] = None + source_as_bytes: Optional[bytes] = None filename = None public_id = None uri_prefix = None @@ -107,27 +111,33 @@ def load_from_source( g = source else: raise RuntimeError("Cannot pass in both target=rdflib.Graph/Dataset and g=graph.") - elif isinstance(source, IOBase): - source_is_file = True + elif isinstance(source, (BufferedIOBase, TextIOBase)): + if hasattr(source, 'name'): + filename = source.name # type: ignore + public_id = Path(filename).resolve().as_uri() + "#" + if isinstance(source, TextIOBase): + buf = getattr(source, "buffer") # type: BufferedIOBase + source_as_file = source = buf + else: + source_as_file = source if hasattr(source, 'closed'): - source_is_open = not bool(source.closed) - source_was_open = source_is_open + if not bool(source.closed): + open_source = source + source_was_open = True else: # Assume it is open now and it was open when we started. - source_is_open = True + open_source = source source_was_open = True - if hasattr(source, 'name'): - filename = source.name # type: ignore - public_id = Path(filename).resolve().as_uri() + "#" + elif isinstance(source, str): if is_windows and source.startswith('file:///'): public_id = source - source_is_file = True filename = source[8:] + source_as_filename = filename elif not is_windows and source.startswith('file://'): public_id = source - source_is_file = True filename = source[7:] + source_as_filename = filename elif source.startswith('http:') or source.startswith('https:'): public_id = source try: @@ -142,17 +152,17 @@ def load_from_source( source_is_graph = True else: filename = resp.geturl() - source = resp.fp + fp = resp.fp # type: BufferedIOBase source_was_open = False - source_is_open = True + source = open_source = fp else: first_char = source[0] if is_windows and (first_char == '\\' or (len(source) > 3 and source[1:3] == ":\\")): - source_is_file = True filename = source + source_as_filename = filename elif first_char == '/' or source[0:3] == "./": - source_is_file = True filename = source + source_as_filename = filename elif ( first_char == '#' or first_char == '@' @@ -162,17 +172,17 @@ def load_from_source( or first_char == '[' ): # Contains some JSON or XML or Turtle stuff - source_is_file = False + source_as_file = None + source_as_filename = None elif len(source) < 140: - source_is_file = True filename = source + source_as_filename = filename # TODO: Do we still need this? Not sure why this was added, but works better without it # if public_id and not public_id.endswith('#'): # public_id = "{}#".format(public_id) - if not source_is_file and not source_is_open and isinstance(source, str): + if not source_as_file and not source_as_filename and not open_source and isinstance(source, str): # source is raw RDF data. - source = source.encode('utf-8') - source_is_bytes = True + source_as_bytes = source = source.encode('utf-8') elif isinstance(source, bytes): if source.startswith(b'file:') or source.startswith(b'http:') or source.startswith(b'https:'): raise ValueError("file:// and http:// strings should be given as str, not bytes.") @@ -186,22 +196,25 @@ def load_from_source( or first_char_b == b'[' ): # Contains some JSON or XML or Turtle stuff - source_is_file = False + source_as_file = None + source_as_filename = None elif len(source) < 140: filename = source.decode('utf-8') - source_is_file = True - if not source_is_file and not source_is_open: - source_is_bytes = True + source_as_filename = filename + if not source_as_file and not source_as_filename and not open_source: + source_as_bytes = source else: raise ValueError("Cannot determine the format of the input graph") if g is None: if source_is_graph: - g = source + target_g: Union[rdflib.Graph, rdflib.ConjunctiveGraph, rdflib.Dataset] = source # type: ignore else: - g = rdflib.Dataset() if multigraph else rdflib.Graph() + target_g = rdflib.Dataset() if multigraph else rdflib.Graph() else: if not isinstance(g, (rdflib.Graph, rdflib.Dataset, rdflib.ConjunctiveGraph)): - raise RuntimeError("Passing in g must be a Graph.") + raise RuntimeError("Passing in 'g' must be a rdflib Graph or Dataset.") + target_g = g + if filename: if filename.endswith('.ttl'): rdf_format = rdf_format or 'turtle' @@ -217,33 +230,44 @@ def load_from_source( rdf_format = rdf_format or 'trig' elif filename.endswith('.xml') or filename.endswith('.rdf'): rdf_format = rdf_format or 'xml' - if source_is_file and filename is not None and not source_is_open: + if source_as_filename and filename is not None and not open_source: filename = str(Path(filename).resolve()) if not public_id: public_id = Path(filename).as_uri() + "#" source = open(filename, mode='rb') - source_is_open = True - if not source_is_open and source_is_bytes: - source = BytesIO(source) - source_is_open = True - if source_is_open: + open_source = source + if not open_source and source_as_bytes: + source = BytesIO(source_as_bytes) # type: ignore + open_source = source + if open_source: + _source = open_source # Check if we can seek try: - source.seek(0) + _source.seek(0) # type: ignore except (AttributeError, UnsupportedOperation): # Read it all into memory - new_bytes = BytesIO(source.read()) + new_bytes = BytesIO(_source.read()) if not source_was_open: - source.close() - source = new_bytes + _source.close() + source = _source = new_bytes source_was_open = False + if rdf_format is None: + line = _source.readline().lstrip() + if len(line) > 15: + line = line[:15] + line = line.lower() + if line.startswith(b" 0 except AssertionError: break @@ -277,44 +301,44 @@ def load_from_source( wordval = wordval[1:] if len(wordval) < 1: continue - wordval = wordval.decode('utf-8') + wordval_str = wordval.decode('utf-8') if keyword == b"baseuri": - public_id = wordval + public_id = wordval_str elif keyword == b"prefix": - uri_prefix = wordval + uri_prefix = wordval_str try: - # The only way we can get here is if we were able to see before - source.seek(0) + # The only way we can get here is if we were able to seek before + _source.seek(0) except (AttributeError, UnsupportedOperation): raise RuntimeError("Seek failed while pre-parsing Turtle File.") except ValueError: raise RuntimeError("File closed while pre-parsing Turtle File.") - g.parse(source=source, format=rdf_format, publicID=public_id) + target_g.parse(source=_source, format=rdf_format, publicID=public_id) # If the target was open to begin with, leave it open. if not source_was_open: - source.close() - elif hasattr(source, 'seek'): + _source.close() + elif hasattr(_source, 'seek'): try: - source.seek(0) + _source.seek(0) except (AttributeError, UnsupportedOperation): pass except ValueError: # The parser closed our file! pass source_is_graph = True - elif source_is_graph and (g != source): + elif source_is_graph and (target_g != source): # clone source into g - if isinstance(g, (rdflib.Dataset, rdflib.ConjunctiveGraph)) and isinstance( + if isinstance(target_g, (rdflib.Dataset, rdflib.ConjunctiveGraph)) and isinstance( source, (rdflib.Dataset, rdflib.ConjunctiveGraph) ): - clone_dataset(source, g) - elif isinstance(g, rdflib.Graph) and isinstance(source, (rdflib.Dataset, rdflib.ConjunctiveGraph)): + clone_dataset(source, target_g) + elif isinstance(target_g, rdflib.Graph) and isinstance(source, (rdflib.Dataset, rdflib.ConjunctiveGraph)): raise RuntimeError("Cannot load a Dataset source into a Graph target.") - elif isinstance(g, (rdflib.Dataset, rdflib.ConjunctiveGraph)) and isinstance(source, rdflib.Graph): - target = rdflib.Graph(store=g.store, identifier=public_id) + elif isinstance(target_g, (rdflib.Dataset, rdflib.ConjunctiveGraph)) and isinstance(source, rdflib.Graph): + target = rdflib.Graph(store=target_g.store, identifier=public_id) clone_graph(source, target) - elif isinstance(g, rdflib.Graph) and isinstance(source, rdflib.Graph): - clone_graph(source, g) + elif isinstance(target_g, rdflib.Graph) and isinstance(source, rdflib.Graph): + clone_graph(source, target_g) else: raise RuntimeError("Cannot merge source graph into target graph.") @@ -327,32 +351,32 @@ def load_from_source( # Don't reassign blank prefix, when importing subgraph pass else: - has_named_prefix = g.store.namespace(uri_prefix) + has_named_prefix = target_g.store.namespace(uri_prefix) if not has_named_prefix: - g.namespace_manager.bind(uri_prefix, public_id) + target_g.namespace_manager.bind(uri_prefix, public_id) elif not is_imported_graph: - existing_blank_prefix = g.store.namespace('') + existing_blank_prefix = target_g.store.namespace('') if not existing_blank_prefix: - g.namespace_manager.bind('', public_id) + target_g.namespace_manager.bind('', public_id) if do_owl_imports: if isinstance(do_owl_imports, int): if do_owl_imports > 3: - return g + return target_g else: do_owl_imports = 1 if import_chain is None: import_chain = [] if public_id and (public_id.endswith('#') or public_id.endswith('/')): - root_id = rdflib.URIRef(public_id[:-1]) + root_id: Union[rdflib.URIRef, None] = rdflib.URIRef(public_id[:-1]) else: root_id = rdflib.URIRef(public_id) if public_id else None done_imports = 0 if root_id is not None: - if isinstance(g, (rdflib.ConjunctiveGraph, rdflib.Dataset)): - gs = list(g.contexts()) + if isinstance(target_g, (rdflib.ConjunctiveGraph, rdflib.Dataset)): + gs = list(target_g.contexts()) else: - gs = [g] + gs = [target_g] for ng in gs: owl_imports = list(ng.objects(root_id, rdflib.OWL.imports)) if len(owl_imports) > 0: @@ -361,15 +385,19 @@ def load_from_source( if o in import_chain: continue load_from_source( - o, g=g, multigraph=multigraph, do_owl_imports=do_owl_imports + 1, import_chain=import_chain + o, + g=target_g, + multigraph=multigraph, + do_owl_imports=do_owl_imports + 1, + import_chain=import_chain, ) done_imports += 1 if done_imports < 1 and public_id is not None and root_id != public_id: public_id_uri = rdflib.URIRef(public_id) - if isinstance(g, (rdflib.ConjunctiveGraph, rdflib.Dataset)): - gs = list(g.contexts()) + if isinstance(target_g, (rdflib.ConjunctiveGraph, rdflib.Dataset)): + gs = list(target_g.contexts()) else: - gs = [g] + gs = [target_g] for ng in gs: owl_imports = list(ng.objects(public_id_uri, rdflib.OWL.imports)) if len(owl_imports) > 0: @@ -378,14 +406,18 @@ def load_from_source( if o in import_chain: continue load_from_source( - o, g=g, multigraph=multigraph, do_owl_imports=do_owl_imports + 1, import_chain=import_chain + o, + g=target_g, + multigraph=multigraph, + do_owl_imports=do_owl_imports + 1, + import_chain=import_chain, ) done_imports += 1 if done_imports < 1: - if isinstance(g, (rdflib.ConjunctiveGraph, rdflib.Dataset)): - gs = list(g.contexts()) + if isinstance(target_g, (rdflib.ConjunctiveGraph, rdflib.Dataset)): + gs = list(target_g.contexts()) else: - gs = [g] + gs = [target_g] for ng in gs: ontologies = ng.subjects(rdflib.RDF.type, rdflib.OWL.Ontology) for ont in ontologies: @@ -400,7 +432,11 @@ def load_from_source( if o in import_chain: continue load_from_source( - o, g=g, multigraph=multigraph, do_owl_imports=do_owl_imports + 1, import_chain=import_chain + o, + g=target_g, + multigraph=multigraph, + do_owl_imports=do_owl_imports + 1, + import_chain=import_chain, ) done_imports += 1 - return g + return target_g diff --git a/pyshacl/rdfutil/stringify.py b/pyshacl/rdfutil/stringify.py index 11a05ec..5462676 100644 --- a/pyshacl/rdfutil/stringify.py +++ b/pyshacl/rdfutil/stringify.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from functools import wraps -from typing import List, Optional +from typing import List, Optional, Union import rdflib @@ -131,7 +131,7 @@ def find_node_named_graph(dataset, node): def stringify_node( graph: rdflib.Graph, node: rdflib.term.Identifier, - ns_manager: Optional[NamespaceManager] = None, + ns_manager: Optional[Union[NamespaceManager, rdflib.Graph]] = None, recursion: int = 0, ): if ns_manager is None: @@ -139,6 +139,8 @@ def stringify_node( if isinstance(ns_manager, rdflib.Graph): # json-ld loader can set namespace_manager to the conjunctive graph itself. ns_manager = ns_manager.namespace_manager + if ns_manager is None or isinstance(ns_manager, rdflib.Graph): + raise RuntimeError("Cannot stringify node, no namespaces known.") ns_manager.bind("sh", SH, override=False, replace=False) if isinstance(node, rdflib.Literal): return stringify_literal(graph, node, ns_manager=ns_manager) diff --git a/test/issues/test_040/__init__.py b/test/issues/test_040/__init__.py index 01b5978..6299795 100644 --- a/test/issues/test_040/__init__.py +++ b/test/issues/test_040/__init__.py @@ -7,9 +7,13 @@ from pyshacl import validate from pyshacl.rdfutil import load_from_source -data_graph = load_from_source("./test/issues/test_040/sample-network.ttl") + +with open("./test/issues/test_040/sample-network.ttl", "r") as f: + data_graph = load_from_source(f) + shacl_graph = load_from_source("./test/issues/test_040/03-Network.ttl") + def test_040(): conforms, g, s = validate(data_graph=data_graph, shacl_graph=shacl_graph, ont_graph=shacl_graph, inference='rdfs') assert conforms diff --git a/test/issues/test_098.py b/test/issues/test_098.py new file mode 100644 index 0000000..a959aa2 --- /dev/null +++ b/test/issues/test_098.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# +""" +https://github.com/RDFLib/pySHACL/issues/98 +""" +import os + +from pyshacl import validate + + +mixed_file_text = """\ +# baseURI: http://datashapes.org/sh/tests/core/complex/personexample.test +# imports: http://datashapes.org/schema +# prefix: ex + +@prefix dash: . +@prefix ex: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . + + + rdf:type owl:Ontology ; + rdfs:label "Test of personexample" ; + owl:imports ; +. +ex:Alice + rdf:type ex:Person ; + ex:ssn "987-65-432A" ; +. +ex:Bob + rdf:type ex:Person ; + ex:ssn "123-45-6789" ; + ex:ssn "124-35-6789" ; +. +ex:Calvin + rdf:type ex:Person ; + ex:birthDate "1999-09-09"^^xsd:date ; + ex:worksFor ex:UntypedCompany ; +. + +ex:PersonShape + rdf:type sh:NodeShape ; + sh:closed "true"^^xsd:boolean ; + sh:ignoredProperties ( + rdf:type + ) ; + sh:property [ + sh:path ex:ssn ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:pattern "^\\\\d{3}-\\\\d{2}-\\\\d{4}$" ; + sh:message "SSN must be 3 digits - 2 digits - 4 digits." + ] ; + sh:property [ + sh:path ex:worksFor ; + sh:class ex:Company ; + sh:nodeKind sh:IRI + ] ; + sh:property [ + sh:path [ + sh:inversePath ex:worksFor ; + ] ; + sh:name "employee" ; + ] ; + sh:targetClass ex:Person ; +. + +""" + + +def test_98(): + DEB_BUILD_ARCH = os.environ.get('DEB_BUILD_ARCH', None) + DEB_HOST_ARCH = os.environ.get('DEB_HOST_ARCH', None) + if DEB_BUILD_ARCH is not None or DEB_HOST_ARCH is not None: + print("Cannot run owl:imports in debhelper tests.") + assert True + return True + res1 = validate( + mixed_file_text, + shacl_graph=mixed_file_text, + data_graph_format='turtle', + shacl_graph_format='turtle', + do_owl_imports=True, + debug=True, + ) + conforms, _, _ = res1 + assert not conforms