Repository: cloudant/python-cloudant
Pull request #501 (2021-06-01 16:28:30)

Use custom encoder (if provided) for all view `key` params not just `keys`
<!--
Thanks for your hard work, please ensure all items are complete before opening.
-->
## Checklist

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt)
- [x] Added tests for code changes _or_ test/build only changes
- [x] Updated the change log file (`CHANGES.md`) _or_ test/build only changes
- [x] Completed the PR template below:

## Description

Fixes https://github.com/cloudant/python-cloudant/issues/221#issuecomment-752424499

## Approach

Used custom encoder at `_py_to_couch_translate` for `endkey`, `key`, and `startkey` parameters.

## Schema & API Changes

`endkey`, `key` and `startkey` parameters will be encoded with the custom encoder.

## Security and Privacy

- No change

## Testing

Test that all_docs uses the custom encoder. Fixed `test_valid_endkey`, `test_valid_key` and `test_valid_startkey ` with long type.

## Monitoring and Logging

- No change



Patch:
diff --git a/.travis.yml b/.travis.yml
index b49557a..9f770aa 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,8 +8,6 @@ python:
 env:
   - ADMIN_PARTY=true COUCHDB_VERSION=2.3.1
   - ADMIN_PARTY=false COUCHDB_VERSION=2.3.1
-  - ADMIN_PARTY=true COUCHDB_VERSION=1.7.2
-  - ADMIN_PARTY=false COUCHDB_VERSION=1.7.2
 
 services:
   - docker
diff --git a/CHANGES.md b/CHANGES.md
index 8885ae2..522d5ed 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -5,6 +5,7 @@
 - [FIXED] Fixed result paging for grouped view queries.
 - [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`.
 - [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`.
+- [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`.
 
 # 2.14.0 (2020-08-17)
 
diff --git a/MANIFEST.in b/MANIFEST.in
index 96ebb5e..7c050d9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,1 +1,1 @@
-include requirements.txt VERSION
+include requirements.txt VERSION LICENSE
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index 6e765c1..962f1de 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -160,7 +160,7 @@ def feed_arg_types(feed_type):
         return _COUCH_DB_UPDATES_ARG_TYPES
     return _CHANGES_ARG_TYPES
 
-def python_to_couch(options):
+def python_to_couch(options, encoder=None):
     """
     Translates query options from python style options into CouchDB/Cloudant
     query options.  For example ``{'include_docs': True}`` will
@@ -171,13 +171,14 @@ def python_to_couch(options):
     :func:`~cloudant.view.View.__call__` callable, both used to retrieve data.
 
     :param dict options: Python style parameters to be translated.
+    :param encoder: Custom encoder, defaults to None
 
     :returns: Dictionary of translated CouchDB/Cloudant query parameters
     """
     translation = dict()
     for key, val in iteritems_(options):
         py_to_couch_validate(key, val)
-        translation.update(_py_to_couch_translate(key, val))
+        translation.update(_py_to_couch_translate(key, val, encoder))
     return translation
 
 def py_to_couch_validate(key, val):
@@ -201,7 +202,7 @@ def py_to_couch_validate(key, val):
         if val not in ('ok', 'update_after'):
             raise CloudantArgumentError(135, val)
 
-def _py_to_couch_translate(key, val):
+def _py_to_couch_translate(key, val, encoder=None):
     """
     Performs the conversion of the Python parameter value to its CouchDB
     equivalent.
@@ -209,6 +210,8 @@ def _py_to_couch_translate(key, val):
     try:
         if key in ['keys', 'endkey_docid', 'startkey_docid', 'stale', 'update']:
             return {key: val}
+        if key in ['endkey', 'key', 'startkey']:
+            return {key: json.dumps(val, cls=encoder)}
         if val is None:
             return {key: None}
         arg_converter = TYPE_CONVERTERS.get(type(val))
@@ -249,7 +252,7 @@ def get_docs(r_session, url, encoder=None, headers=None, **params):
     keys = None
     if keys_list is not None:
         keys = json.dumps({'keys': keys_list}, cls=encoder)
-    f_params = python_to_couch(params)
+    f_params = python_to_couch(params, encoder)
     resp = None
     if keys is not None:
         # If we're using POST we are sending JSON so add the header


Test patch:
diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py
index c8c5cfe..a4e4bad 100644
--- a/tests/unit/database_tests.py
+++ b/tests/unit/database_tests.py
@@ -512,6 +512,15 @@ class DatabaseTests(UnitTestDbBase):
         data = self.db.all_docs(limit=1, skip=LONG_NUMBER)
         self.assertEqual(len(data.get('rows')), 1)
 
+    def test_all_docs_get_uses_custom_encoder(self):
+        """
+        Test that all_docs uses the custom encoder.
+        """
+        self.set_up_client(auto_connect=True, encoder="AEncoder")
+        database = self.client[self.test_dbname]
+        with self.assertRaises(CloudantArgumentError):
+            database.all_docs(endkey=['foo', 10])
+
     def test_custom_result_context_manager(self):
         """
         Test using the database custom result context manager
diff --git a/tests/unit/param_translation_tests.py b/tests/unit/param_translation_tests.py
index 4da0d05..6aac721 100644
--- a/tests/unit/param_translation_tests.py
+++ b/tests/unit/param_translation_tests.py
@@ -36,7 +36,7 @@ class PythonToCouchTests(unittest.TestCase):
             {'descending': 'true'}
         )
         self.assertEqual(
-            python_to_couch({'descending': False}), 
+            python_to_couch({'descending': False}),
             {'descending': 'false'}
         )
 
@@ -44,9 +44,9 @@ class PythonToCouchTests(unittest.TestCase):
         """
         Test endkey translation is successful.
         """
-        self.assertEqual(python_to_couch({'endkey': 10}), {'endkey': 10})
+        self.assertEqual(python_to_couch({'endkey': 10}), {'endkey': '10'})
         # Test with long type
-        self.assertEqual(python_to_couch({'endkey': LONG_NUMBER}), {'endkey': LONG_NUMBER})
+        self.assertEqual(python_to_couch({'endkey': LONG_NUMBER}), {'endkey': str(LONG_NUMBER)})
         self.assertEqual(
             python_to_couch({'endkey': 'foo'}),
             {'endkey': '"foo"'}
@@ -120,9 +120,9 @@ class PythonToCouchTests(unittest.TestCase):
         """
         Test key translation is successful.
         """
-        self.assertEqual(python_to_couch({'key': 10}), {'key': 10})
+        self.assertEqual(python_to_couch({'key': 10}), {'key': '10'})
         # Test with long type
-        self.assertEqual(python_to_couch({'key': LONG_NUMBER}), {'key': LONG_NUMBER})
+        self.assertEqual(python_to_couch({'key': LONG_NUMBER}), {'key': str(LONG_NUMBER)})
         self.assertEqual(python_to_couch({'key': 'foo'}), {'key': '"foo"'})
         self.assertEqual(
             python_to_couch({'key': ['foo', 10]}),
@@ -194,9 +194,9 @@ class PythonToCouchTests(unittest.TestCase):
         """
         Test startkey translation is successful.
         """
-        self.assertEqual(python_to_couch({'startkey': 10}), {'startkey': 10})
+        self.assertEqual(python_to_couch({'startkey': 10}), {'startkey': '10'})
         # Test with long type
-        self.assertEqual(python_to_couch({'startkey': LONG_NUMBER}), {'startkey': LONG_NUMBER})
+        self.assertEqual(python_to_couch({'startkey': LONG_NUMBER}), {'startkey': str(LONG_NUMBER)})
         self.assertEqual(
             python_to_couch({'startkey': 'foo'}),
             {'startkey': '"foo"'}