Repository: DOV-Vlaanderen/pydov

Issue:
Support Join with a dataframe containing a single element too
<!-- You can ask questions about the DOV webservices or about the `pydov` package. If you have a question about the `pydov` Python package, please use following template. -->

* PyDOV version: master
* Python version: 3.6
* Operating System: Windows 10

### Description

When you use Join with a dataframe with only one (unique) element, you get an error. It would be nice if the Join would work with a dataframe containing only a single element too.

### What I Did

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-41-455ef57ac5bc> in <module>()
      4 gwl = GrondwaterLocatieSearch()
      5 
----> 6 gwl.search(Join(df, 'pkey_grondwaterlocatie'))

c:\projecten\pydov\pydov_git\pydov\util\query.py in __init__(self, dataframe, on, using)
    101         if len(set(value_list)) < 2:
    102             raise ValueError("dataframe should contain at least two "
--> 103                              "different values in column '{}'.".format(using))
    104 
    105         super(Join, self).__init__(on, value_list)

ValueError: dataframe should contain at least two different values in column 'pkey_grondwaterlocatie'.
```


Discussion:

Patch:
diff --git a/pydov/util/query.py b/pydov/util/query.py
index f7cd9a2..dae02b5 100644
--- a/pydov/util/query.py
+++ b/pydov/util/query.py
@@ -7,7 +7,7 @@ from owslib.fes import (
 )
 
 
-class PropertyInList(Or):
+class PropertyInList(object):
     """Filter expression to test whether a given property has one of the
     values from a list.
 
@@ -33,19 +33,30 @@ class PropertyInList(Or):
         Raises
         ------
         ValueError
-            If the given list does not contain at least two distinct items.
+            If the given list does not contain at least a single item.
 
         """
         if not isinstance(lst, list) and not isinstance(lst, set):
             raise ValueError('list should be of type "list" or "set"')
 
-        if len(set(lst)) < 2:
-            raise ValueError('list should contain at least two different '
-                             'elements.')
+        if len(set(lst)) < 1:
+            raise ValueError('list should contain at least a single item')
+        elif len(set(lst)) == 1:
+            self.query = PropertyIsEqualTo(propertyname, set(lst).pop())
+        else:
+            self.query = Or(
+                [PropertyIsEqualTo(propertyname, i) for i in set(lst)])
 
-        super(PropertyInList, self).__init__(
-            [PropertyIsEqualTo(propertyname, i) for i in set(lst)]
-        )
+    def toXML(self):
+        """Return the XML representation of the PropertyInList query.
+
+        Returns
+        -------
+        xml : etree.ElementTree
+            XML representation of the PropertyInList
+
+        """
+        return self.query.toXML()
 
 
 class Join(PropertyInList):
@@ -83,9 +94,8 @@ class Join(PropertyInList):
             If `using` is None and the `on` column is not present in the
             dataframe.
 
-            If the dataframe does not contain at least two different values
-            in the `using` column. A Join is probably overkill here,
-            use PropertyIsEqualTo instead.
+            If the dataframe does not contain at least a single non-null value
+            in the `using` column.
 
         """
         if using is None:
@@ -98,8 +108,8 @@ class Join(PropertyInList):
 
         value_list = list(dataframe[using].dropna().unique())
 
-        if len(set(value_list)) < 2:
-            raise ValueError("dataframe should contain at least two "
-                             "different values in column '{}'.".format(using))
+        if len(set(value_list)) < 1:
+            raise ValueError("dataframe should contain at least a single "
+                             "value in column '{}'.".format(using))
 
         super(Join, self).__init__(on, value_list)


Test patch:
diff --git a/tests/test_util_query.py b/tests/test_util_query.py
index c14c1ed..750acc3 100644
--- a/tests/test_util_query.py
+++ b/tests/test_util_query.py
@@ -1,5 +1,6 @@
 """Module grouping tests for the pydov.util.query module."""
 import pandas as pd
+import numpy as np
 import pytest
 
 from pydov.util.query import (
@@ -67,26 +68,63 @@ class TestPropertyInList(object):
 
         assert len(l_output) == 0
 
-    def test_tooshort(self):
+    def test_list_single(self):
         """Test the PropertyInList expression with a list containing
         a single item.
 
-        Test whether a ValueError is raised.
+        Test whether the generated query is correct and does contain only a
+        single PropertyIsEqualTo.
 
         """
-        with pytest.raises(ValueError):
-            l = ['a']
-            PropertyInList('methode', l)
+        l = ['a']
+
+        query = PropertyInList('methode', l)
+        xml = query.toXML()
+
+        assert xml.tag == '{http://www.opengis.net/ogc}PropertyIsEqualTo'
+
+        propertyname = xml.find('./{http://www.opengis.net/ogc}PropertyName')
+        assert propertyname.text == 'methode'
 
-    def test_tooshort_duplicate(self):
+        literal = xml.find('./{http://www.opengis.net/ogc}Literal')
+        assert literal.text in l
+
+        l.remove(literal.text)
+        assert len(l) == 0
+
+    def test_list_single_duplicate(self):
         """Test the PropertyInList expression with a list containing
-        a two identical items.
+        a single duplicated item.
+
+        Test whether the generated query is correct and does contain only a
+        single PropertyIsEqualTo.
+
+        """
+        l = ['a', 'a']
+        l_output = ['a']
+
+        query = PropertyInList('methode', l)
+        xml = query.toXML()
+
+        assert xml.tag == '{http://www.opengis.net/ogc}PropertyIsEqualTo'
+
+        propertyname = xml.find('./{http://www.opengis.net/ogc}PropertyName')
+        assert propertyname.text == 'methode'
+
+        literal = xml.find('./{http://www.opengis.net/ogc}Literal')
+        assert literal.text in l_output
+
+        l_output.remove(literal.text)
+        assert len(l_output) == 0
+
+    def test_emptylist(self):
+        """Test the PropertyInList expression with an empty list.
 
         Test whether a ValueError is raised.
 
         """
         with pytest.raises(ValueError):
-            l = ['a', 'a']
+            l = []
             PropertyInList('methode', l)
 
     def test_nolist(self):
@@ -194,38 +232,77 @@ class TestJoin(object):
 
             Join(df, 'pkey_sondering')
 
-    def test_tooshort(self):
+    def test_single(self):
         """Test the Join expression with a dataframe containing a single row.
 
-        Test whether a ValueError is raised.
+        Test whether the generated query is correct and does contain only a
+        single PropertyIsEqualTo.
 
         """
-        with pytest.raises(ValueError):
-            l = ['https://www.dov.vlaanderen.be/data/boring/1986-068853']
+        l = ['https://www.dov.vlaanderen.be/data/boring/1986-068853']
 
-            df = pd.DataFrame({
-                'pkey_boring': pd.Series(l),
-                'diepte_tot_m': pd.Series([10])
-            })
+        df = pd.DataFrame({
+            'pkey_boring': pd.Series(l),
+            'diepte_tot_m': pd.Series([10])
+        })
 
-            Join(df, 'pkey_boring')
+        query = Join(df, 'pkey_boring')
+        xml = query.toXML()
+
+        assert xml.tag == '{http://www.opengis.net/ogc}PropertyIsEqualTo'
+
+        propertyname = xml.find('./{http://www.opengis.net/ogc}PropertyName')
+        assert propertyname.text == 'pkey_boring'
 
-    def test_tooshort_duplicate(self):
+        literal = xml.find('./{http://www.opengis.net/ogc}Literal')
+        assert literal.text in l
+
+        l.remove(literal.text)
+        assert len(l) == 0
+
+    def test_single_duplicate(self):
         """Test the Join expression with a dataframe containing two
         identical keys.
 
-        Test whether a ValueError is raised.
+        Test whether the generated query is correct and does contain only a
+        single PropertyIsEqualTo.
 
         """
-        with pytest.raises(ValueError):
-            l = ['https://www.dov.vlaanderen.be/data/boring/1986-068853',
-                 'https://www.dov.vlaanderen.be/data/boring/1986-068853']
+        l = ['https://www.dov.vlaanderen.be/data/boring/1986-068853',
+             'https://www.dov.vlaanderen.be/data/boring/1986-068853']
+        l_output = ['https://www.dov.vlaanderen.be/data/boring/1986-068853']
 
-            df = pd.DataFrame({
-                'pkey_boring': pd.Series(l),
-                'diepte_tot_m': pd.Series([10, 20])
-            })
+        df = pd.DataFrame({
+            'pkey_boring': pd.Series(l),
+            'diepte_tot_m': pd.Series([10, 20])
+        })
+
+        query = Join(df, 'pkey_boring')
+        xml = query.toXML()
+
+        assert xml.tag == '{http://www.opengis.net/ogc}PropertyIsEqualTo'
+
+        propertyname = xml.find('./{http://www.opengis.net/ogc}PropertyName')
+        assert propertyname.text == 'pkey_boring'
+
+        literal = xml.find('./{http://www.opengis.net/ogc}Literal')
+        assert literal.text in l_output
+
+        l_output.remove(literal.text)
+        assert len(l_output) == 0
+
+    def test_empty(self):
+        """Test the Join expression with an empty dataframe.
+
+        Test whether a ValueError is raised
+
+        """
+        df = pd.DataFrame({
+            'pkey_boring': [np.nan, np.nan],
+            'diepte_tot_m': pd.Series([10, 20])
+        })
 
+        with pytest.raises(ValueError):
             Join(df, 'pkey_boring')
 
     def test_on(self):