Repository: RDFLib/rdflib

Issue:
Turtle de/serialize incorrectly interprets gYear
In 5.0.0, when a `gYear` is parsed and then re-serialised, from any format to any other format, RDFlib adds "-01-01" to the literal so, for example, `"1982"^^xsd:gYear` goes to `"1982-01-01"^^xsd:gYear` which is invalid.

Testing code:

```python
from rdflib import Graph

data =  """
        PREFIX sdo: <https://schema.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

        <x:> sdo:startDate "1982"^^xsd:gYear .
        """

g = Graph().parse(data=data, format="ttl")
assert "1982-01-01" not in g.serialize(format="ttl").decode()


data2 = """<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:sdo="https://schema.org/"
>
  <rdf:Description rdf:about="x:">
    <sdo:startDate rdf:datatype="http://www.w3.org/2001/XMLSchema#gYear">1982</sdo:startDate>
  </rdf:Description>
</rdf:RDF>"""

g2 = Graph().parse(data=data2, format="xml")
assert "1982-01-01" not in g2.serialize(format="xml").decode()
```

Comments:

Patch:
diff --git a/docs/intro_to_sparql.rst b/docs/intro_to_sparql.rst
index 97c7f281..8c7efd34 100644
--- a/docs/intro_to_sparql.rst
+++ b/docs/intro_to_sparql.rst
@@ -17,56 +17,62 @@ Queries can be evaluated against a graph with the
 :meth:`rdflib.graph.Graph.update`.
 
 The query method returns a :class:`rdflib.query.Result` instance. For
-SELECT queries, iterating over this return
+SELECT queries, iterating over this returns
 :class:`rdflib.query.ResultRow` instances, each containing a set of
 variable bindings. For CONSTRUCT/DESCRIBE queries, iterating over the
-result object gives the triples. For ASK queries, iterating will yield
-the single boolean answer, or evaluating the result object in a
-boolean-context (i.e. ``bool(result)``)
+result object yields triples. For ASK queries, the single Boolean 
+answer is obtained by iterating over the result object or by 
+evaluating the result object in a Boolean context 
+(i.e. ``bool(result)``).
 
-Continuing the example...
+Consider the example...
 
 .. code-block:: python
 
     import rdflib
-
     g = rdflib.Graph()
+    g.parse("http://danbri.org/foaf.rdf#")
 
-    # ... add some triples to g somehow ...
-    g.parse("some_foaf_file.rdf")
-
-    qres = g.query(
-        """SELECT DISTINCT ?aname ?bname
-           WHERE {
-              ?a foaf:knows ?b .
-              ?a foaf:name ?aname .
-              ?b foaf:name ?bname .
-           }""")
+    knows_query = """
+    SELECT DISTINCT ?aname ?bname
+    WHERE {
+        ?a foaf:knows ?b .
+        ?a foaf:name ?aname .
+        ?b foaf:name ?bname .
+    }"""
 
-    for row in qres:
+    for row in g.query(knows_query):
         print("%s knows %s" % row)
 
 The results are tuples of values in the same order as your SELECT
-arguments.  Alternatively, the values can be accessed by variable
-name, either as attributes, or as items: ``row.b`` and ``row["b"]`` is
-equivalent.
+arguments.  The values can be accessed individually by variable
+name, either as attributes (``row.b``) or as items (``row["b"]``).
 
 .. code-block:: text
 
-    Timothy Berners-Lee knows Edd Dumbill
-    Timothy Berners-Lee knows Jennifer Golbeck
-    Timothy Berners-Lee knows Nicholas Gibbins
-    Timothy Berners-Lee knows Nigel Shadbolt
-    Dan Brickley knows binzac
-    Timothy Berners-Lee knows Eric Miller
-    Drew Perttula knows David McClosky
-    Timothy Berners-Lee knows Dan Connolly
+    Dan Brickley knows Tim Berners-Lee
+    Dan Brickley knows Dean Jackson
+    Dan Brickley knows Mischa Tuffield
+    Dan Brickley knows Ludovic Hirlimann
+    Dan Brickley knows Libby Miller
     ...
 
 As an alternative to using ``PREFIX`` in the SPARQL query, namespace
-bindings can be passed in with the ``initNs`` kwarg, see
-:doc:`namespaces_and_bindings`.
+bindings can be passed in with the ``initNs`` kwarg (see
+:doc:`namespaces_and_bindings`):
 
+.. code-block:: python
+
+    import rdflib 
+    from rdflib import FOAF
+    g = rdflib.Graph()
+    g.parse("http://danbri.org/foaf.rdf#")
+ 
+    result = g.query(knows_query, initNs={ 'foaf': FOAF })
+ 
+    for row in result:
+        print(f"{row.aname} knows {row['bname']}")
+ 
 Variables can also be pre-bound, using ``initBindings`` kwarg can be
 used to pass in a ``dict`` of initial bindings, this is particularly
 useful for prepared queries, as described below.
diff --git a/docs/sphinx-requirements.txt b/docs/sphinx-requirements.txt
index 48ba7549..b4f0ddd9 100644
--- a/docs/sphinx-requirements.txt
+++ b/docs/sphinx-requirements.txt
@@ -1,3 +1,3 @@
-sphinx==4.0.1
+sphinx==4.0.2
 sphinxcontrib-apidoc
 git+https://github.com/gniezen/n3pygments.git
diff --git a/rdflib/term.py b/rdflib/term.py
index 0f627e69..d9e06d82 100644
--- a/rdflib/term.py
+++ b/rdflib/term.py
@@ -1437,6 +1437,8 @@ _XSD_YEARMONTHDURATION = URIRef(_XSD_PFX + "yearMonthDuration")
 _OWL_RATIONAL = URIRef("http://www.w3.org/2002/07/owl#rational")
 _XSD_B64BINARY = URIRef(_XSD_PFX + "base64Binary")
 _XSD_HEXBINARY = URIRef(_XSD_PFX + "hexBinary")
+_XSD_GYEAR = URIRef(_XSD_PFX + "gYear")
+_XSD_GYEARMONTH = URIRef(_XSD_PFX + "gYearMonth")
 # TODO: gYearMonth, gYear, gMonthDay, gDay, gMonth
 
 _NUMERIC_LITERAL_TYPES = (
@@ -1558,6 +1560,8 @@ _GenericPythonToXSDRules = [
 ]
 
 _SpecificPythonToXSDRules = [
+    ((date, _XSD_GYEAR), lambda val: val.strftime("%04Y")),
+    ((date, _XSD_GYEARMONTH), lambda val: val.strftime("%04Y-%02m")),
     ((str, _XSD_HEXBINARY), hexlify),
     ((bytes, _XSD_HEXBINARY), hexlify),
     ((str, _XSD_B64BINARY), b64encode),


Test patch:
diff --git a/test/test_issue977.py b/test/test_issue977.py
new file mode 100644
index 00000000..26f61c5a
--- /dev/null
+++ b/test/test_issue977.py
@@ -0,0 +1,30 @@
+import unittest
+
+from rdflib import Graph, XSD, RDF, RDFS, URIRef, Literal
+
+
+class TestIssue977(unittest.TestCase):
+
+    def setUp(self):
+        self.graph = Graph()
+        # Bind prefixes.
+        self.graph.bind('isbn', 'urn:isbn:')
+        self.graph.bind('webn', 'http://w3c.org/example/isbn/')
+        # Populate graph.
+        self.graph.add((URIRef('urn:isbn:1503280780'), RDFS.label, Literal('Moby Dick')))
+        self.graph.add((URIRef('http://w3c.org/example/isbn/1503280780'), RDFS.label, Literal('Moby Dick')))        
+
+    def test_namespace_manager(self):
+        assert 'isbn', 'urn:isbn:' in tuple(self.graph.namespaces())
+        assert 'webn', 'http://w3c.org/example/isbn/' in tuple(self.graph.namespaces())
+
+    def test_turtle_serialization(self):
+        serialization = self.graph.serialize(None, format='turtle')
+        print(f'Test Issue 977, serialization output:\n---\n{serialization}---')
+        # Test serialization.
+        assert '@prefix webn:' in serialization, "Prefix webn not found in serialization!"
+        assert '@prefix isbn:' in serialization, "Prefix isbn not found in serialization!"
+
+
+if __name__ == "__main__":
+	unittest.main()
diff --git a/test/test_literal.py b/test/test_literal.py
index 656bfb10..98cd18a8 100644
--- a/test/test_literal.py
+++ b/test/test_literal.py
@@ -1,7 +1,9 @@
 import unittest
+import datetime
 
 import rdflib  # needed for eval(repr(...)) below
 from rdflib.term import Literal, URIRef, _XSD_DOUBLE, bind, _XSD_BOOLEAN
+from rdflib.namespace import XSD
 
 
 def uformat(s):
@@ -188,5 +190,79 @@ class TestBindings(unittest.TestCase):
         self.assertEqual(specific_l.datatype, datatype)
 
 
+class TestXsdLiterals(unittest.TestCase):
+    def test_make_literals(self):
+        """
+        Tests literal construction.
+        """
+        inputs = [
+            # these literals do not get conerted to python types
+            ("ABCD", XSD.integer, None),
+            ("ABCD", XSD.gYear, None),
+            ("-10000", XSD.gYear, None),
+            ("-1921-00", XSD.gYearMonth, None),
+            ("1921-00", XSD.gMonthDay, None),
+            ("1921-13", XSD.gMonthDay, None),
+            ("-1921-00", XSD.gMonthDay, None),
+            ("10", XSD.gDay, None),
+            ("-1", XSD.gDay, None),
+            ("0000", XSD.gYear, None),
+            ("0000-00-00", XSD.date, None),
+            ("NOT A VALID HEX STRING", XSD.hexBinary, None),
+            ("NOT A VALID BASE64 STRING", XSD.base64Binary, None),
+            # these literals get converted to python types
+            ("1921-05-01", XSD.date, datetime.date),
+            ("1921-05-01T00:00:00", XSD.dateTime, datetime.datetime),
+            ("1921-05", XSD.gYearMonth, datetime.date),
+            ("0001-01", XSD.gYearMonth, datetime.date),
+            ("0001-12", XSD.gYearMonth, datetime.date),
+            ("2002-01", XSD.gYearMonth, datetime.date),
+            ("9999-01", XSD.gYearMonth, datetime.date),
+            ("9999-12", XSD.gYearMonth, datetime.date),
+            ("1921", XSD.gYear, datetime.date),
+            ("2000", XSD.gYear, datetime.date),
+            ("0001", XSD.gYear, datetime.date),
+            ("9999", XSD.gYear, datetime.date),
+            ("1982", XSD.gYear, datetime.date),
+            ("2002", XSD.gYear, datetime.date),
+            ("1921-05-01T00:00:00+00:30", XSD.dateTime, datetime.datetime),
+            ("1921-05-01T00:00:00-00:30", XSD.dateTime, datetime.datetime),
+            ("abcdef0123", XSD.hexBinary, bytes),
+            ("", XSD.hexBinary, bytes),
+            ("UkRGTGli", XSD.base64Binary, bytes),
+            ("", XSD.base64Binary, bytes),
+        ]
+        self.check_make_literals(inputs)
+
+    @unittest.expectedFailure
+    def test_make_literals_ki(self):
+        """
+        Known issues with literal construction.
+        """
+        inputs = [
+            ("1921-01Z", XSD.gYearMonth, datetime.date),
+            ("1921Z", XSD.gYear, datetime.date),
+            ("1921-00", XSD.gYearMonth, datetime.date),
+            ("1921-05-01Z", XSD.date, datetime.date),
+            ("1921-05-01+00:30", XSD.date, datetime.date),
+            ("1921-05-01+00:30", XSD.date, datetime.date),
+            ("1921-05-01+00:00", XSD.date, datetime.date),
+            ("1921-05-01+00:00", XSD.date, datetime.date),
+            ("1921-05-01T00:00:00Z", XSD.dateTime, datetime.datetime),
+        ]
+        self.check_make_literals(inputs)
+
+    def check_make_literals(self, inputs):
+        for literal_pair in inputs:
+            (lexical, type, value_cls) = literal_pair
+            with self.subTest(f"tesing {literal_pair}"):
+                literal = Literal(lexical, datatype=type)
+                if value_cls is not None:
+                    self.assertIsInstance(literal.value, value_cls)
+                else:
+                    self.assertIsNone(literal.value)
+                self.assertEqual(lexical, f"{literal}")
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/test_parser.py b/test/test_parser.py
index e337969c..da38ea91 100644
--- a/test/test_parser.py
+++ b/test/test_parser.py
@@ -44,5 +44,47 @@ class ParserTestCase(unittest.TestCase):
         self.assertEqual(type, RDFS.Class)
 
 
+class TestGitHubIssues(unittest.TestCase):
+    def test_issue_1228_a(self):
+        data = """
+        PREFIX sdo: <https://schema.org/>
+        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
+
+        <x:> sdo:startDate "1982"^^xsd:gYear .
+        """
+
+        g = Graph().parse(data=data, format="ttl")
+        self.assertNotIn("1982-01-01", data)
+        self.assertNotIn("1982-01-01", g.serialize(format="ttl"))
+
+    def test_issue_1228_b(self):
+        data = """\
+<?xml version="1.0" encoding="UTF-8"?>
+    <rdf:RDF
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:sdo="https://schema.org/"
+    >
+    <rdf:Description rdf:about="x:">
+        <sdo:startDate
+            rdf:datatype="http://www.w3.org/2001/XMLSchema#gYear">1982</sdo:startDate>
+    </rdf:Description>
+</rdf:RDF>"""
+
+        g = Graph().parse(data=data, format="xml")
+        self.assertNotIn("1982-01-01", data)
+        self.assertNotIn("1982-01-01", g.serialize(format="xml"))
+
+    def test_issue_806(self):
+        data = (
+            "<http://dbpedia.org/resource/Australian_Labor_Party> "
+            "<http://dbpedia.org/ontology/formationYear> "
+            '"1891"^^<http://www.w3.org/2001/XMLSchema#gYear> .'
+        )
+        g = Graph()
+        g.parse(data=data, format="nt")
+        for _, _, o in g:
+            self.assertNotIn("1891-01-01", o.n3())
+
+
 if __name__ == "__main__":
     unittest.main()