1"""This module contains several handy functions primarily meant for internal use."""
2
3from __future__ import division
4
5from datetime import date, datetime, time, timedelta, tzinfo
6from calendar import timegm
7from functools import partial
8from inspect import isclass, ismethod
9import re
10import sys
11
12from pytz import timezone, utc, FixedOffset
13import six
14
15try:
16    from inspect import signature
17except ImportError:  # pragma: nocover
18    from funcsigs import signature
19
20try:
21    from threading import TIMEOUT_MAX
22except ImportError:
23    TIMEOUT_MAX = 4294967  # Maximum value accepted by Event.wait() on Windows
24
25try:
26    from asyncio import iscoroutinefunction
27except ImportError:
28    try:
29        from trollius import iscoroutinefunction
30    except ImportError:
31        def iscoroutinefunction(func):
32            return False
33
34__all__ = ('asint', 'asbool', 'astimezone', 'convert_to_datetime', 'datetime_to_utc_timestamp',
35           'utc_timestamp_to_datetime', 'timedelta_seconds', 'datetime_ceil', 'get_callable_name',
36           'obj_to_ref', 'ref_to_obj', 'maybe_ref', 'repr_escape', 'check_callable_args',
37           'TIMEOUT_MAX')
38
39
40class _Undefined(object):
41    def __nonzero__(self):
42        return False
43
44    def __bool__(self):
45        return False
46
47    def __repr__(self):
48        return '<undefined>'
49
50
51undefined = _Undefined()  #: a unique object that only signifies that no value is defined
52
53
54def asint(text):
55    """
56    Safely converts a string to an integer, returning ``None`` if the string is ``None``.
57
58    :type text: str
59    :rtype: int
60
61    """
62    if text is not None:
63        return int(text)
64
65
66def asbool(obj):
67    """
68    Interprets an object as a boolean value.
69
70    :rtype: bool
71
72    """
73    if isinstance(obj, str):
74        obj = obj.strip().lower()
75        if obj in ('true', 'yes', 'on', 'y', 't', '1'):
76            return True
77        if obj in ('false', 'no', 'off', 'n', 'f', '0'):
78            return False
79        raise ValueError('Unable to interpret value "%s" as boolean' % obj)
80    return bool(obj)
81
82
83def astimezone(obj):
84    """
85    Interprets an object as a timezone.
86
87    :rtype: tzinfo
88
89    """
90    if isinstance(obj, six.string_types):
91        return timezone(obj)
92    if isinstance(obj, tzinfo):
93        if not hasattr(obj, 'localize') or not hasattr(obj, 'normalize'):
94            raise TypeError('Only timezones from the pytz library are supported')
95        if obj.zone == 'local':
96            raise ValueError(
97                'Unable to determine the name of the local timezone -- you must explicitly '
98                'specify the name of the local timezone. Please refrain from using timezones like '
99                'EST to prevent problems with daylight saving time. Instead, use a locale based '
100                'timezone name (such as Europe/Helsinki).')
101        return obj
102    if obj is not None:
103        raise TypeError('Expected tzinfo, got %s instead' % obj.__class__.__name__)
104
105
106_DATE_REGEX = re.compile(
107    r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
108    r'(?:[ T](?P<hour>\d{1,2}):(?P<minute>\d{1,2}):(?P<second>\d{1,2})'
109    r'(?:\.(?P<microsecond>\d{1,6}))?'
110    r'(?P<timezone>Z|[+-]\d\d:\d\d)?)?$')
111
112
113def convert_to_datetime(input, tz, arg_name):
114    """
115    Converts the given object to a timezone aware datetime object.
116
117    If a timezone aware datetime object is passed, it is returned unmodified.
118    If a native datetime object is passed, it is given the specified timezone.
119    If the input is a string, it is parsed as a datetime with the given timezone.
120
121    Date strings are accepted in three different forms: date only (Y-m-d), date with time
122    (Y-m-d H:M:S) or with date+time with microseconds (Y-m-d H:M:S.micro). Additionally you can
123    override the time zone by giving a specific offset in the format specified by ISO 8601:
124    Z (UTC), +HH:MM or -HH:MM.
125
126    :param str|datetime input: the datetime or string to convert to a timezone aware datetime
127    :param datetime.tzinfo tz: timezone to interpret ``input`` in
128    :param str arg_name: the name of the argument (used in an error message)
129    :rtype: datetime
130
131    """
132    if input is None:
133        return
134    elif isinstance(input, datetime):
135        datetime_ = input
136    elif isinstance(input, date):
137        datetime_ = datetime.combine(input, time())
138    elif isinstance(input, six.string_types):
139        m = _DATE_REGEX.match(input)
140        if not m:
141            raise ValueError('Invalid date string')
142
143        values = m.groupdict()
144        tzname = values.pop('timezone')
145        if tzname == 'Z':
146            tz = utc
147        elif tzname:
148            hours, minutes = (int(x) for x in tzname[1:].split(':'))
149            sign = 1 if tzname[0] == '+' else -1
150            tz = FixedOffset(sign * (hours * 60 + minutes))
151
152        values = {k: int(v or 0) for k, v in values.items()}
153        datetime_ = datetime(**values)
154    else:
155        raise TypeError('Unsupported type for %s: %s' % (arg_name, input.__class__.__name__))
156
157    if datetime_.tzinfo is not None:
158        return datetime_
159    if tz is None:
160        raise ValueError(
161            'The "tz" argument must be specified if %s has no timezone information' % arg_name)
162    if isinstance(tz, six.string_types):
163        tz = timezone(tz)
164
165    try:
166        return tz.localize(datetime_, is_dst=None)
167    except AttributeError:
168        raise TypeError(
169            'Only pytz timezones are supported (need the localize() and normalize() methods)')
170
171
172def datetime_to_utc_timestamp(timeval):
173    """
174    Converts a datetime instance to a timestamp.
175
176    :type timeval: datetime
177    :rtype: float
178
179    """
180    if timeval is not None:
181        return timegm(timeval.utctimetuple()) + timeval.microsecond / 1000000
182
183
184def utc_timestamp_to_datetime(timestamp):
185    """
186    Converts the given timestamp to a datetime instance.
187
188    :type timestamp: float
189    :rtype: datetime
190
191    """
192    if timestamp is not None:
193        return datetime.fromtimestamp(timestamp, utc)
194
195
196def timedelta_seconds(delta):
197    """
198    Converts the given timedelta to seconds.
199
200    :type delta: timedelta
201    :rtype: float
202
203    """
204    return delta.days * 24 * 60 * 60 + delta.seconds + \
205        delta.microseconds / 1000000.0
206
207
208def datetime_ceil(dateval):
209    """
210    Rounds the given datetime object upwards.
211
212    :type dateval: datetime
213
214    """
215    if dateval.microsecond > 0:
216        return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond)
217    return dateval
218
219
220def datetime_repr(dateval):
221    return dateval.strftime('%Y-%m-%d %H:%M:%S %Z') if dateval else 'None'
222
223
224def get_callable_name(func):
225    """
226    Returns the best available display name for the given function/callable.
227
228    :rtype: str
229
230    """
231    # the easy case (on Python 3.3+)
232    if hasattr(func, '__qualname__'):
233        return func.__qualname__
234
235    # class methods, bound and unbound methods
236    f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None)
237    if f_self and hasattr(func, '__name__'):
238        f_class = f_self if isclass(f_self) else f_self.__class__
239    else:
240        f_class = getattr(func, 'im_class', None)
241
242    if f_class and hasattr(func, '__name__'):
243        return '%s.%s' % (f_class.__name__, func.__name__)
244
245    # class or class instance
246    if hasattr(func, '__call__'):
247        # class
248        if hasattr(func, '__name__'):
249            return func.__name__
250
251        # instance of a class with a __call__ method
252        return func.__class__.__name__
253
254    raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func)
255
256
257def obj_to_ref(obj):
258    """
259    Returns the path to the given callable.
260
261    :rtype: str
262    :raises TypeError: if the given object is not callable
263    :raises ValueError: if the given object is a :class:`~functools.partial`, lambda or a nested
264        function
265
266    """
267    if isinstance(obj, partial):
268        raise ValueError('Cannot create a reference to a partial()')
269
270    name = get_callable_name(obj)
271    if '<lambda>' in name:
272        raise ValueError('Cannot create a reference to a lambda')
273    if '<locals>' in name:
274        raise ValueError('Cannot create a reference to a nested function')
275
276    if ismethod(obj):
277        if hasattr(obj, 'im_self') and obj.im_self:
278            # bound method
279            module = obj.im_self.__module__
280        elif hasattr(obj, 'im_class') and obj.im_class:
281            # unbound method
282            module = obj.im_class.__module__
283        else:
284            module = obj.__module__
285    else:
286        module = obj.__module__
287    return '%s:%s' % (module, name)
288
289
290def ref_to_obj(ref):
291    """
292    Returns the object pointed to by ``ref``.
293
294    :type ref: str
295
296    """
297    if not isinstance(ref, six.string_types):
298        raise TypeError('References must be strings')
299    if ':' not in ref:
300        raise ValueError('Invalid reference')
301
302    modulename, rest = ref.split(':', 1)
303    try:
304        obj = __import__(modulename, fromlist=[rest])
305    except ImportError:
306        raise LookupError('Error resolving reference %s: could not import module' % ref)
307
308    try:
309        for name in rest.split('.'):
310            obj = getattr(obj, name)
311        return obj
312    except Exception:
313        raise LookupError('Error resolving reference %s: error looking up object' % ref)
314
315
316def maybe_ref(ref):
317    """
318    Returns the object that the given reference points to, if it is indeed a reference.
319    If it is not a reference, the object is returned as-is.
320
321    """
322    if not isinstance(ref, str):
323        return ref
324    return ref_to_obj(ref)
325
326
327if six.PY2:
328    def repr_escape(string):
329        if isinstance(string, six.text_type):
330            return string.encode('ascii', 'backslashreplace')
331        return string
332else:
333    def repr_escape(string):
334        return string
335
336
337def check_callable_args(func, args, kwargs):
338    """
339    Ensures that the given callable can be called with the given arguments.
340
341    :type args: tuple
342    :type kwargs: dict
343
344    """
345    pos_kwargs_conflicts = []  # parameters that have a match in both args and kwargs
346    positional_only_kwargs = []  # positional-only parameters that have a match in kwargs
347    unsatisfied_args = []  # parameters in signature that don't have a match in args or kwargs
348    unsatisfied_kwargs = []  # keyword-only arguments that don't have a match in kwargs
349    unmatched_args = list(args)  # args that didn't match any of the parameters in the signature
350    # kwargs that didn't match any of the parameters in the signature
351    unmatched_kwargs = list(kwargs)
352    # indicates if the signature defines *args and **kwargs respectively
353    has_varargs = has_var_kwargs = False
354
355    try:
356        if sys.version_info >= (3, 5):
357            sig = signature(func, follow_wrapped=False)
358        else:
359            sig = signature(func)
360    except ValueError:
361        # signature() doesn't work against every kind of callable
362        return
363
364    for param in six.itervalues(sig.parameters):
365        if param.kind == param.POSITIONAL_OR_KEYWORD:
366            if param.name in unmatched_kwargs and unmatched_args:
367                pos_kwargs_conflicts.append(param.name)
368            elif unmatched_args:
369                del unmatched_args[0]
370            elif param.name in unmatched_kwargs:
371                unmatched_kwargs.remove(param.name)
372            elif param.default is param.empty:
373                unsatisfied_args.append(param.name)
374        elif param.kind == param.POSITIONAL_ONLY:
375            if unmatched_args:
376                del unmatched_args[0]
377            elif param.name in unmatched_kwargs:
378                unmatched_kwargs.remove(param.name)
379                positional_only_kwargs.append(param.name)
380            elif param.default is param.empty:
381                unsatisfied_args.append(param.name)
382        elif param.kind == param.KEYWORD_ONLY:
383            if param.name in unmatched_kwargs:
384                unmatched_kwargs.remove(param.name)
385            elif param.default is param.empty:
386                unsatisfied_kwargs.append(param.name)
387        elif param.kind == param.VAR_POSITIONAL:
388            has_varargs = True
389        elif param.kind == param.VAR_KEYWORD:
390            has_var_kwargs = True
391
392    # Make sure there are no conflicts between args and kwargs
393    if pos_kwargs_conflicts:
394        raise ValueError('The following arguments are supplied in both args and kwargs: %s' %
395                         ', '.join(pos_kwargs_conflicts))
396
397    # Check if keyword arguments are being fed to positional-only parameters
398    if positional_only_kwargs:
399        raise ValueError('The following arguments cannot be given as keyword arguments: %s' %
400                         ', '.join(positional_only_kwargs))
401
402    # Check that the number of positional arguments minus the number of matched kwargs matches the
403    # argspec
404    if unsatisfied_args:
405        raise ValueError('The following arguments have not been supplied: %s' %
406                         ', '.join(unsatisfied_args))
407
408    # Check that all keyword-only arguments have been supplied
409    if unsatisfied_kwargs:
410        raise ValueError(
411            'The following keyword-only arguments have not been supplied in kwargs: %s' %
412            ', '.join(unsatisfied_kwargs))
413
414    # Check that the callable can accept the given number of positional arguments
415    if not has_varargs and unmatched_args:
416        raise ValueError(
417            'The list of positional arguments is longer than the target callable can handle '
418            '(allowed: %d, given in args: %d)' % (len(args) - len(unmatched_args), len(args)))
419
420    # Check that the callable can accept the given keyword arguments
421    if not has_var_kwargs and unmatched_kwargs:
422        raise ValueError(
423            'The target callable does not accept the following keyword arguments: %s' %
424            ', '.join(unmatched_kwargs))
425
426
427def iscoroutinefunction_partial(f):
428    while isinstance(f, partial):
429        f = f.func
430
431    # The asyncio version of iscoroutinefunction includes testing for @coroutine
432    # decorations vs. the inspect version which does not.
433    return iscoroutinefunction(f)
434