Repository: Stranger6667/pyanyapi

Issue:
Add option to strip results
At parser level, False by default

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index b01d832..35babdf 100644
--- a/README.rst
+++ b/README.rst
@@ -31,7 +31,7 @@ Usage
 -----
 
 Library provides an ability to create API over various content.
-Currently there are bundled tools to work with HTML, XML, JSON.
+Currently there are bundled tools to work with HTML, XML, JSON and YAML.
 Initially it was created to work with ``requests`` library.
 
 Basic setup
@@ -139,6 +139,22 @@ Settings attribute is merged from all ancestors of current parser.
     >>> SecondChildParser({'child': '//more'}).settings['child']
     //more
 
+Results stripping
+~~~~~~~~~~~~~~~~~
+
+Parsers can automagically strip trailing whitespaces with ``strip=True`` option.
+
+.. code:: python
+
+    from pyanyapi import RegExpParser
+
+
+    >>> settings = {'p': 'string(//p)'}
+    >>> XMLParser(settings).parse('<p> Pcontent </p>').p
+     Pcontent
+    >>> XMLParser(settings, strip=True).parse('<p> Pcontent </p>).p
+    Pcontent
+
 HTML & XML
 ~~~~~~~~~~
 
diff --git a/pyanyapi/_compat.py b/pyanyapi/_compat.py
index 6ef744e..6322456 100644
--- a/pyanyapi/_compat.py
+++ b/pyanyapi/_compat.py
@@ -16,3 +16,9 @@ try:
     import ujson as json
 except ImportError:
     import json
+
+
+try:
+    string_types = (str, unicode)
+except NameError:
+    string_types = (str, )
diff --git a/pyanyapi/interfaces.py b/pyanyapi/interfaces.py
index 88122a4..7dfb67b 100644
--- a/pyanyapi/interfaces.py
+++ b/pyanyapi/interfaces.py
@@ -6,7 +6,7 @@ import re
 
 import yaml
 
-from ._compat import json, etree, objectify, XMLParser, HTMLParser
+from ._compat import json, etree, objectify, XMLParser, HTMLParser, string_types
 from .exceptions import ResponseParseError
 from .helpers import memoize
 
@@ -21,8 +21,9 @@ class BaseInterface(object):
     content = None
     empty_result = None
 
-    def __init__(self, content):
+    def __init__(self, content, strip=False):
         self.content = content
+        self.strip = strip
         self.parse = memoize(self.parse)
 
     @classmethod
@@ -58,6 +59,11 @@ class BaseInterface(object):
             if hasattr(attr, '_attached') and type(attr).__name__ == 'cached_property'
         )
 
+    def maybe_strip(self, value):
+        if self.strip and isinstance(value, string_types):
+            return value.strip()
+        return value
+
 
 # Uses as fallback. None - can be obtained from JSON's null, any string also can be, so unique object is a best choice
 EMPTY_RESULT = object()
@@ -132,15 +138,13 @@ class XPathInterface(BaseInterface):
             result = self.parse(settings['base'])
             child_query = settings.get('children')
             if child_query:
-                return [''.join(element.xpath(child_query)).strip() for element in result]
-            elif isinstance(result, list):
-                return result
-            return result.strip()
+                return [self.maybe_strip(''.join(element.xpath(child_query))) for element in result]
+            return result
 
         return self.parse(settings)
 
     def parse(self, query):
-        return self.parsed_content.xpath(query)
+        return self.maybe_strip(self.parsed_content.xpath(query))
 
 
 class XMLInterface(XPathInterface):
@@ -209,7 +213,7 @@ class DictInterface(BaseInterface):
                         return self.empty_result
             else:
                 return target
-        return target
+        return self.maybe_strip(target)
 
     def execute_method(self, settings):
         if isinstance(settings, dict):
@@ -266,7 +270,7 @@ class AJAXInterface(JSONInterface):
     def get_inner_interface(self, text, json_part):
         if json_part not in self._inner_cache:
             inner_content = super(AJAXInterface, self).get_from_dict(text, json_part)
-            self._inner_cache[json_part] = self.inner_interface_class(inner_content)
+            self._inner_cache[json_part] = self.inner_interface_class(inner_content, self.strip)
         return self._inner_cache[json_part]
 
     def get_from_dict(self, target, query):
@@ -292,14 +296,14 @@ class RegExpInterface(BaseInterface):
     So, response will be like 'ok' or 'Error 100'.
     """
 
-    def __init__(self, content, flags=0):
+    def __init__(self, content, strip=False, flags=0):
         self.flags = flags
-        super(RegExpInterface, self).__init__(content)
+        super(RegExpInterface, self).__init__(content, strip)
 
     def execute_method(self, settings):
         matches = re.findall(settings, self.content, self.flags)
         if matches:
-            return matches[0]
+            return self.maybe_strip(matches[0])
         return self.empty_result
 
     def parse(self, query):
diff --git a/pyanyapi/parsers.py b/pyanyapi/parsers.py
index 05fceef..25c009f 100644
--- a/pyanyapi/parsers.py
+++ b/pyanyapi/parsers.py
@@ -23,7 +23,8 @@ class BaseParser(object):
     """
     interface_class = None
 
-    def __init__(self, settings=None):
+    def __init__(self, settings=None, strip=False):
+        self.strip = strip
         parents_settings = self.get_parents_settings()
         if settings:
             parents_settings.update(settings)
@@ -68,7 +69,7 @@ class BaseParser(object):
         return self.parse(content).parse_all()
 
     def get_interface_kwargs(self):
-        return {'content': self.content}
+        return {'content': self.content, 'strip': self.strip}
 
     def prepare_content(self, content):
         """
@@ -165,9 +166,9 @@ class AJAXParser(LXMLParser):
 class RegExpParser(BaseParser):
     interface_class = RegExpInterface
 
-    def __init__(self, settings=None, flags=0):
+    def __init__(self, settings=None, strip=False, flags=0):
         self.flags = flags
-        super(RegExpParser, self).__init__(settings)
+        super(RegExpParser, self).__init__(settings, strip)
 
     def get_interface_kwargs(self):
         kwargs = super(RegExpParser, self).get_interface_kwargs()


Test patch:
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index 25e54b3..85f4471 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -179,7 +179,7 @@ def test_complex_config():
     parsed = XMLParser({'test': {'base': '//test', 'children': 'text()|*//text()'}}).parse(
         '<xml><test>123 </test><test><inside> 234</inside></test></xml>'
     )
-    assert parsed.test == ['123', '234']
+    assert parsed.test == ['123 ', ' 234']
 
 
 def test_json_parse():
diff --git a/tests/test_strip.py b/tests/test_strip.py
new file mode 100644
index 0000000..d649312
--- /dev/null
+++ b/tests/test_strip.py
@@ -0,0 +1,34 @@
+# coding: utf-8
+from .conftest import lxml_is_supported
+from pyanyapi import RegExpParser, JSONParser, AJAXParser, XMLParser
+
+
+JSON_CONTENT = '{"container":" 1 "}'
+AJAX_CONTENT = '{"content": "<p> Pcontent </p>"}'
+XML_CONTENT = '<p> Pcontent </p>'
+
+
+def test_strip_regexp_parser():
+    settings = {'all': '.+'}
+    assert RegExpParser(settings).parse(' 1 ').all == ' 1 '
+    assert RegExpParser(settings, strip=True).parse(' 1 ').all == '1'
+
+
+def test_strip_json_parser():
+    settings = {'all': 'container'}
+    assert JSONParser(settings).parse(JSON_CONTENT).all == ' 1 '
+    assert JSONParser(settings, strip=True).parse(JSON_CONTENT).all == '1'
+
+
+@lxml_is_supported
+def test_strip_ajax_parser():
+    settings = {'all': 'content > string(//p)'}
+    assert AJAXParser(settings).parse(AJAX_CONTENT).all == ' Pcontent '
+    assert AJAXParser(settings, strip=True).parse(AJAX_CONTENT).all == 'Pcontent'
+
+
+@lxml_is_supported
+def test_strip_xml_parser():
+    settings = {'all': 'string(//p)'}
+    assert XMLParser(settings).parse(XML_CONTENT).all == ' Pcontent '
+    assert XMLParser(settings, strip=True).parse(XML_CONTENT).all == 'Pcontent'