1# -*- coding: utf-8 -*-
2"""
3ObsPy's compatibility layer.
4
5Includes things to easy dealing with Py2/Py3 differences as well as making
6it work with various versions of our dependencies.
7"""
8from future.utils import PY2
9
10import collections
11import importlib
12import io
13import json
14import sys
15import unittest
16
17import numpy as np
18
19# optional dependencies
20try:
21    if PY2:
22        import mock  # NOQA
23    else:
24        from unittest import mock  # NOQA
25except ImportError:
26    pass
27
28if PY2:
29    from string import maketrans  # NOQA
30    from urlparse import urlparse  # NOQA
31else:
32    maketrans = bytes.maketrans  # NOQA
33    from urllib.parse import urlparse  # NOQA
34
35
36# Define the string types.
37if PY2:
38    string_types = (basestring,)  # NOQA
39else:
40    string_types = (str,)  # NOQA
41
42
43# Importing the ABCs from collections will no longer work with Python 3.8.
44if PY2:
45    collections_abc = collections  # NOQA
46else:
47    try:
48        collections_abc = collections.abc  # NOQA
49    except AttributeError:
50        # Python 3.4 compat, see https://bugs.python.org/msg212284
51        # some older Linux distribution (like Debian jessie) are still in LTS,
52        # so be nice, this doesn't hurt and can be removed again later on
53        collections_abc = importlib.import_module("collections.abc")  # NOQA
54
55
56if PY2:
57    class RegExTestCase(unittest.TestCase):
58        def assertRaisesRegex(self, exception, regex, callable,  # NOQA
59                              *args, **kwargs):
60            return self.assertRaisesRegexp(exception, regex, callable,
61                                           *args, **kwargs)
62else:
63    class RegExTestCase(unittest.TestCase):
64        pass
65
66
67# NumPy does not offer the from_buffer method under Python 3 and instead
68# relies on the built-in memoryview object.
69if PY2:
70    def from_buffer(data, dtype):
71        # For compatibility with NumPy 1.4
72        if isinstance(dtype, unicode):  # noqa
73            dtype = str(dtype)
74        if data:
75            try:
76                data = data.encode()
77            except Exception:
78                pass
79            return np.frombuffer(data, dtype=dtype).copy()
80        else:
81            return np.array([], dtype=dtype)
82else:
83    def from_buffer(data, dtype):
84        try:
85            data = data.encode()
86        except Exception:
87            pass
88        return np.array(memoryview(data)).view(dtype).copy()  # NOQA
89
90
91if PY2:
92    from ConfigParser import SafeConfigParser as ConfigParser  # NOQA
93else:
94    from configparser import ConfigParser  # NOQA
95
96
97def is_text_buffer(obj):
98    """
99    Helper function determining if the passed object is an object that can
100    read and write text or not.
101
102    :param obj: The object to be tested.
103    :return: True/False
104    """
105    # Default open()'ed files and StringIO (in Python 2) don't inherit from any
106    # of the io classes thus we only test the methods of the objects which
107    # in Python 2 should be safe enough.
108    if PY2 and not isinstance(obj, io.BufferedIOBase) and \
109            not isinstance(obj, io.TextIOBase):
110        if hasattr(obj, "read") and hasattr(obj, "write") \
111                and hasattr(obj, "seek") and hasattr(obj, "tell"):
112            return True
113        return False
114
115    return isinstance(obj, io.TextIOBase)
116
117
118def is_bytes_buffer(obj):
119    """
120    Helper function determining if the passed object is an object that can
121    read and write bytes or not.
122
123    :param obj: The object to be tested.
124    :return: True/False
125    """
126    # Default open()'ed files and StringIO (in Python 2) don't inherit from any
127    # of the io classes thus we only test the methods of the objects which
128    # in Python 2 should be safe enough.
129    if PY2 and not isinstance(obj, io.BufferedIOBase) and \
130            not isinstance(obj, io.TextIOBase):
131        if hasattr(obj, "read") and hasattr(obj, "write") \
132                and hasattr(obj, "seek") and hasattr(obj, "tell"):
133            return True
134        return False
135
136    return isinstance(obj, io.BufferedIOBase)
137
138
139def round_away(number):
140    """
141    Simple function that rounds a number to the nearest integer. If the number
142    is halfway between two integers, it will round away from zero. Of course
143    only works up machine precision. This should hopefully behave like the
144    round() function in Python 2.
145
146    This is potentially desired behavior in the trim functions but some more
147    thought should be poured into it.
148
149    The np.round() function rounds towards the even nearest even number in case
150    of half-way splits.
151
152    >>> round_away(2.5)
153    3
154    >>> round_away(-2.5)
155    -3
156
157    >>> round_away(10.5)
158    11
159    >>> round_away(-10.5)
160    -11
161
162    >>> round_away(11.0)
163    11
164    >>> round_away(-11.0)
165    -11
166    """
167    floor = np.floor(number)
168    ceil = np.ceil(number)
169    if (floor != ceil) and (abs(number - floor) == abs(ceil - number)):
170        return int(int(number) + int(np.sign(number)))
171    else:
172        return int(np.round(number))
173
174
175if sys.version_info[0] < 3:
176    def py3_round(number, ndigits=None):
177        """
178        Similar function to python 3's built-in round function.
179
180        Returns a float if ndigits is greater than 0, else returns an integer.
181
182        Note:
183        This function should be replace by the builtin round when obspy
184        drops support for python 2.7.
185        Unlike python'3 rounding, this function always rounds up on half
186        increments rather than implementing banker's rounding.
187
188        :type number: int or float
189        :param number: A real number to be rounded
190        :type ndigits: int
191        :param ndigits: number of digits
192        :return: An int if ndigites <= 0, else a float rounded to ndigits.
193        """
194        if ndigits is None or ndigits <= 0:
195            mult = 10 ** -(ndigits or 0)
196            return ((int(number) + mult // 2) // mult) * mult
197        else:
198            return round(number, ndigits)
199else:
200    py3_round = round
201
202
203def get_json_from_response(r):
204    """
205    Get a JSON response in a way that also works for very old request
206    versions.
207
208    :type r: :class:`requests.Response
209    :param r: The server's response.
210    """
211    if hasattr(r, "json"):
212        if isinstance(r.json, dict):
213            return r.json
214        return r.json()
215
216    c = r.content
217    try:
218        c = c.decode()
219    except Exception:
220        pass
221    return json.loads(c)
222
223
224def get_text_from_response(r):
225    """
226    Get a text response in a way that also works for very old request versions.
227
228    :type r: :class:`requests.Response
229    :param r: The server's response.
230    """
231    if hasattr(r, "text"):
232        return r.text
233
234    c = r.content
235    try:
236        c = c.decode()
237    except Exception:
238        pass
239    return c
240
241
242def get_reason_from_response(r):
243    """
244    Get the status text.
245
246    :type r: :class:`requests.Response
247    :param r: The server's response.
248    """
249    # Very old requests version might not have the reason attribute.
250    if hasattr(r, "reason"):
251        c = r.reason
252    else:  # pragma: no cover
253        c = r.raw.reason
254
255    if hasattr(c, "encode"):
256        c = c.encode()
257
258    return c
259