Repository: RDFLib/rdflib

Issue:
Graph parse module does not handle URI / IRI with non-ascii characters
To reproduce behavior:

```python
from rdflib import Graph

g = Graph()
g.parse("https://dbpedia.org/page/Almería")
```

I was able to bypass it locally by editing the function in `parser.py`
```python
def _create_input_source_from_location(file, format, input_source, location):
    # Fix for Windows problem https://github.com/RDFLib/rdflib/issues/145
    path = pathlib.Path(location)
    if path.exists():
        location = path.absolute().as_uri()

    base = pathlib.Path.cwd().as_uri()

    concept = location.split('/')[-1]                # I added this line and the line below
    location = location.replace(concept, quote(concept))
    absolute_location = URIRef(location, base=base)

    if absolute_location.startswith("file:///"):
        filename = url2pathname(absolute_location.replace("file:///", "/"))
        file = open(filename, "rb")
    else:
        input_source = URLInputSource(absolute_location, format)

    auto_close = True
    # publicID = publicID or absolute_location  # Further to fix
    # for issue 130

    return absolute_location, auto_close, file, input_source
```
according to the help [here](https://stackoverflow.com/questions/4389572/how-to-fetch-a-non-ascii-url-with-urlopen)

Is there a better way to deal with this scenario or should this be a PR?

Comments:

Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 8cb4f5a8..39178818 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,8 @@ pep8-naming = ["-N802", "-N803", "-N806", "-N815"]
 pep8-naming = ["-N802", "-N803", "-N806", "-N816"]
 [tool.flakeheaven.exceptions."rdflib/plugins/serializers/turtle.py"]
 pep8-naming = ["-N802", "-N806", "-N815"]
+[tool.flakeheaven.exceptions."rdflib/__init__.py"]
+pycodestyle = ["-E402"]
 
 
 [tool.black]
diff --git a/rdflib/__init__.py b/rdflib/__init__.py
index 2dbba042..47b3d146 100644
--- a/rdflib/__init__.py
+++ b/rdflib/__init__.py
@@ -84,6 +84,8 @@ __all__ = [
     "VOID",
     "XSD",
     "util",
+    "plugin",
+    "query",
 ]
 
 import logging
@@ -157,7 +159,7 @@ In particular, this determines how the rich comparison operators for
 Literal work, eq, __neq__, __lt__, etc.
 """
 
-from rdflib import plugin, query
+
 from rdflib.graph import ConjunctiveGraph, Dataset, Graph
 from rdflib.namespace import (
     BRICK,
@@ -190,9 +192,5 @@ from rdflib.namespace import (
 )
 from rdflib.term import BNode, IdentifiedNode, Literal, URIRef, Variable
 
-# tedious sop to flake8
-assert plugin
-assert query
-
-from rdflib import util
-from rdflib.container import *
+from rdflib import plugin, query, util  # isort:skip
+from rdflib.container import *  # isort:skip # noqa:F401,F403
diff --git a/rdflib/graph.py b/rdflib/graph.py
index 401b14a7..eb1dc7bc 100644
--- a/rdflib/graph.py
+++ b/rdflib/graph.py
@@ -24,8 +24,11 @@ from urllib.parse import urlparse
 from urllib.request import url2pathname
 from warnings import warn
 
+import rdflib.exceptions as exceptions
+import rdflib.namespace as namespace  # noqa: F401 # This is here because it is used in a docstring.
+import rdflib.plugin as plugin
+import rdflib.query as query
 import rdflib.util  # avoid circular dependency
-from rdflib import exceptions, namespace, plugin, query
 from rdflib.collection import Collection
 from rdflib.exceptions import ParserError
 from rdflib.namespace import RDF, Namespace, NamespaceManager
@@ -120,13 +123,13 @@ Instantiating Graphs with default store (Memory) and default identifier
     <class 'rdflib.term.BNode'>
 
 Instantiating Graphs with a Memory store and an identifier -
-<http://rdflib.net>:
+<https://rdflib.github.io>:
 
-    >>> g = Graph('Memory', URIRef("http://rdflib.net"))
+    >>> g = Graph('Memory', URIRef("https://rdflib.github.io"))
     >>> g.identifier
-    rdflib.term.URIRef('http://rdflib.net')
+    rdflib.term.URIRef('https://rdflib.github.io')
     >>> str(g)  # doctest: +NORMALIZE_WHITESPACE
-    "<http://rdflib.net> a rdfg:Graph;rdflib:storage
+    "<https://rdflib.github.io> a rdfg:Graph;rdflib:storage
      [a rdflib:Store;rdfs:label 'Memory']."
 
 Creating a ConjunctiveGraph - The top level container for all named Graphs
@@ -146,7 +149,7 @@ via triple pattern:
     >>> g.add((statementId, RDF.type, RDF.Statement)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g.add((statementId, RDF.subject,
-    ...     URIRef("http://rdflib.net/store/ConjunctiveGraph"))) # doctest: +ELLIPSIS
+    ...     URIRef("https://rdflib.github.io/store/ConjunctiveGraph"))) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g.add((statementId, RDF.predicate, namespace.RDFS.label)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
@@ -217,7 +220,7 @@ the same store:
     >>> g1.add((stmt1, RDF.type, RDF.Statement)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g1.add((stmt1, RDF.subject,
-    ...     URIRef('http://rdflib.net/store/ConjunctiveGraph'))) # doctest: +ELLIPSIS
+    ...     URIRef('https://rdflib.github.io/store/ConjunctiveGraph'))) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g1.add((stmt1, RDF.predicate, namespace.RDFS.label)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
@@ -226,7 +229,7 @@ the same store:
     >>> g2.add((stmt2, RDF.type, RDF.Statement)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g2.add((stmt2, RDF.subject,
-    ...     URIRef('http://rdflib.net/store/ConjunctiveGraph'))) # doctest: +ELLIPSIS
+    ...     URIRef('https://rdflib.github.io/store/ConjunctiveGraph'))) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g2.add((stmt2, RDF.predicate, RDF.type)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
@@ -235,7 +238,7 @@ the same store:
     >>> g3.add((stmt3, RDF.type, RDF.Statement)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g3.add((stmt3, RDF.subject,
-    ...     URIRef('http://rdflib.net/store/ConjunctiveGraph'))) # doctest: +ELLIPSIS
+    ...     URIRef('https://rdflib.github.io/store/ConjunctiveGraph'))) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
     >>> g3.add((stmt3, RDF.predicate, namespace.RDFS.comment)) # doctest: +ELLIPSIS
     <Graph identifier=... (<class 'rdflib.graph.Graph'>)>
@@ -272,7 +275,7 @@ Parsing N3 from a string
     ... @prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
     ... @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
     ... [ a rdf:Statement ;
-    ...   rdf:subject <http://rdflib.net/store#ConjunctiveGraph>;
+    ...   rdf:subject <https://rdflib.github.io/store#ConjunctiveGraph>;
     ...   rdf:predicate rdfs:label;
     ...   rdf:object "Conjunctive Graph" ] .
     ... '''
@@ -282,11 +285,11 @@ Parsing N3 from a string
 
 Using Namespace class:
 
-    >>> RDFLib = Namespace("http://rdflib.net/")
+    >>> RDFLib = Namespace("https://rdflib.github.io/")
     >>> RDFLib.ConjunctiveGraph
-    rdflib.term.URIRef('http://rdflib.net/ConjunctiveGraph')
+    rdflib.term.URIRef('https://rdflib.github.io/ConjunctiveGraph')
     >>> RDFLib["Graph"]
-    rdflib.term.URIRef('http://rdflib.net/Graph')
+    rdflib.term.URIRef('https://rdflib.github.io/Graph')
 
 """
 
@@ -2002,7 +2005,7 @@ class Dataset(ConjunctiveGraph):
     >>> for c in ds.graphs():  # doctest: +SKIP
     ...     print(c)  # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
     DEFAULT
-    http://rdlib.net/.well-known/genid/rdflib/N...
+    https://rdflib.github.io/.well-known/genid/rdflib/N...
     http://www.example.com/gr
     >>> # Note that the Dataset.graphs() call returns names of empty graphs,
     >>> # too. This can be restricted:
@@ -2046,10 +2049,12 @@ class Dataset(ConjunctiveGraph):
 
     def graph(self, identifier=None, base=None):
         if identifier is None:
-            from rdflib.term import rdflib_skolem_genid
+            from rdflib.term import _SKOLEM_DEFAULT_AUTHORITY, rdflib_skolem_genid
 
             self.bind(
-                "genid", "http://rdflib.net" + rdflib_skolem_genid, override=False
+                "genid",
+                _SKOLEM_DEFAULT_AUTHORITY + rdflib_skolem_genid,
+                override=False,
             )
             identifier = BNode().skolemize()
 
diff --git a/rdflib/parser.py b/rdflib/parser.py
index 6bce4842..ceee51d4 100644
--- a/rdflib/parser.py
+++ b/rdflib/parser.py
@@ -31,6 +31,7 @@ from urllib.parse import urljoin
 from urllib.request import Request, url2pathname, urlopen
 from xml.sax import xmlreader
 
+import rdflib.util
 from rdflib import __version__
 from rdflib.namespace import Namespace
 from rdflib.term import URIRef
@@ -183,7 +184,8 @@ class StringInputSource(InputSource):
 
 
 headers = {
-    "User-agent": "rdflib-%s (http://rdflib.net/; eikeon@eikeon.com)" % __version__
+    "User-agent": "rdflib-%s (https://rdflib.github.io/; eikeon@eikeon.com)"
+    % __version__
 }
 
 
@@ -447,7 +449,7 @@ def _create_input_source_from_location(
 
     base = pathlib.Path.cwd().as_uri()
 
-    absolute_location = URIRef(location, base=base)
+    absolute_location = URIRef(rdflib.util._iri2uri(location), base=base)
 
     if absolute_location.startswith("file:///"):
         filename = url2pathname(absolute_location.replace("file:///", "/"))
diff --git a/rdflib/term.py b/rdflib/term.py
index 30dd7fc6..c82df3d5 100644
--- a/rdflib/term.py
+++ b/rdflib/term.py
@@ -75,6 +75,8 @@ if TYPE_CHECKING:
     from .namespace import NamespaceManager
     from .paths import AlternativePath, InvPath, NegatedPath, Path, SequencePath
 
+_SKOLEM_DEFAULT_AUTHORITY = "https://rdflib.github.io"
+
 logger = logging.getLogger(__name__)
 skolem_genid = "/.well-known/genid/"
 rdflib_skolem_genid = "/.well-known/genid/rdflib/"
@@ -482,7 +484,7 @@ class BNode(IdentifiedNode):
         .. versionadded:: 4.0
         """
         if authority is None:
-            authority = "http://rdlib.net/"
+            authority = _SKOLEM_DEFAULT_AUTHORITY
         if basepath is None:
             basepath = rdflib_skolem_genid
         skolem = "%s%s" % (basepath, str(self))
diff --git a/rdflib/util.py b/rdflib/util.py
index 246b5cc8..b73a9594 100644
--- a/rdflib/util.py
+++ b/rdflib/util.py
@@ -36,6 +36,7 @@ from typing import (
     Tuple,
     TypeVar,
 )
+from urllib.parse import quote, urlsplit, urlunsplit
 
 import rdflib.graph  # avoid circular dependency
 from rdflib.compat import sign
@@ -58,6 +59,7 @@ __all__ = [
     "find_roots",
     "get_tree",
     "_coalesce",
+    "_iri2uri",
 ]
 
 
@@ -476,3 +478,36 @@ def _coalesce(*args: Optional[_AnyT]) -> Optional[_AnyT]:
         if arg is not None:
             return arg
     return None
+
+
+def _iri2uri(iri: str) -> str:
+    """
+    Convert an IRI to a URI (Python 3).
+    https://stackoverflow.com/a/42309027
+    https://stackoverflow.com/a/40654295
+    netloc should be encoded using IDNA;
+    non-ascii URL path should be encoded to UTF-8 and then percent-escaped;
+    non-ascii query parameters should be encoded to the encoding of a page
+    URL was extracted from (or to the encoding server uses), then
+    percent-escaped.
+    >>> _iri2uri("https://dbpedia.org/resource/Almería")
+    'https://dbpedia.org/resource/Almer%C3%ADa'
+    """
+
+    (scheme, netloc, path, query, fragment) = urlsplit(iri)
+
+    # Just support http/https, otherwise return the iri unmolested
+    if scheme not in ["http", "https"]:
+        return iri
+
+    scheme = quote(scheme)
+    netloc = quote(netloc.encode("idna").decode("utf-8"))
+    path = quote(path)
+    query = quote(query)
+    fragment = quote(fragment)
+    uri = urlunsplit((scheme, netloc, path, query, fragment))
+
+    if iri.endswith("#") and not uri.endswith("#"):
+        uri += "#"
+
+    return uri


Test patch:
diff --git a/test/jsonld/test_localsuite.py b/test/jsonld/test_localsuite.py
index 8088e07c..117efe8c 100644
--- a/test/jsonld/test_localsuite.py
+++ b/test/jsonld/test_localsuite.py
@@ -8,7 +8,7 @@ from rdflib.term import URIRef
 
 from . import runner
 
-TC_BASE = "http://rdflib.net/rdflib-jsonld/local-testsuite/"
+TC_BASE = "https://rdflib.github.io/rdflib-jsonld/local-testsuite/"
 
 
 testsuite_dir = p.join(p.abspath(p.dirname(__file__)), "local-suite")
diff --git a/test/test_dataset/test_dataset.py b/test/test_dataset/test_dataset.py
index f09bcb61..577e289c 100644
--- a/test/test_dataset/test_dataset.py
+++ b/test/test_dataset/test_dataset.py
@@ -7,7 +7,7 @@ from test.data import context1, likes, pizza, tarek
 import pytest
 
 from rdflib import URIRef, plugin
-from rdflib.graph import DATASET_DEFAULT_GRAPH_ID, Dataset
+from rdflib.graph import DATASET_DEFAULT_GRAPH_ID, Dataset, Graph, Namespace
 
 # Will also run SPARQLUpdateStore tests against local SPARQL1.1 endpoint if
 # available. This assumes SPARQL1.1 query/update endpoints running locally at
@@ -231,3 +231,38 @@ def test_iter(get_dataset):
         i_new += 1
 
     assert i_new == i_trad  # both should be 3
+
+
+EGSCHEMA = Namespace("example:")
+
+
+def test_subgraph_without_identifier() -> None:
+    """
+    Graphs with no identifies assigned are identified by Skolem IRIs with a
+    prefix that is bound to `genid`.
+
+    TODO: This is somewhat questionable and arbitrary behaviour and should be
+    reviewed at some point.
+    """
+
+    dataset = Dataset()
+
+    nman = dataset.namespace_manager
+
+    genid_prefix = URIRef("https://rdflib.github.io/.well-known/genid/rdflib/")
+
+    namespaces = set(nman.namespaces())
+    assert (
+        next((namespace for namespace in namespaces if namespace[0] == "genid"), None)
+        is None
+    )
+
+    subgraph: Graph = dataset.graph()
+    subgraph.add((EGSCHEMA["subject"], EGSCHEMA["predicate"], EGSCHEMA["object"]))
+
+    namespaces = set(nman.namespaces())
+    assert next(
+        (namespace for namespace in namespaces if namespace[0] == "genid"), None
+    ) == ("genid", genid_prefix)
+
+    assert f"{subgraph.identifier}".startswith(genid_prefix)
diff --git a/test/test_graph/test_graph_http.py b/test/test_graph/test_graph_http.py
index 1ef2fea8..1ae203d0 100644
--- a/test/test_graph/test_graph_http.py
+++ b/test/test_graph/test_graph_http.py
@@ -1,12 +1,15 @@
 import re
 from http.server import BaseHTTPRequestHandler
+from test.data import TEST_DATA_DIR
 from test.utils import GraphHelper
+from test.utils.graph import cached_graph
 from test.utils.httpservermock import (
     MethodName,
     MockHTTPResponse,
     ServedBaseHTTPServerMock,
     ctx_http_server,
 )
+from typing import Generator
 from urllib.error import HTTPError
 
 import pytest
@@ -227,3 +230,37 @@ class TestGraphHTTP:
                 graph.parse(location=url, format="turtle")
 
             assert raised.value.code == 500
+
+
+@pytest.fixture(scope="module")
+def module_httpmock() -> Generator[ServedBaseHTTPServerMock, None, None]:
+    with ServedBaseHTTPServerMock() as httpmock:
+        yield httpmock
+
+
+@pytest.fixture(scope="function")
+def httpmock(
+    module_httpmock: ServedBaseHTTPServerMock,
+) -> Generator[ServedBaseHTTPServerMock, None, None]:
+    module_httpmock.reset()
+    yield module_httpmock
+
+
+def test_iri_source(httpmock: ServedBaseHTTPServerMock) -> None:
+    diverse_triples_path = TEST_DATA_DIR / "variants/diverse_triples.ttl"
+
+    httpmock.responses[MethodName.GET].append(
+        MockHTTPResponse(
+            200,
+            "OK",
+            diverse_triples_path.read_bytes(),
+            {"Content-Type": ["text/turtle"]},
+        )
+    )
+    g = Graph()
+    g.parse(f"{httpmock.url}/resource/Almería")
+    assert httpmock.call_count == 1
+    GraphHelper.assert_triple_sets_equals(cached_graph((diverse_triples_path,)), g)
+
+    req = httpmock.requests[MethodName.GET].pop(0)
+    assert req.path == "/resource/Almer%C3%ADa"
diff --git a/test/test_graph/test_skolemization.py b/test/test_graph/test_skolemization.py
index f045bf8d..aee86bae 100644
--- a/test/test_graph/test_skolemization.py
+++ b/test/test_graph/test_skolemization.py
@@ -22,10 +22,15 @@ base_triples = {
     [
         (URIRef("http://example.com"), None),
         (Literal("some string in here ..."), None),
-        (BNode("GMeng4V7"), "http://rdlib.net/.well-known/genid/rdflib/GMeng4V7"),
+        (
+            BNode("GMeng4V7"),
+            "https://rdflib.github.io/.well-known/genid/rdflib/GMeng4V7",
+        ),
         (
             BNode(),
-            re.compile("^" + re.escape("http://rdlib.net/.well-known/genid/rdflib/")),
+            re.compile(
+                "^" + re.escape("https://rdflib.github.io/.well-known/genid/rdflib/")
+            ),
         ),
     ],
 )
@@ -61,9 +66,9 @@ def test_skolemization(
     [
         ("http://example.com", None),
         ("http://example.com/not/.well-known/genid/1", None),
-        ("http://rdlib.net/not/.well-known/genid/1", None),
+        ("https://rdflib.github.io/not/.well-known/genid/1", None),
         ("http://example.com/.well-known/genid/1", re.compile("^N")),
-        ("http://rdlib.net/.well-known/genid/rdflib/GMeng4V7", "GMeng4V7"),
+        ("https://rdflib.github.io/.well-known/genid/rdflib/GMeng4V7", "GMeng4V7"),
     ],
 )
 def test_deskolemization(
diff --git a/test/test_skolem_genid.py b/test/test_skolem_genid.py
index 7123fba4..ee88f5a8 100644
--- a/test/test_skolem_genid.py
+++ b/test/test_skolem_genid.py
@@ -4,7 +4,7 @@ from rdflib.term import Genid, RDFLibGenid
 
 def test_skolem_genid_and_rdflibgenid():
     rdflib_genid = URIRef(
-        "http://rdflib.net/.well-known/genid/rdflib/N97c39b957bc444949a82793519348dc2"
+        "https://rdflib.github.io/.well-known/genid/rdflib/N97c39b957bc444949a82793519348dc2"
     )
     custom_genid = URIRef(
         "http://example.com/.well-known/genid/example/Ne864c0e3684044f381d518fdac652f2e"
diff --git a/test/test_util.py b/test/test_util.py
index 16ecbcb3..3ca54270 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -15,7 +15,7 @@ from rdflib import XSD, util
 from rdflib.graph import ConjunctiveGraph, Graph, QuotedGraph
 from rdflib.namespace import RDF, RDFS
 from rdflib.term import BNode, IdentifiedNode, Literal, Node, URIRef
-from rdflib.util import _coalesce, find_roots, get_tree
+from rdflib.util import _coalesce, _iri2uri, find_roots, get_tree
 
 n3source = """\
 @prefix : <http://www.w3.org/2000/10/swap/Primer#>.
@@ -547,3 +547,80 @@ def test_get_tree(
         assert catcher.value is not None
     else:
         assert expected_result == result
+
+
+@pytest.mark.parametrize(
+    ["iri", "expected_result"],
+    [
+        (
+            "https://example.com/resource/Almería",
+            {
+                "https://example.com/resource/Almer%C3%ADa",
+            },
+        ),
+        (
+            "https://example.com/resource/Almeria",
+            {
+                "https://example.com/resource/Almeria",
+            },
+        ),
+        (
+            "https://åæø.example.com/",
+            {
+                "https://xn--5cac8c.example.com/",
+            },
+        ),
+        (
+            # Note: expected result is the same because the function only works
+            # for http and https.
+            "example:é",
+            {
+                "example:é",
+            },
+        ),
+        (
+            # Note: expected result is the same because the function only works
+            # for http and https.
+            "urn:example:é",
+            {
+                "urn:example:é",
+            },
+        ),
+        (
+            "http://example.com/?é=1",
+            {
+                "http://example.com/?%C3%A9=1",
+                "http://example.com/?%C3%A9%3D1",
+            },
+        ),
+        (
+            "http://example.com/#é",
+            {
+                "http://example.com/#%C3%A9",
+            },
+        ),
+        (
+            "http://example.com/é#",
+            {
+                "http://example.com/%C3%A9#",
+            },
+        ),
+    ],
+)
+def test_iri2uri(iri: str, expected_result: Union[Set[str], Type[Exception]]) -> None:
+    """
+    Tests that
+    """
+    catcher: Optional[pytest.ExceptionInfo[Exception]] = None
+
+    with ExitStack() as xstack:
+        if isinstance(expected_result, type) and issubclass(expected_result, Exception):
+            catcher = xstack.enter_context(pytest.raises(expected_result))
+        result = _iri2uri(iri)
+        logging.debug("result = %s", result)
+    if catcher is not None:
+        assert catcher is not None
+        assert catcher.value is not None
+    else:
+        assert isinstance(expected_result, set)
+        assert result in expected_result
diff --git a/test/test_w3c_spec/test_rdfxml_w3c.py b/test/test_w3c_spec/test_rdfxml_w3c.py
index 9a5a4c6a..cfd940d2 100644
--- a/test/test_w3c_spec/test_rdfxml_w3c.py
+++ b/test/test_w3c_spec/test_rdfxml_w3c.py
@@ -230,7 +230,7 @@ FOAF = Namespace("http://xmlns.com/foaf/0.1/")
 results = Graph()
 
 system = BNode("system")
-results.add((system, FOAF["homepage"], URIRef("http://rdflib.net/")))
+results.add((system, FOAF["homepage"], URIRef("https://rdflib.github.io/")))
 results.add((system, RDFS.label, Literal("RDFLib")))
 results.add((system, RDFS.comment, Literal("")))