Skip to content
Merged
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
9 changes: 6 additions & 3 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Test PyShEx
env:
UV_VERSION: "0.7.13"
UV_VERSION: "0.12.18"
on:
push:
branches:
- main
- master
pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -47,6 +47,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: true

- name: Install uv
uses: astral-sh/setup-uv@v7
Expand All @@ -66,6 +67,8 @@ jobs:
- name: Generate coverage results
# Set bash shell to fail correctly on Windows https://github.com/actions/runner-images/issues/6668
shell: bash
env:
SKIP_EXTERNAL_URLS: "true"
run: |
uv run coverage run -m pytest
uv run coverage xml
Expand All @@ -76,5 +79,5 @@ jobs:
with:
name: codecov-results-${{ matrix.os }}-${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
file: coverage.xml
files: coverage.xml
fail_ci_if_error: false
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "tests/data/shexTest"]
path = tests/data/shexTest
url = git@github.com:shexSpec/shexTest.git
url = https://github.com/shexSpec/shexTest.git
2 changes: 1 addition & 1 deletion ancilliary/earlreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _add_result(self, entry: BNode, status: bool) -> None:
rslt = BNode()
self.add(rslt, RDF.type, EARL.TestResult)\
.add(rslt, EARL.outcome, EARL[status])\
.add(rslt, DC.date, Literal(datetime.datetime.utcnow().isoformat()))\
.add(rslt, DC.date, Literal(datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).isoformat()))\
.add(entry, EARL.result, rslt)

def __str__(self) -> str:
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ dev = [
"coverage",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
# tests/data holds test *data* (including the shexTest submodule, which ships
# its own pytest suite) — never collect from it
addopts = "--ignore=tests/data"

[tool.black]
line-length = 120
target-version = ["py310", "py311", "py312", "py313", "py314"]
Expand Down
17 changes: 13 additions & 4 deletions pyshex/prefixlib.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re

from pyshexc.parser_impl.generate_shexj import load_shex_file
from pyshex.utils.schema_loader import load_shex_file
from rdflib import Namespace, Graph, RDF, RDFS, XSD, URIRef, __version__
from rdflib.namespace import DOAP, FOAF, DC, DCTERMS, SKOS, OWL, XMLNS
if __version__.startswith("5."):
Expand Down Expand Up @@ -66,14 +66,23 @@ def add_shex(self, schema: str) -> "PrefixLibrary":

def add_rdf(self, rdf: str | Graph, format: str | None = "turtle") -> "PrefixLibrary":
if not isinstance(rdf, Graph):
g = Graph()
# "core" keeps the pre-rdflib-6 binding set (rdf, rdfs, xsd, ...) rather
# than the ~30 namespaces rdflib now binds by default. The json-ld
# parser re-binds the full default set mid-parse, so for that format
# anything beyond core that matches a default binding is dropped —
# at the cost of losing @context prefixes identical to a default.
g = Graph(bind_namespaces="core")
injectable = set(Graph().namespace_manager.namespaces()) - \
set(g.namespace_manager.namespaces()) if format and 'json-ld' in format else set()
if '\n' in rdf or '\r' in rdf or ' ' in rdf:
g.parse(data=rdf, format=format)
else:
g.parse(rdf, format=format)
namespaces = [(k, v) for k, v in g.namespace_manager.namespaces()
if (k, v) not in injectable]
else:
g = rdf
for k, v in g.namespace_manager.namespaces():
namespaces = list(rdf.namespace_manager.namespaces())
for k, v in namespaces:
setattr(self, k.upper(), Namespace(v))
return self

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(self, ts: Iterator[RDFTriple] | Iterator[Triple] | None = None) ->
def __str__(self) -> str:
g = Graph()
[g.add((e.s, e.p, e.o)) for e in self]
return re.sub(r'^@prefix.*', '', g.serialize(format="turtle").decode(), flags=re.MULTILINE).strip()
return re.sub(r'^@prefix.*', '', g.serialize(format="turtle"), flags=re.MULTILINE).strip()

def add_triples(self, triples: Iterator[Triple]):
super().update([RDFTriple(t) for t in triples])
2 changes: 1 addition & 1 deletion pyshex/shex_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def rdf(self) -> str:

:return: The rendering of whatever RDF is currently being evaluated
"""
return self.g.serialize(format=self.rdf_format).decode()
return self.g.serialize(format=self.rdf_format)

@rdf.setter
def rdf(self, rdf: str | Graph | None) -> None:
Expand Down
36 changes: 34 additions & 2 deletions pyshex/utils/schema_loader.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
import codecs
import os
import re
from pathlib import PureWindowsPath
from typing import cast, TextIO
from urllib import request

import chardet

from ShExJSG import ShExJ
from pyjsg.jsglib import loads
from pyshexc.parser_impl import generate_shexj
from pyshexc.parser_impl.generate_shexj import load_shex_file


def _base_uri(loc: str | None) -> str | None:
""" A Windows filesystem path is not a legal IRI (drive letter, backslashes):
convert it to a file:/// URI before it is used as a document base. POSIX paths
are legal IRI references and are left untouched. """
if loc and (re.match(r'^[A-Za-z]:[/\\]', loc) or loc.startswith('\\\\')):
return PureWindowsPath(loc).as_uri()
return loc


def load_shex_file(shexfilename: str) -> str:
""" Read a ShEx file or URL, honoring a UTF-8 BOM and otherwise guessing the encoding.

Same as pyshexc.parser_impl.generate_shexj.load_shex_file, except that it falls back to
UTF-8 when chardet (>= 7) reports no encoding, instead of crashing in bytes.decode(None).
"""
if '://' in shexfilename:
with request.urlopen(shexfilename) as response:
data = response.read()
else:
with open(shexfilename, 'rb') as inf:
data = inf.read()
if data.startswith(codecs.BOM_UTF8):
return data.decode('utf-8-sig')
result = chardet.detect(data)
encoding = result['encoding'] if result['encoding'] and float(result['confidence'] or 0) > 0.9 else 'UTF-8'
return data.decode(encoding)


class SchemaLoader:
Expand Down Expand Up @@ -54,7 +86,7 @@ def loads(self, schema_txt: str) -> ShExJ.Schema:
# TODO: figure out how to propagate self.base_location into this parse
return cast(ShExJ.Schema, loads(schema_txt, ShExJ))
else:
return generate_shexj.parse(schema_txt, self.base_location)
return generate_shexj.parse(schema_txt, _base_uri(self.base_location))

def location_rewrite(self, schema_location: str) -> str:
if self.root_location is not None and self.redirect_location is not None:
Expand Down
7 changes: 5 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

# True means that we skip all tests that go outside our own environment (e.g. wikidata, etc)
# You can set this to True, False or base it on the present of a file in the root directory called "tests/data/SKIP_EXTERNAL_URLS"
SKIP_EXTERNAL_URLS = os.environ.get('SKIP_EXTERNAL_URLS', None)
# Must be a real bool: a string handed to pytest.mark.skipif gets eval'ed as a Python expression
_skip_env = os.environ.get('SKIP_EXTERNAL_URLS', None)
SKIP_EXTERNAL_URLS_MSG = "External url's are not tested - set tests.__init__.py.SKIP_EXTERNAL_URLS to False to run"

datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data'))
if SKIP_EXTERNAL_URLS is None:
if _skip_env is None:
SKIP_EXTERNAL_URLS = os.path.exists(os.path.join(datadir, 'SKIP_EXTERNAL_URLS'))
else:
SKIP_EXTERNAL_URLS = _skip_env.lower() not in ('', '0', 'false', 'no')

print("Skipping external URL tests" if SKIP_EXTERNAL_URLS else "Including external URLs in tests")

Expand Down
Loading
Loading