Repository: andycasey/ads

Issue:
Search parameters wrapped in parentheses should not be auto-quoted
## Expected Behavior
Don't auto-quote search params wrapped in parentheses.
```python
>>> q = ads.SearchQuery(title='("solar" OR "sun" OR "helio" OR "cme" OR "corona")')
>>> q._query
{'q': ' title:("solar" OR "sun" OR "helio" OR "cme" OR "corona")',
 [...] }
```


## Current Behavior
```python
>>> q = ads.SearchQuery(title='("solar" OR "sun" OR "helio" OR "cme" OR "corona")')
>>> q._query
{'q': ' title:"("solar" OR "sun" OR "helio" OR "cme" OR "corona")"',
 [...] }
```

## Possible Solution
Use `bool(re.match('\s*\(.*\)\s*', v))` to determine whether a parameter value is wrapped in parentheses. Don't wrap in quotes if already wrapped in parentheses.

## Context
Trying to structure my ads queries in a JSON file rather than use a long string.



Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/38697542/badge)](https://coveralls.io/builds/38697542)

Coverage increased (+0.03%) to 92.239% when pulling **d030ffdad3678ad02045ee232d3b5f7303a66c23 on panasenco:master** into **63e1e53e9e495e4b54ee395b263d0dc6d539dcc8 on andycasey:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/38697542/badge)](https://coveralls.io/builds/38697542)

Coverage increased (+0.03%) to 92.239% when pulling **d030ffdad3678ad02045ee232d3b5f7303a66c23 on panasenco:master** into **63e1e53e9e495e4b54ee395b263d0dc6d539dcc8 on andycasey:master**.

panasenco: Sure, that's a reasonable request. I'll try to add some tests sometime this week.
panasenco: I think bumping the Python3 version on TravisCI to a more modern one like 3.8 or 3.9 will fix the import issue it's experiencing.
vsudilov: > I think bumping the Python3 version on TravisCI to a more modern one like 3.8 or 3.9 will fix the import issue it's experiencing.

Yeah for sure.

Patch:
diff --git a/.gitignore b/.gitignore
index 35b071d..04102ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,8 @@
 *idea*
 *pyo
 *__pycache__*
+*.egg-info
+.coverage
 
 docs/_build
+venv/
diff --git a/README.rst b/README.rst
index 0ab9cd7..8ae5ec2 100644
--- a/README.rst
+++ b/README.rst
@@ -27,4 +27,10 @@ Quickstart
    [u'Measurements of Omega and Lambda from 42 High-Redshift Supernovae']
    [u'Observational Evidence from Supernovae for an Accelerating Universe and a Cosmological Constant']
    [u'First-Year Wilkinson Microwave Anisotropy Probe (WMAP) Observations: Determination of Cosmological Parameters']
-   [u'Abundances of the elements: Meteoritic and solar']
\ No newline at end of file
+   [u'Abundances of the elements: Meteoritic and solar']
+
+Running Tests
+=============
+
+   > cd /path/to/ads
+   > python -m unittest discover
diff --git a/ads/search.py b/ads/search.py
index 64944a8..0587282 100644
--- a/ads/search.py
+++ b/ads/search.py
@@ -3,6 +3,7 @@ Interface to the adsws search api.
 """
 
 import warnings
+import re
 import six
 import math
 
@@ -437,8 +438,11 @@ class SearchQuery(BaseQuery):
 
             # Format and add kwarg (key, value) pairs to q
             if kwargs:
-                _ = [u'{}:"{}"'.format(k, v) for k, v in six.iteritems(kwargs)]
-                self._query['q'] = u'{} {}'.format(self._query['q'], ' '.join(_))
+                for field, value in six.iteritems(kwargs):
+                    if not re.match('\s*\(.*\)\s*', value):
+                        # Wrap value in quotes if not already in parentheses
+                        value = u'"{}"'.format(value)
+                    self._query['q'] += u' {}:{}'.format(field, value)
 
         assert self._query.get('rows') > 0, "rows must be greater than 0"
         assert self._query.get('q'), "q must not be empty"


Test patch:
diff --git a/ads/tests/test_base.py b/ads/tests/test_base.py
index b6caff4..ccdb696 100644
--- a/ads/tests/test_base.py
+++ b/ads/tests/test_base.py
@@ -57,11 +57,10 @@ class TestBaseQuery(unittest.TestCase):
 
         # Write temporary file and override the config variable with the
         # tempfile paths
-        tf1, tf2 = NamedTemporaryFile(), NamedTemporaryFile()
+        tf1, tf2 = NamedTemporaryFile(delete=False), NamedTemporaryFile(delete=False)
         tf1.write('tok3\n'.encode("utf-8"))
         tf2.write(' tok4 '.encode("utf-8"))
-        [f.flush() for f in [tf1, tf2]]
-        [f.seek(0) for f in [tf1, tf2]]
+        [f.close() for f in [tf1, tf2]]
         ads.base.TOKEN_FILES = [tf1.name, tf2.name]
 
         bq = BaseQuery()
@@ -76,11 +75,11 @@ class TestBaseQuery(unittest.TestCase):
 
         self.assertEqual(bq.token, 'tok3')
         reset_token()
-        tf1.close()
+        os.remove(tf1.name)
 
         self.assertEqual(bq.token, 'tok4')
         reset_token()
-        tf2.close()
+        os.remove(tf2.name)
 
         self.assertEqual(bq.token, None)
 
diff --git a/ads/tests/test_search.py b/ads/tests/test_search.py
index c3b7666..e3fedd6 100644
--- a/ads/tests/test_search.py
+++ b/ads/tests/test_search.py
@@ -250,6 +250,13 @@ class TestSearchQuery(unittest.TestCase):
             sq.query['q'].split(),
             'aff:"institute" star'.split(),
         )
+        
+        # Test parentheses don't get wrapped in quotes (#114)
+        sq = SearchQuery(title='("solar" OR "sun" OR "helio" OR "cme" OR "corona")')
+        self.assertEqual(
+            sq.query['q'],
+            ' title:("solar" OR "sun" OR "helio" OR "cme" OR "corona")'
+        )
 
         sq = SearchQuery(q="star", token="test-token")
         self.assertEqual(sq.token, "test-token")