Repository: RDFLib/rdflib

Issue:
Add __iter__ method to Dataset
#### Motivation

It is convenient to be able to iterate over all triples in a `Graph` instance without invoking `graph.triples((None, None, None))`. When working with a small instance of `Dataset`, there are occasions where one will want to observe all of the quads in a similar manner.

#### Notes

This is trivial to implement by simply having an introduced `__iter__` method invoke `self.quads((None, None, None, None))` in exactly [the same way that it is done in `Graph`](https://github.com/RDFLib/rdflib/blob/538446e08f992b7fc333151367dd216611dcd8e3/rdflib/graph.py#L500).

Discussion:

Patch:
diff --git a/rdflib/graph.py b/rdflib/graph.py
index a689b35a..b25765ac 100644
--- a/rdflib/graph.py
+++ b/rdflib/graph.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union, Type, cast, overload
+from typing import Optional, Union, Type, cast, overload, Generator, Tuple
 import logging
 from warnings import warn
 import random
@@ -29,6 +29,9 @@ assert Namespace  # avoid warning
 
 logger = logging.getLogger(__name__)
 
+# Type aliases to make unpacking what's going on a little more human friendly
+ContextNode = Union[BNode, URIRef]
+DatasetQuad = Tuple[Node, URIRef, Node, Optional[ContextNode]]
 
 __doc__ = """\
 
@@ -1759,8 +1762,8 @@ class Dataset(ConjunctiveGraph):
      rdflib.term.URIRef("http://example.org/y"),
      rdflib.term.Literal("bar"))
     >>>
-    >>> # querying quads return quads; the fourth argument can be unrestricted
-    >>> # or restricted to a graph
+    >>> # querying quads() return quads; the fourth argument can be unrestricted
+    >>> # (None) or restricted to a graph
     >>> for q in ds.quads((None, None, None, None)):  # doctest: +SKIP
     ...     print(q)  # doctest: +NORMALIZE_WHITESPACE
     (rdflib.term.URIRef("http://example.org/a"),
@@ -1776,7 +1779,24 @@ class Dataset(ConjunctiveGraph):
      rdflib.term.Literal("foo-bar"),
      rdflib.term.URIRef("http://www.example.com/gr"))
     >>>
-    >>> for q in ds.quads((None,None,None,g)):  # doctest: +SKIP
+    >>> # unrestricted looping is equivalent to iterating over the entire Dataset
+    >>> for q in ds:  # doctest: +SKIP
+    ...     print(q)  # doctest: +NORMALIZE_WHITESPACE
+    (rdflib.term.URIRef("http://example.org/a"),
+     rdflib.term.URIRef("http://www.example.org/b"),
+     rdflib.term.Literal("foo"),
+     None)
+    (rdflib.term.URIRef("http://example.org/x"),
+     rdflib.term.URIRef("http://example.org/y"),
+     rdflib.term.Literal("bar"),
+     rdflib.term.URIRef("http://www.example.com/gr"))
+    (rdflib.term.URIRef("http://example.org/x"),
+     rdflib.term.URIRef("http://example.org/z"),
+     rdflib.term.Literal("foo-bar"),
+     rdflib.term.URIRef("http://www.example.com/gr"))
+    >>>
+    >>> # resticting iteration to a graph:
+    >>> for q in ds.quads((None, None, None, g)):  # doctest: +SKIP
     ...     print(q)  # doctest: +NORMALIZE_WHITESPACE
     (rdflib.term.URIRef("http://example.org/x"),
      rdflib.term.URIRef("http://example.org/y"),
@@ -1896,6 +1916,11 @@ class Dataset(ConjunctiveGraph):
             else:
                 yield s, p, o, c.identifier
 
+    def __iter__(self) -> Generator[DatasetQuad, None, None]:
+        """Iterates over all quads in the store"""
+        return self.quads((None, None, None, None))
+
+
 
 class QuotedGraph(Graph):
     """


Test patch:
diff --git a/test/test_dataset.py b/test/test_dataset.py
index 734b58cd..4860b9c3 100644
--- a/test/test_dataset.py
+++ b/test/test_dataset.py
@@ -167,7 +167,36 @@ class DatasetTestCase(unittest.TestCase):
         self.assertEqual(list(self.graph.objects(self.tarek, None)), [])
         self.assertEqual(list(g1.objects(self.tarek, None)), [self.pizza])
 
-
+    def testIter(self):
+        """PR 1382: adds __iter__ to Dataset"""
+        d = Dataset()
+        uri_a = URIRef("https://example.com/a")
+        uri_b = URIRef("https://example.com/b")
+        uri_c = URIRef("https://example.com/c")
+        uri_d = URIRef("https://example.com/d")
+
+        d.add_graph(URIRef("https://example.com/g1"))
+        d.add((uri_a, uri_b, uri_c, URIRef("https://example.com/g1")))
+        d.add((uri_a, uri_b, uri_c, URIRef(
+            "https://example.com/g1")))  # pointless addition: duplicates above
+
+        d.add_graph(URIRef("https://example.com/g2"))
+        d.add((uri_a, uri_b, uri_c, URIRef("https://example.com/g2")))
+        d.add((uri_a, uri_b, uri_d, URIRef("https://example.com/g1")))  # new, uri_d
+
+        # traditional iterator
+        i_trad = 0
+        for t in d.quads((None, None, None)):
+            i_trad += 1
+
+        # new Dataset.__iter__ iterator
+        i_new = 0
+        for t in d:
+            i_new += 1
+
+        self.assertEqual(i_new, i_trad)  # both should be 3
+        
+        
 # dynamically create classes for each registered Store
 
 pluginname = None