util.py 13 KB

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