summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIwan Aucamp <aucampia@gmail.com>2023-05-17 18:51:55 +0200
committerGitHub <noreply@github.com>2023-05-17 18:51:55 +0200
commite0b3152a799e7bb04770fcddf010b22c0b05379e (patch)
treebffb71170674bde9f9af2f70e51c490a6808132c
parent2c8d1e13b812a28078b0b31f58386057f54bedb4 (diff)
downloadrdflib-e0b3152a799e7bb04770fcddf010b22c0b05379e.tar.gz
fix: HTTP 308 Permanent Redirect status code handling (#2389)
Change the handling of HTTP status code 308 to behave more like `urllib.request.HTTPRedirectHandler`, most critically, the new 308 handling will create a new `urllib.request.Request` object with the new URL, which will prevent state from being carried over from the original request. One case where this is important is when the domain name changes, for example, when the original URL is `http://www.w3.org/ns/adms.ttl` and the redirect URL is `https://uri.semic.eu/w3c/ns/adms.ttl`. With the previous behaviour, the redirect would contain a `Host` header with the value `www.w3.org` instead of `uri.semic.eu` because the `Host` header is placed in `Request.unredirected_hdrs` and takes precedence over the `Host` header in `Request.headers`. Other changes: - Only handle HTTP status code 308 on Python versions before 3.11 as Python 3.11 will handle 308 by default [[ref](https://docs.python.org/3.11/whatsnew/changelog.html#id128)]. - Move code which uses `http://www.w3.org/ns/adms.ttl` and `http://www.w3.org/ns/adms.rdf` out of `test_guess_format_for_parse` into a separate parameterized test, which instead uses the embedded http server. This allows the test to fully control the `Content-Type` header in the response instead of relying on the value that the server is sending. This is needed because the server is sending `Content-Type: text/plain` for the `adms.ttl` file, which is not a valid RDF format, and the test is expecting `Content-Type: text/turtle`. Fixes: - <https://github.com/RDFLib/rdflib/issues/2382>.
-rw-r--r--rdflib/_networking.py117
-rw-r--r--rdflib/parser.py19
-rw-r--r--test/conftest.py34
-rw-r--r--test/data.py18
-rw-r--r--test/data/defined_namespaces/adms.rdf277
-rw-r--r--test/data/defined_namespaces/adms.ttl175
-rw-r--r--test/data/defined_namespaces/rdfs.rdf130
-rwxr-xr-xtest/data/fetcher.py15
-rw-r--r--test/test_graph/test_graph.py61
-rw-r--r--test/test_graph/test_graph_redirect.py45
-rw-r--r--test/test_misc/test_input_source.py17
-rw-r--r--test/test_misc/test_networking_redirect.py217
-rw-r--r--test/utils/exceptions.py29
-rw-r--r--test/utils/http.py9
-rw-r--r--test/utils/httpfileserver.py10
15 files changed, 1121 insertions, 52 deletions
diff --git a/rdflib/_networking.py b/rdflib/_networking.py
new file mode 100644
index 00000000..311096a8
--- /dev/null
+++ b/rdflib/_networking.py
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+import string
+import sys
+from typing import Dict
+from urllib.error import HTTPError
+from urllib.parse import quote as urlquote
+from urllib.parse import urljoin, urlsplit
+from urllib.request import HTTPRedirectHandler, Request, urlopen
+from urllib.response import addinfourl
+
+
+def _make_redirect_request(request: Request, http_error: HTTPError) -> Request:
+ """
+ Create a new request object for a redirected request.
+
+ The logic is based on `urllib.request.HTTPRedirectHandler` from `this commit <https://github.com/python/cpython/blob/b58bc8c2a9a316891a5ea1a0487aebfc86c2793a/Lib/urllib/request.py#L641-L751>_`.
+
+ :param request: The original request that resulted in the redirect.
+ :param http_error: The response to the original request that indicates a
+ redirect should occur and contains the new location.
+ :return: A new request object to the location indicated by the response.
+ :raises HTTPError: the supplied ``http_error`` if the redirect request
+ cannot be created.
+ :raises ValueError: If the response code is `None`.
+ :raises ValueError: If the response does not contain a ``Location`` header
+ or the ``Location`` header is not a string.
+ :raises HTTPError: If the scheme of the new location is not ``http``,
+ ``https``, or ``ftp``.
+ :raises HTTPError: If there are too many redirects or a redirect loop.
+ """
+ new_url = http_error.headers.get("Location")
+ if new_url is None:
+ raise http_error
+ if not isinstance(new_url, str):
+ raise ValueError(f"Location header {new_url!r} is not a string")
+
+ new_url_parts = urlsplit(new_url)
+
+ # For security reasons don't allow redirection to anything other than http,
+ # https or ftp.
+ if new_url_parts.scheme not in ("http", "https", "ftp", ""):
+ raise HTTPError(
+ new_url,
+ http_error.code,
+ f"{http_error.reason} - Redirection to url {new_url!r} is not allowed",
+ http_error.headers,
+ http_error.fp,
+ )
+
+ # http.client.parse_headers() decodes as ISO-8859-1. Recover the original
+ # bytes and percent-encode non-ASCII bytes, and any special characters such
+ # as the space.
+ new_url = urlquote(new_url, encoding="iso-8859-1", safe=string.punctuation)
+ new_url = urljoin(request.full_url, new_url)
+
+ # XXX Probably want to forget about the state of the current
+ # request, although that might interact poorly with other
+ # handlers that also use handler-specific request attributes
+ content_headers = ("content-length", "content-type")
+ newheaders = {
+ k: v for k, v in request.headers.items() if k.lower() not in content_headers
+ }
+ new_request = Request(
+ new_url,
+ headers=newheaders,
+ origin_req_host=request.origin_req_host,
+ unverifiable=True,
+ )
+
+ visited: Dict[str, int]
+ if hasattr(request, "redirect_dict"):
+ visited = request.redirect_dict
+ if (
+ visited.get(new_url, 0) >= HTTPRedirectHandler.max_repeats
+ or len(visited) >= HTTPRedirectHandler.max_redirections
+ ):
+ raise HTTPError(
+ request.full_url,
+ http_error.code,
+ HTTPRedirectHandler.inf_msg + http_error.reason,
+ http_error.headers,
+ http_error.fp,
+ )
+ else:
+ visited = {}
+ setattr(request, "redirect_dict", visited)
+
+ setattr(new_request, "redirect_dict", visited)
+ visited[new_url] = visited.get(new_url, 0) + 1
+ return new_request
+
+
+def _urlopen(request: Request) -> addinfourl:
+ """
+ This is a shim for `urlopen` that handles HTTP redirects with status code
+ 308 (Permanent Redirect).
+
+ This function should be removed once all supported versions of Python
+ handles the 308 HTTP status code.
+
+ :param request: The request to open.
+ :return: The response to the request.
+ """
+ try:
+ return urlopen(request)
+ except HTTPError as error:
+ if error.code == 308 and sys.version_info < (3, 11):
+ # HTTP response code 308 (Permanent Redirect) is not supported by python
+ # versions older than 3.11. See <https://bugs.python.org/issue40321> and
+ # <https://github.com/python/cpython/issues/84501> for more details.
+ # This custom error handling should be removed once all supported
+ # versions of Python handles 308.
+ new_request = _make_redirect_request(request, error)
+ return _urlopen(new_request)
+ else:
+ raise
diff --git a/rdflib/parser.py b/rdflib/parser.py
index 6cf6f1da..a35c1d82 100644
--- a/rdflib/parser.py
+++ b/rdflib/parser.py
@@ -27,13 +27,13 @@ from typing import (
Tuple,
Union,
)
-from urllib.error import HTTPError
from urllib.parse import urljoin
-from urllib.request import Request, url2pathname, urlopen
+from urllib.request import Request, url2pathname
from xml.sax import xmlreader
import rdflib.util
from rdflib import __version__
+from rdflib._networking import _urlopen
from rdflib.namespace import Namespace
from rdflib.term import URIRef
@@ -267,21 +267,6 @@ class URLInputSource(InputSource):
req = Request(system_id, None, myheaders) # type: ignore[arg-type]
- def _urlopen(req: Request) -> Any:
- try:
- return urlopen(req)
- except HTTPError as ex:
- # 308 (Permanent Redirect) is not supported by current python version(s)
- # See https://bugs.python.org/issue40321
- # This custom error handling should be removed once all
- # supported versions of python support 308.
- if ex.code == 308:
- # type error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str")
- req.full_url = ex.headers.get("Location") # type: ignore[assignment]
- return _urlopen(req)
- else:
- raise
-
response: addinfourl = _urlopen(req)
self.url = response.geturl() # in case redirections took place
self.links = self.get_links(response)
diff --git a/test/conftest.py b/test/conftest.py
index 2f61c9fe..38f4dabc 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -44,22 +44,44 @@ def rdfs_graph() -> Graph:
return Graph().parse(TEST_DATA_DIR / "defined_namespaces/rdfs.ttl", format="turtle")
+_ServedBaseHTTPServerMocks = Tuple[ServedBaseHTTPServerMock, ServedBaseHTTPServerMock]
+
+
@pytest.fixture(scope="session")
-def _session_function_httpmock() -> Generator[ServedBaseHTTPServerMock, None, None]:
+def _session_function_httpmocks() -> Generator[_ServedBaseHTTPServerMocks, None, None]:
"""
This fixture is session scoped, but it is reset for each function in
:func:`function_httpmock`. This should not be used directly.
"""
- with ServedBaseHTTPServerMock() as httpmock:
- yield httpmock
+ with ServedBaseHTTPServerMock() as httpmock_a, ServedBaseHTTPServerMock() as httpmock_b:
+ yield httpmock_a, httpmock_b
@pytest.fixture(scope="function")
def function_httpmock(
- _session_function_httpmock: ServedBaseHTTPServerMock,
+ _session_function_httpmocks: _ServedBaseHTTPServerMocks,
) -> Generator[ServedBaseHTTPServerMock, None, None]:
- _session_function_httpmock.reset()
- yield _session_function_httpmock
+ """
+ HTTP server mock that is reset for each test function.
+ """
+ (mock, _) = _session_function_httpmocks
+ mock.reset()
+ yield mock
+
+
+@pytest.fixture(scope="function")
+def function_httpmocks(
+ _session_function_httpmocks: _ServedBaseHTTPServerMocks,
+) -> Generator[Tuple[ServedBaseHTTPServerMock, ServedBaseHTTPServerMock], None, None]:
+ """
+ Alternative HTTP server mock that is reset for each test function.
+
+ This exists in case a tests needs to work with two different HTTP servers.
+ """
+ (mock_a, mock_b) = _session_function_httpmocks
+ mock_a.reset()
+ mock_b.reset()
+ yield mock_a, mock_b
@pytest.fixture(scope="session", autouse=True)
diff --git a/test/data.py b/test/data.py
index f1271aae..779c522a 100644
--- a/test/data.py
+++ b/test/data.py
@@ -1,6 +1,7 @@
from pathlib import Path
from rdflib import URIRef
+from rdflib.graph import Graph
TEST_DIR = Path(__file__).parent
TEST_DATA_DIR = TEST_DIR / "data"
@@ -19,3 +20,20 @@ cheese = URIRef("urn:example:cheese")
context0 = URIRef("urn:example:context-0")
context1 = URIRef("urn:example:context-1")
context2 = URIRef("urn:example:context-2")
+
+
+simple_triple_graph = Graph().add(
+ (
+ URIRef("http://example.org/subject"),
+ URIRef("http://example.org/predicate"),
+ URIRef("http://example.org/object"),
+ )
+)
+"""
+A simple graph with a single triple. This is equivalent to the following RDF files:
+
+* ``test/data/variants/simple_triple.nq``
+* ``test/data/variants/simple_triple.nt``
+* ``test/data/variants/simple_triple.ttl``
+* ``test/data/variants/simple_triple.xml``
+"""
diff --git a/test/data/defined_namespaces/adms.rdf b/test/data/defined_namespaces/adms.rdf
new file mode 100644
index 00000000..cb56922a
--- /dev/null
+++ b/test/data/defined_namespaces/adms.rdf
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <ns1:Ontology xmlns:ns1="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms">
+ <ns2:issued xmlns:ns2="http://purl.org/dc/terms/">2023-04-05</ns2:issued>
+ <ns3:license xmlns:ns3="http://purl.org/dc/terms/"
+ rdf:resource="https://creativecommons.org/licenses/by/4.0/"/>
+ <ns4:mediator xmlns:ns4="http://purl.org/dc/terms/">
+ <rdf:Description>
+ <ns5:homepage xmlns:ns5="http://xmlns.com/foaf/0.1/"
+ rdf:resource="https://semic.eu"/>
+ <ns6:name xmlns:ns6="http://xmlns.com/foaf/0.1/">Semantic Interoperability Community (SEMIC)</ns6:name>
+ </rdf:Description>
+ </ns4:mediator>
+ <ns7:label xmlns:ns7="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">adms</ns7:label>
+ <ns8:label xmlns:ns8="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="nl">adms</ns8:label>
+ <ns9:editor xmlns:ns9="http://www.w3.org/2001/02pd/rec54#">
+ <ns10:Person xmlns:ns10="http://xmlns.com/foaf/0.1/">
+ <ns10:firstName>Bert</ns10:firstName>
+ <ns10:lastName>Van Nuffelen</ns10:lastName>
+ <ns10:mbox rdf:resource="mailto:bert.van.nuffelen@tenforce.com"/>
+ <ns11:affiliation xmlns:ns11="https://schema.org/">
+ <rdf:Description>
+ <ns10:name>TenForce</ns10:name>
+ </rdf:Description>
+ </ns11:affiliation>
+ </ns10:Person>
+ </ns9:editor>
+ <ns12:editor xmlns:ns12="http://www.w3.org/2001/02pd/rec54#">
+ <ns13:Person xmlns:ns13="http://xmlns.com/foaf/0.1/">
+ <ns13:firstName>Natasa</ns13:firstName>
+ <ns13:lastName>Sofou</ns13:lastName>
+ </ns13:Person>
+ </ns12:editor>
+ <ns14:editor xmlns:ns14="http://www.w3.org/2001/02pd/rec54#">
+ <ns15:Person xmlns:ns15="http://xmlns.com/foaf/0.1/">
+ <ns15:firstName>Pavlina</ns15:firstName>
+ <ns15:lastName>Fragkou</ns15:lastName>
+ <ns16:affiliation xmlns:ns16="https://schema.org/">
+ <rdf:Description>
+ <ns15:name>SEMIC EU</ns15:name>
+ </rdf:Description>
+ </ns16:affiliation>
+ </ns15:Person>
+ </ns14:editor>
+ <ns17:editor xmlns:ns17="http://www.w3.org/2001/02pd/rec54#">
+ <ns18:Person xmlns:ns18="http://xmlns.com/foaf/0.1/">
+ <ns18:firstName>Makx</ns18:firstName>
+ <ns18:lastName>Dekkers</ns18:lastName>
+ </ns18:Person>
+ </ns17:editor>
+ <ns19:maker xmlns:ns19="http://xmlns.com/foaf/0.1/">
+ <ns19:Person>
+ <ns19:firstName>Pavlina</ns19:firstName>
+ <ns19:lastName>Fragkou</ns19:lastName>
+ <ns20:affiliation xmlns:ns20="https://schema.org/">
+ <rdf:Description>
+ <ns19:name>SEMIC EU</ns19:name>
+ </rdf:Description>
+ </ns20:affiliation>
+ </ns19:Person>
+ </ns19:maker>
+ </ns1:Ontology>
+ <ns21:Class xmlns:ns21="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#Asset">
+ <ns22:comment xmlns:ns22="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">An abstract entity that reflects the intellectual content of the asset and represents those characteristics of the asset that are independent of its physical embodiment. This abstract entity combines the FRBR entities work (a distinct intellectual or artistic creation) and expression (the intellectual or artistic realization of a work)</ns22:comment>
+ <ns23:isDefinedBy xmlns:ns23="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns24:label xmlns:ns24="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Asset</ns24:label>
+ </ns21:Class>
+ <ns25:Class xmlns:ns25="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#AssetDistribution">
+ <ns26:comment xmlns:ns26="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A particular physical embodiment of an Asset, which is an example of the FRBR entity manifestation (the physical embodiment of an expression of a work).</ns26:comment>
+ <ns27:isDefinedBy xmlns:ns27="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns28:label xmlns:ns28="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Asset Distribution</ns28:label>
+ </ns25:Class>
+ <ns29:Class xmlns:ns29="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#AssetRepository">
+ <ns30:comment xmlns:ns30="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A system or service that provides facilities for storage and maintenance of descriptions of Assets and Asset Distributions, and functionality that allows users to search and access these descriptions. An Asset Repository will typically contain descriptions of several Assets and related Asset Distributions.</ns30:comment>
+ <ns31:isDefinedBy xmlns:ns31="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns32:label xmlns:ns32="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Asset repository</ns32:label>
+ </ns29:Class>
+ <ns33:Class xmlns:ns33="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#Identifier">
+ <ns34:comment xmlns:ns34="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">This is based on the UN/CEFACT Identifier class.</ns34:comment>
+ <ns35:isDefinedBy xmlns:ns35="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns36:label xmlns:ns36="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Identifier</ns36:label>
+ </ns33:Class>
+ <ns37:ObjectProperty xmlns:ns37="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#identifier">
+ <ns38:comment xmlns:ns38="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Links a resource to an adms:Identifier class.</ns38:comment>
+ <ns39:domain xmlns:ns39="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns40:isDefinedBy xmlns:ns40="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns41:label xmlns:ns41="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">identifier</ns41:label>
+ <ns42:range xmlns:ns42="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms#Identifier"/>
+ </ns37:ObjectProperty>
+ <ns43:ObjectProperty xmlns:ns43="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#includedAsset">
+ <ns44:comment xmlns:ns44="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">An Asset that is contained in the Asset being described, e.g. when there are several vocabularies defined in a single document.</ns44:comment>
+ <ns45:domain xmlns:ns45="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms#Asset"/>
+ <ns46:isDefinedBy xmlns:ns46="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns47:label xmlns:ns47="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">included asset</ns47:label>
+ <ns48:range xmlns:ns48="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms#Asset"/>
+ </ns43:ObjectProperty>
+ <ns49:ObjectProperty xmlns:ns49="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#interoperabilityLevel">
+ <ns50:comment xmlns:ns50="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">The interoperability level for which the Asset is relevant.</ns50:comment>
+ <ns51:domain xmlns:ns51="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms#Asset"/>
+ <ns52:isDefinedBy xmlns:ns52="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns53:label xmlns:ns53="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">interoperability level</ns53:label>
+ <ns54:range xmlns:ns54="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2004/02/skos/core#Concept"/>
+ </ns49:ObjectProperty>
+ <ns55:ObjectProperty xmlns:ns55="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#last">
+ <ns56:comment xmlns:ns56="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A link to the current or latest version of the Asset.</ns56:comment>
+ <ns57:domain xmlns:ns57="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns58:isDefinedBy xmlns:ns58="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns59:label xmlns:ns59="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">last</ns59:label>
+ <ns60:range xmlns:ns60="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns61:subPropertyOf xmlns:ns61="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/1999/xhtml/vocab#last"/>
+ </ns55:ObjectProperty>
+ <ns62:ObjectProperty xmlns:ns62="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#next">
+ <ns63:comment xmlns:ns63="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A link to the next version of the Asset.</ns63:comment>
+ <ns64:domain xmlns:ns64="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns65:isDefinedBy xmlns:ns65="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns66:label xmlns:ns66="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">next</ns66:label>
+ <ns67:range xmlns:ns67="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns68:subPropertyOf xmlns:ns68="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/1999/xhtml/vocab#next"/>
+ </ns62:ObjectProperty>
+ <ns69:ObjectProperty xmlns:ns69="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#prev">
+ <ns70:comment xmlns:ns70="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A link to the previous version of the Asset.</ns70:comment>
+ <ns71:domain xmlns:ns71="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns72:isDefinedBy xmlns:ns72="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns73:label xmlns:ns73="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">prev</ns73:label>
+ <ns74:range xmlns:ns74="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns75:subPropertyOf xmlns:ns75="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/1999/xhtml/vocab#prev"/>
+ </ns69:ObjectProperty>
+ <ns76:ObjectProperty xmlns:ns76="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#representationTechnique">
+ <ns77:comment xmlns:ns77="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">More information about the format in which an Asset Distribution is released. This is different from the file format as, for example, a ZIP file (file format) could contain an XML schema (representation technique).</ns77:comment>
+ <ns78:domain xmlns:ns78="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns79:isDefinedBy xmlns:ns79="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns80:label xmlns:ns80="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">representation technique</ns80:label>
+ <ns81:range xmlns:ns81="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2004/02/skos/core#Concept"/>
+ </ns76:ObjectProperty>
+ <ns82:ObjectProperty xmlns:ns82="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#sample">
+ <ns83:comment xmlns:ns83="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Links to a sample of an Asset (which is itself an Asset).</ns83:comment>
+ <ns84:domain xmlns:ns84="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns85:isDefinedBy xmlns:ns85="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns86:label xmlns:ns86="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">sample</ns86:label>
+ <ns87:range xmlns:ns87="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ </ns82:ObjectProperty>
+ <ns88:DatatypeProperty xmlns:ns88="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#schemaAgency">
+ <ns89:comment xmlns:ns89="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">The name of the agency that issued the identifier.</ns89:comment>
+ <ns90:domain xmlns:ns90="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms#Identifier"/>
+ <ns91:isDefinedBy xmlns:ns91="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns92:label xmlns:ns92="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">schema agency</ns92:label>
+ <ns93:range xmlns:ns93="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Literal"/>
+ </ns88:DatatypeProperty>
+ <ns94:ObjectProperty xmlns:ns94="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#status">
+ <ns95:comment xmlns:ns95="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">The status of the Asset in the context of a particular workflow process.</ns95:comment>
+ <ns96:domain xmlns:ns96="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns97:isDefinedBy xmlns:ns97="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns98:label xmlns:ns98="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">status</ns98:label>
+ <ns99:range xmlns:ns99="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2004/02/skos/core#Concept"/>
+ </ns94:ObjectProperty>
+ <ns100:ObjectProperty xmlns:ns100="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#supportedSchema">
+ <ns101:comment xmlns:ns101="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A schema according to which the Asset Repository can provide data about its content, e.g. ADMS.</ns101:comment>
+ <ns102:domain xmlns:ns102="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns103:isDefinedBy xmlns:ns103="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns104:label xmlns:ns104="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">supported schema</ns104:label>
+ <ns105:range xmlns:ns105="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms#Asset"/>
+ </ns100:ObjectProperty>
+ <ns106:ObjectProperty xmlns:ns106="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#translation">
+ <ns107:comment xmlns:ns107="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">Links Assets that are translations of each other.</ns107:comment>
+ <ns108:domain xmlns:ns108="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns109:isDefinedBy xmlns:ns109="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns110:label xmlns:ns110="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">translation</ns110:label>
+ <ns111:range xmlns:ns111="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ </ns106:ObjectProperty>
+ <ns112:DatatypeProperty xmlns:ns112="http://www.w3.org/2002/07/owl#"
+ rdf:about="http://www.w3.org/ns/adms#versionNotes">
+ <ns113:comment xmlns:ns113="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">A description of changes between this version and the previous version of the Asset.</ns113:comment>
+ <ns114:domain xmlns:ns114="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <ns115:isDefinedBy xmlns:ns115="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/ns/adms"/>
+ <ns116:label xmlns:ns116="http://www.w3.org/2000/01/rdf-schema#"
+ xml:lang="en">version notes</ns116:label>
+ <ns117:range xmlns:ns117="http://www.w3.org/2000/01/rdf-schema#"
+ rdf:resource="http://www.w3.org/2000/01/rdf-schema#Literal"/>
+ </ns112:DatatypeProperty>
+</rdf:RDF>
diff --git a/test/data/defined_namespaces/adms.ttl b/test/data/defined_namespaces/adms.ttl
new file mode 100644
index 00000000..86561101
--- /dev/null
+++ b/test/data/defined_namespaces/adms.ttl
@@ -0,0 +1,175 @@
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+
+<http://www.w3.org/ns/adms>
+ <http://purl.org/dc/terms/issued> "2023-04-05" ;
+ <http://purl.org/dc/terms/license> <https://creativecommons.org/licenses/by/4.0/> ;
+ <http://purl.org/dc/terms/mediator> [
+ <http://xmlns.com/foaf/0.1/homepage> <https://semic.eu> ;
+ <http://xmlns.com/foaf/0.1/name> "Semantic Interoperability Community (SEMIC)"
+ ] ;
+ a <http://www.w3.org/2002/07/owl#Ontology> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "adms"@en, "adms"@nl ;
+ <http://www.w3.org/2001/02pd/rec54#editor> [
+ a <http://xmlns.com/foaf/0.1/Person> ;
+ <http://xmlns.com/foaf/0.1/firstName> "Bert" ;
+ <http://xmlns.com/foaf/0.1/lastName> "Van Nuffelen" ;
+ <http://xmlns.com/foaf/0.1/mbox> <mailto:bert.van.nuffelen@tenforce.com> ;
+ <https://schema.org/affiliation> [
+ <http://xmlns.com/foaf/0.1/name> "TenForce"
+ ]
+ ], [
+ a <http://xmlns.com/foaf/0.1/Person> ;
+ <http://xmlns.com/foaf/0.1/firstName> "Natasa" ;
+ <http://xmlns.com/foaf/0.1/lastName> "Sofou"
+ ], [
+ a <http://xmlns.com/foaf/0.1/Person> ;
+ <http://xmlns.com/foaf/0.1/firstName> "Pavlina" ;
+ <http://xmlns.com/foaf/0.1/lastName> "Fragkou" ;
+ <https://schema.org/affiliation> [
+ <http://xmlns.com/foaf/0.1/name> "SEMIC EU"
+ ]
+ ], [
+ a <http://xmlns.com/foaf/0.1/Person> ;
+ <http://xmlns.com/foaf/0.1/firstName> "Makx" ;
+ <http://xmlns.com/foaf/0.1/lastName> "Dekkers"
+ ] ;
+ <http://xmlns.com/foaf/0.1/maker> [
+ a <http://xmlns.com/foaf/0.1/Person> ;
+ <http://xmlns.com/foaf/0.1/firstName> "Pavlina" ;
+ <http://xmlns.com/foaf/0.1/lastName> "Fragkou" ;
+ <https://schema.org/affiliation> [
+ <http://xmlns.com/foaf/0.1/name> "SEMIC EU"
+ ]
+ ] .
+
+<http://www.w3.org/ns/adms#Asset>
+ a <http://www.w3.org/2002/07/owl#Class> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "An abstract entity that reflects the intellectual content of the asset and represents those characteristics of the asset that are independent of its physical embodiment. This abstract entity combines the FRBR entities work (a distinct intellectual or artistic creation) and expression (the intellectual or artistic realization of a work)"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "Asset"@en .
+
+<http://www.w3.org/ns/adms#AssetDistribution>
+ a <http://www.w3.org/2002/07/owl#Class> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A particular physical embodiment of an Asset, which is an example of the FRBR entity manifestation (the physical embodiment of an expression of a work)."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "Asset Distribution"@en .
+
+<http://www.w3.org/ns/adms#AssetRepository>
+ a <http://www.w3.org/2002/07/owl#Class> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A system or service that provides facilities for storage and maintenance of descriptions of Assets and Asset Distributions, and functionality that allows users to search and access these descriptions. An Asset Repository will typically contain descriptions of several Assets and related Asset Distributions."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "Asset repository"@en .
+
+<http://www.w3.org/ns/adms#Identifier>
+ a <http://www.w3.org/2002/07/owl#Class> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "This is based on the UN/CEFACT Identifier class."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "Identifier"@en .
+
+<http://www.w3.org/ns/adms#identifier>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "Links a resource to an adms:Identifier class."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "identifier"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/ns/adms#Identifier> .
+
+<http://www.w3.org/ns/adms#includedAsset>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "An Asset that is contained in the Asset being described, e.g. when there are several vocabularies defined in a single document."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/ns/adms#Asset> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "included asset"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/ns/adms#Asset> .
+
+<http://www.w3.org/ns/adms#interoperabilityLevel>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "The interoperability level for which the Asset is relevant."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/ns/adms#Asset> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "interoperability level"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2004/02/skos/core#Concept> .
+
+<http://www.w3.org/ns/adms#last>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A link to the current or latest version of the Asset."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "last"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#subPropertyOf> <http://www.w3.org/1999/xhtml/vocab#last> .
+
+<http://www.w3.org/ns/adms#next>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A link to the next version of the Asset."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "next"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#subPropertyOf> <http://www.w3.org/1999/xhtml/vocab#next> .
+
+<http://www.w3.org/ns/adms#prev>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A link to the previous version of the Asset."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "prev"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#subPropertyOf> <http://www.w3.org/1999/xhtml/vocab#prev> .
+
+<http://www.w3.org/ns/adms#representationTechnique>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "More information about the format in which an Asset Distribution is released. This is different from the file format as, for example, a ZIP file (file format) could contain an XML schema (representation technique)."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "representation technique"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2004/02/skos/core#Concept> .
+
+<http://www.w3.org/ns/adms#sample>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "Links to a sample of an Asset (which is itself an Asset)."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "sample"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Resource> .
+
+<http://www.w3.org/ns/adms#schemaAgency>
+ a <http://www.w3.org/2002/07/owl#DatatypeProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "The name of the agency that issued the identifier."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/ns/adms#Identifier> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "schema agency"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Literal> .
+
+<http://www.w3.org/ns/adms#status>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "The status of the Asset in the context of a particular workflow process."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "status"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2004/02/skos/core#Concept> .
+
+<http://www.w3.org/ns/adms#supportedSchema>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A schema according to which the Asset Repository can provide data about its content, e.g. ADMS."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "supported schema"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/ns/adms#Asset> .
+
+<http://www.w3.org/ns/adms#translation>
+ a <http://www.w3.org/2002/07/owl#ObjectProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "Links Assets that are translations of each other."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "translation"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Resource> .
+
+<http://www.w3.org/ns/adms#versionNotes>
+ a <http://www.w3.org/2002/07/owl#DatatypeProperty> ;
+ <http://www.w3.org/2000/01/rdf-schema#comment> "A description of changes between this version and the previous version of the Asset."@en ;
+ <http://www.w3.org/2000/01/rdf-schema#domain> <http://www.w3.org/2000/01/rdf-schema#Resource> ;
+ <http://www.w3.org/2000/01/rdf-schema#isDefinedBy> <http://www.w3.org/ns/adms> ;
+ <http://www.w3.org/2000/01/rdf-schema#label> "version notes"@en ;
+ <http://www.w3.org/2000/01/rdf-schema#range> <http://www.w3.org/2000/01/rdf-schema#Literal> .
+
diff --git a/test/data/defined_namespaces/rdfs.rdf b/test/data/defined_namespaces/rdfs.rdf
new file mode 100644
index 00000000..bf17bab0
--- /dev/null
+++ b/test/data/defined_namespaces/rdfs.rdf
@@ -0,0 +1,130 @@
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+ xmlns:owl="http://www.w3.org/2002/07/owl#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+
+ <owl:Ontology
+ rdf:about="http://www.w3.org/2000/01/rdf-schema#"
+ dc:title="The RDF Schema vocabulary (RDFS)"/>
+
+<rdfs:Class rdf:about="http://www.w3.org/2000/01/rdf-schema#Resource">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>Resource</rdfs:label>
+ <rdfs:comment>The class resource, everything.</rdfs:comment>
+</rdfs:Class>
+
+<rdfs:Class rdf:about="http://www.w3.org/2000/01/rdf-schema#Class">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>Class</rdfs:label>
+ <rdfs:comment>The class of classes.</rdfs:comment>
+ <rdfs:subClassOf rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+</rdfs:Class>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#subClassOf">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>subClassOf</rdfs:label>
+ <rdfs:comment>The subject is a subclass of a class.</rdfs:comment>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Class"/>
+ <rdfs:domain rdf:resource="http://www.w3.org/2000/01/rdf-schema#Class"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#subPropertyOf">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>subPropertyOf</rdfs:label>
+ <rdfs:comment>The subject is a subproperty of a property.</rdfs:comment>
+ <rdfs:range rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Property"/>
+ <rdfs:domain rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Property"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#comment">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>comment</rdfs:label>
+ <rdfs:comment>A description of the subject resource.</rdfs:comment>
+ <rdfs:domain rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Literal"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#label">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>label</rdfs:label>
+ <rdfs:comment>A human-readable name for the subject.</rdfs:comment>
+ <rdfs:domain rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Literal"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#domain">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>domain</rdfs:label>
+ <rdfs:comment>A domain of the subject property.</rdfs:comment>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Class"/>
+ <rdfs:domain rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Property"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#range">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>range</rdfs:label>
+ <rdfs:comment>A range of the subject property.</rdfs:comment>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Class"/>
+ <rdfs:domain rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Property"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#seeAlso">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>seeAlso</rdfs:label>
+ <rdfs:comment>Further information about the subject resource.</rdfs:comment>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <rdfs:domain rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+</rdf:Property>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#isDefinedBy">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:subPropertyOf rdf:resource="http://www.w3.org/2000/01/rdf-schema#seeAlso"/>
+ <rdfs:label>isDefinedBy</rdfs:label>
+ <rdfs:comment>The defininition of the subject resource.</rdfs:comment>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <rdfs:domain rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+</rdf:Property>
+
+<rdfs:Class rdf:about="http://www.w3.org/2000/01/rdf-schema#Literal">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>Literal</rdfs:label>
+ <rdfs:comment>The class of literal values, eg. textual strings and integers.</rdfs:comment>
+ <rdfs:subClassOf rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+</rdfs:Class>
+
+<rdfs:Class rdf:about="http://www.w3.org/2000/01/rdf-schema#Container">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>Container</rdfs:label>
+ <rdfs:subClassOf rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <rdfs:comment>The class of RDF containers.</rdfs:comment>
+</rdfs:Class>
+
+<rdfs:Class rdf:about="http://www.w3.org/2000/01/rdf-schema#ContainerMembershipProperty">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>ContainerMembershipProperty</rdfs:label>
+ <rdfs:comment>The class of container membership properties, rdf:_1, rdf:_2, ...,
+ all of which are sub-properties of 'member'.</rdfs:comment>
+ <rdfs:subClassOf rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#Property"/>
+</rdfs:Class>
+
+<rdf:Property rdf:about="http://www.w3.org/2000/01/rdf-schema#member">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>member</rdfs:label>
+ <rdfs:comment>A member of the subject resource.</rdfs:comment>
+ <rdfs:domain rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+ <rdfs:range rdf:resource="http://www.w3.org/2000/01/rdf-schema#Resource"/>
+</rdf:Property>
+
+<rdfs:Class rdf:about="http://www.w3.org/2000/01/rdf-schema#Datatype">
+ <rdfs:isDefinedBy rdf:resource="http://www.w3.org/2000/01/rdf-schema#"/>
+ <rdfs:label>Datatype</rdfs:label>
+ <rdfs:comment>The class of RDF datatypes.</rdfs:comment>
+ <rdfs:subClassOf rdf:resource="http://www.w3.org/2000/01/rdf-schema#Class"/>
+</rdfs:Class>
+
+<rdf:Description rdf:about="http://www.w3.org/2000/01/rdf-schema#">
+ <rdfs:seeAlso rdf:resource="http://www.w3.org/2000/01/rdf-schema-more"/>
+</rdf:Description>
+
+</rdf:RDF>
diff --git a/test/data/fetcher.py b/test/data/fetcher.py
index 7c9e4ff0..1ea8e337 100755
--- a/test/data/fetcher.py
+++ b/test/data/fetcher.py
@@ -249,6 +249,21 @@ RESOURCES: List[Resource] = [
local_path=(DATA_PATH / "defined_namespaces/rdfs.ttl"),
),
FileResource(
+ remote=Request(
+ "http://www.w3.org/2000/01/rdf-schema#",
+ headers={"Accept": "application/rdf+xml"},
+ ),
+ local_path=(DATA_PATH / "defined_namespaces/rdfs.rdf"),
+ ),
+ FileResource(
+ remote=Request("http://www.w3.org/ns/adms.rdf"),
+ local_path=(DATA_PATH / "defined_namespaces/adms.rdf"),
+ ),
+ FileResource(
+ remote=Request("http://www.w3.org/ns/adms.ttl"),
+ local_path=(DATA_PATH / "defined_namespaces/adms.ttl"),
+ ),
+ FileResource(
remote=Request("https://www.w3.org/ns/rdftest.ttl"),
local_path=(DATA_PATH / "defined_namespaces/rdftest.ttl"),
),
diff --git a/test/test_graph/test_graph.py b/test/test_graph/test_graph.py
index b133c2b5..289d577a 100644
--- a/test/test_graph/test_graph.py
+++ b/test/test_graph/test_graph.py
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
import logging
import os
+from contextlib import ExitStack
from pathlib import Path
from test.data import TEST_DATA_DIR, bob, cheese, hates, likes, michel, pizza, tarek
from test.utils import GraphHelper, get_unique_plugin_names
+from test.utils.exceptions import ExceptionChecker
from test.utils.httpfileserver import HTTPFileServer, ProtoFileResource
-from typing import Callable, Optional, Set
+from typing import Callable, Optional, Set, Tuple, Union
from urllib.error import HTTPError, URLError
import pytest
@@ -342,14 +344,6 @@ def test_guess_format_for_parse(
# only getting HTML
with pytest.raises(PluginException):
graph.parse(location=file_info.request_url)
-
- try:
- graph.parse(location="http://www.w3.org/ns/adms.ttl")
- graph.parse(location="http://www.w3.org/ns/adms.rdf")
- except (URLError, HTTPError):
- # this endpoint is currently not available, ignore this test.
- pass
-
try:
# persistent Australian Government online RDF resource without a file-like ending
graph.parse(location="https://linked.data.gov.au/def/agrif?_format=text/turtle")
@@ -358,6 +352,55 @@ def test_guess_format_for_parse(
pass
+@pytest.mark.parametrize(
+ ("file", "content_type", "expected_result"),
+ (
+ (TEST_DATA_DIR / "defined_namespaces/adms.rdf", "application/rdf+xml", 132),
+ (TEST_DATA_DIR / "defined_namespaces/adms.ttl", "text/turtle", 132),
+ (TEST_DATA_DIR / "defined_namespaces/adms.ttl", None, 132),
+ (
+ TEST_DATA_DIR / "defined_namespaces/adms.rdf",
+ None,
+ ExceptionChecker(
+ ParserError,
+ r"Could not guess RDF format .* from file extension so tried Turtle",
+ ),
+ ),
+ ),
+)
+def test_guess_format_for_parse_http(
+ make_graph: GraphFactory,
+ http_file_server: HTTPFileServer,
+ file: Path,
+ content_type: Optional[str],
+ expected_result: Union[int, ExceptionChecker],
+) -> None:
+ graph = make_graph()
+ headers: Tuple[Tuple[str, str], ...] = tuple()
+ if content_type is not None:
+ headers = (("Content-Type", content_type),)
+
+ file_info = http_file_server.add_file_with_caching(
+ ProtoFileResource(headers, file),
+ suffix=f"/{file.name}",
+ )
+ catcher: Optional[pytest.ExceptionInfo[Exception]] = None
+
+ assert 0 == len(graph)
+ with ExitStack() as exit_stack:
+ if isinstance(expected_result, ExceptionChecker):
+ catcher = exit_stack.enter_context(pytest.raises(expected_result.type))
+ graph.parse(location=file_info.request_url)
+
+ if catcher is not None:
+ # assert catcher.value is not None
+ assert isinstance(expected_result, ExceptionChecker)
+ logging.debug("graph = %s", list(graph.triples((None, None, None))))
+ else:
+ assert isinstance(expected_result, int)
+ assert expected_result == len(graph)
+
+
def test_parse_file_uri(make_graph: GraphFactory):
EG = Namespace("http://example.org/#")
g = make_graph()
diff --git a/test/test_graph/test_graph_redirect.py b/test/test_graph/test_graph_redirect.py
new file mode 100644
index 00000000..c61adbc5
--- /dev/null
+++ b/test/test_graph/test_graph_redirect.py
@@ -0,0 +1,45 @@
+from test.data import TEST_DATA_DIR, simple_triple_graph
+from test.utils import GraphHelper
+from test.utils.http import MethodName, MockHTTPResponse
+from test.utils.httpservermock import ServedBaseHTTPServerMock
+from typing import Tuple
+from urllib.parse import urlparse
+
+from rdflib.graph import Graph
+
+
+def test_graph_redirect_new_host(
+ function_httpmocks: Tuple[ServedBaseHTTPServerMock, ServedBaseHTTPServerMock]
+) -> None:
+ """
+ Redirect to new host results in a request with the right Host header
+ parameter.
+ """
+
+ mock_a, mock_b = function_httpmocks
+
+ mock_a.responses[MethodName.GET].append(
+ MockHTTPResponse(
+ 308,
+ "Permanent Redirect",
+ b"",
+ {"Location": [f"{mock_b.url}/b/data.ttl"]},
+ )
+ )
+
+ mock_b.responses[MethodName.GET].append(
+ MockHTTPResponse(
+ 200,
+ "OK",
+ (TEST_DATA_DIR / "variants" / "simple_triple.ttl").read_bytes(),
+ {"Content-Type": ["text/turtle"]},
+ )
+ )
+
+ graph = Graph()
+ graph.parse(location=f"{mock_a.url}/a/data.ttl")
+ 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]:
+ assert request.headers["Host"] == urlparse(mock.url).netloc
diff --git a/test/test_misc/test_input_source.py b/test/test_misc/test_input_source.py
index f3da062b..90e6e238 100644
--- a/test/test_misc/test_input_source.py
+++ b/test/test_misc/test_input_source.py
@@ -11,6 +11,7 @@ from dataclasses import dataclass
# from itertools import product
from pathlib import Path
from test.utils import GraphHelper
+from test.utils.exceptions import ExceptionChecker
from test.utils.httpfileserver import (
HTTPFileInfo,
HTTPFileServer,
@@ -27,7 +28,6 @@ from typing import ( # Callable,
Generic,
Iterable,
Optional,
- Pattern,
TextIO,
Tuple,
Type,
@@ -251,21 +251,6 @@ def call_create_input_source(
yield input_source
-@dataclass
-class ExceptionChecker:
- type: Type[Exception]
- pattern: Optional[Pattern[str]] = None
-
- def check(self, exception: Exception) -> None:
- try:
- assert isinstance(exception, self.type)
- if self.pattern is not None:
- assert self.pattern.match(f"{exception}")
- except Exception:
- logging.error("problem checking exception", exc_info=exception)
- raise
-
-
AnyT = TypeVar("AnyT")
diff --git a/test/test_misc/test_networking_redirect.py b/test/test_misc/test_networking_redirect.py
new file mode 100644
index 00000000..acde10d7
--- /dev/null
+++ b/test/test_misc/test_networking_redirect.py
@@ -0,0 +1,217 @@
+from contextlib import ExitStack
+from copy import deepcopy
+from test.utils.exceptions import ExceptionChecker
+from test.utils.http import headers_as_message as headers_as_message
+from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union
+from urllib.error import HTTPError
+from urllib.request import HTTPRedirectHandler, Request
+
+import pytest
+from _pytest.mark.structures import ParameterSet
+
+from rdflib._networking import _make_redirect_request
+
+AnyT = TypeVar("AnyT")
+
+
+def with_attrs(object: AnyT, **kwargs: Any) -> AnyT:
+ for key, value in kwargs.items():
+ setattr(object, key, value)
+ return object
+
+
+class RaisesIdentity:
+ pass
+
+
+def generate_make_redirect_request_cases() -> Iterable[ParameterSet]:
+ yield pytest.param(
+ Request("http://example.com/data.ttl"),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({}),
+ None,
+ ),
+ RaisesIdentity,
+ {},
+ id="Exception passes through if no Location header is present",
+ )
+ yield pytest.param(
+ Request("http://example.com/data.ttl"),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({"Location": [100]}), # type: ignore[arg-type]
+ None,
+ ),
+ ExceptionChecker(ValueError, "Location header 100 is not a string"),
+ {},
+ id="Location must be a string",
+ )
+ yield pytest.param(
+ Request("http://example.com/data.ttl"),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({"Location": ["example:data.ttl"]}),
+ None,
+ ),
+ ExceptionChecker(
+ HTTPError,
+ "HTTP Error 308: Permanent Redirect - Redirection to url 'example:data.ttl' is not allowed",
+ {"code": 308},
+ ),
+ {},
+ id="Error passes through with a slight alterations if the Location header is not a supported URL",
+ )
+
+ url_prefix = "http://example.com"
+ for request_url_suffix, redirect_location, new_url_suffix in [
+ ("/data.ttl", "", "/data.ttl"),
+ ("", "", ""),
+ ("/data.ttl", "a", "/a"),
+ ("", "a", "/a"),
+ ("/a/b/c/", ".", "/a/b/c/"),
+ ("/a/b/c", ".", "/a/b/"),
+ ("/a/b/c/", "..", "/a/b/"),
+ ("/a/b/c", "..", "/a/"),
+ ("/a/b/c/", "/", "/"),
+ ("/a/b/c/", "/x/", "/x/"),
+ ("/a/b/c/", "/x/y", "/x/y"),
+ ("/a/b/c/", f"{url_prefix}", ""),
+ ("/a/b/c/", f"{url_prefix}/", "/"),
+ ("/a/b/c/", f"{url_prefix}/a/../b", "/a/../b"),
+ ("/", f"{url_prefix}/ /data.ttl", "/%20%20%20/data.ttl"),
+ ]:
+ request_url = f"http://example.com{request_url_suffix}"
+ new_url = f"http://example.com{new_url_suffix}"
+ yield pytest.param(
+ Request(request_url),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({"Location": [redirect_location]}),
+ None,
+ ),
+ Request(new_url, unverifiable=True),
+ {new_url: 1},
+ id=f"Redirect from {request_url!r} to {redirect_location!r} is correctly handled",
+ )
+
+ yield pytest.param(
+ Request(
+ "http://example.com/data.ttl",
+ b"foo",
+ headers={
+ "Content-Type": "text/plain",
+ "Content-Length": "3",
+ "Accept": "text/turtle",
+ },
+ ),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({"Location": ["http://example.org/data.ttl"]}),
+ None,
+ ),
+ Request(
+ "http://example.org/data.ttl",
+ headers={"Accept": "text/turtle"},
+ origin_req_host="example.com",
+ unverifiable=True,
+ ),
+ {"http://example.org/data.ttl": 1},
+ id="Headers transfer correctly",
+ )
+
+ yield pytest.param(
+ with_attrs(
+ Request(
+ "http://example.com/data1.ttl",
+ ),
+ redirect_dict=dict(
+ (f"http://example.com/redirect/{index}", 1)
+ for index in range(HTTPRedirectHandler.max_redirections)
+ ),
+ ),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({"Location": ["http://example.org/data2.ttl"]}),
+ None,
+ ),
+ ExceptionChecker(
+ HTTPError,
+ f"HTTP Error 308: {HTTPRedirectHandler.inf_msg}Permanent Redirect",
+ ),
+ {},
+ id="Max redirects is respected",
+ )
+
+ yield pytest.param(
+ with_attrs(
+ Request(
+ "http://example.com/data1.ttl",
+ ),
+ redirect_dict={
+ "http://example.org/data2.ttl": HTTPRedirectHandler.max_repeats
+ },
+ ),
+ HTTPError(
+ "",
+ 308,
+ "Permanent Redirect",
+ headers_as_message({"Location": ["http://example.org/data2.ttl"]}),
+ None,
+ ),
+ ExceptionChecker(
+ HTTPError,
+ f"HTTP Error 308: {HTTPRedirectHandler.inf_msg}Permanent Redirect",
+ ),
+ {},
+ id="Max repeats is respected",
+ )
+
+
+@pytest.mark.parametrize(
+ ("http_request", "http_error", "expected_result", "expected_redirect_dict"),
+ generate_make_redirect_request_cases(),
+)
+def test_make_redirect_request(
+ http_request: Request,
+ http_error: HTTPError,
+ expected_result: Union[Type[RaisesIdentity], ExceptionChecker, Request],
+ expected_redirect_dict: Dict[str, int],
+) -> None:
+ """
+ `_make_redirect_request` correctly handles redirects.
+ """
+ catcher: Optional[pytest.ExceptionInfo[Exception]] = None
+ result: Optional[Request] = None
+ with ExitStack() as stack:
+ if isinstance(expected_result, ExceptionChecker):
+ catcher = stack.enter_context(pytest.raises(expected_result.type))
+ elif expected_result is RaisesIdentity:
+ catcher = stack.enter_context(pytest.raises(HTTPError))
+ result = _make_redirect_request(http_request, http_error)
+
+ if isinstance(expected_result, ExceptionChecker):
+ assert catcher is not None
+ expected_result.check(catcher.value)
+ elif isinstance(expected_result, type):
+ assert catcher is not None
+ assert http_error is catcher.value
+ else:
+ assert expected_redirect_dict == getattr(result, "redirect_dict", None)
+ assert expected_redirect_dict == getattr(http_request, "redirect_dict", None)
+ check = deepcopy(expected_result)
+ check.unverifiable = True
+ check = with_attrs(check, redirect_dict=expected_redirect_dict)
+ assert vars(check) == vars(result)
diff --git a/test/utils/exceptions.py b/test/utils/exceptions.py
new file mode 100644
index 00000000..a814f9b4
--- /dev/null
+++ b/test/utils/exceptions.py
@@ -0,0 +1,29 @@
+import logging
+import re
+from dataclasses import dataclass
+from typing import Any, Dict, Optional, Pattern, Type, Union
+
+
+@dataclass(frozen=True)
+class ExceptionChecker:
+ type: Type[Exception]
+ pattern: Optional[Union[Pattern[str], str]] = None
+ attributes: Optional[Dict[str, Any]] = None
+
+ def check(self, exception: Exception) -> None:
+ logging.debug("checking exception %s/%r", type(exception), exception)
+ pattern = self.pattern
+ if pattern is not None and not isinstance(pattern, re.Pattern):
+ pattern = re.compile(pattern)
+ try:
+ assert isinstance(exception, self.type)
+ if pattern is not None:
+ assert pattern.match(f"{exception}")
+ if self.attributes is not None:
+ for key, value in self.attributes.items():
+ logging.debug("checking exception attribute %s=%r", key, value)
+ assert hasattr(exception, key)
+ assert getattr(exception, key) == value
+ except Exception:
+ logging.error("problem checking exception", exc_info=exception)
+ raise
diff --git a/test/utils/http.py b/test/utils/http.py
index fa13a2ed..e40d2a8c 100644
--- a/test/utils/http.py
+++ b/test/utils/http.py
@@ -108,3 +108,12 @@ def ctx_http_server(server: HTTPServerT) -> Iterator[HTTPServerT]:
server.shutdown()
server.socket.close()
server_thread.join()
+
+
+def headers_as_message(headers: HeadersT) -> email.message.Message:
+ message = email.message.Message()
+ for header, value in header_items(headers):
+ # This will append the value to any existing values for the header
+ # instead of replacing it.
+ message[header] = value
+ return message
diff --git a/test/utils/httpfileserver.py b/test/utils/httpfileserver.py
index 1989070a..49c92e80 100644
--- a/test/utils/httpfileserver.py
+++ b/test/utils/httpfileserver.py
@@ -74,7 +74,7 @@ class HTTPFileInfo:
:param effective_url: The URL that the file will be served from after
redirects.
:param redirects: A sequence of redirects that will be given to the client
- if it uses the ``request_url``. This sequence will terimate in the
+ if it uses the ``request_url``. This sequence will terminate in the
``effective_url``.
"""
@@ -128,15 +128,17 @@ class HTTPFileServer(HTTPServer):
self,
proto_file: ProtoFileResource,
proto_redirects: Optional[Sequence[ProtoRedirectResource]] = None,
+ suffix: str = "",
) -> HTTPFileInfo:
- return self.add_file(proto_file, proto_redirects)
+ return self.add_file(proto_file, proto_redirects, suffix)
def add_file(
self,
proto_file: ProtoFileResource,
proto_redirects: Optional[Sequence[ProtoRedirectResource]] = None,
+ suffix: str = "",
) -> HTTPFileInfo:
- url_path = f"/file/{uuid4().hex}"
+ url_path = f"/file/{uuid4().hex}{suffix}"
url = urljoin(self.url, url_path)
file_resource = FileResource(
url_path=url_path,
@@ -151,7 +153,7 @@ class HTTPFileServer(HTTPServer):
redirects: List[RedirectResource] = []
for proto_redirect in reversed(proto_redirects):
- redirect_url_path = f"/redirect/{uuid4().hex}"
+ redirect_url_path = f"/redirect/{uuid4().hex}{suffix}"
if proto_redirect.location_type == LocationType.URL:
location = url
elif proto_redirect.location_type == LocationType.ABSOLUTE_PATH: