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