Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

tx.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int GmbH
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###############################################################################
  26. import io
  27. import os
  28. import sys
  29. import weakref
  30. import inspect
  31. from functools import partial
  32. from twisted.python.failure import Failure
  33. from twisted.internet.defer import maybeDeferred, Deferred, DeferredList
  34. from twisted.internet.defer import succeed, fail
  35. from twisted.internet.interfaces import IReactorTime
  36. from zope.interface import provider
  37. from txaio.interfaces import IFailedFuture, ILogger, log_levels
  38. from txaio._iotype import guess_stream_needs_encoding
  39. from txaio import _Config
  40. from txaio._common import _BatchedTimer
  41. from txaio import _util
  42. from twisted.logger import Logger as _Logger, formatEvent, ILogObserver
  43. from twisted.logger import globalLogBeginner, formatTime, LogLevel
  44. from twisted.internet.defer import ensureDeferred
  45. from asyncio import iscoroutinefunction
  46. using_twisted = True
  47. using_asyncio = False
  48. config = _Config()
  49. _stderr, _stdout = sys.stderr, sys.stdout
  50. # some book-keeping variables here. _observer is used as a global by
  51. # the "backwards compatible" (Twisted < 15) loggers. The _loggers object
  52. # is a weak-ref set; we add Logger instances to this *until* such
  53. # time as start_logging is called (with the desired log-level) and
  54. # then we call _set_log_level on each instance. After that,
  55. # Logger's ctor uses _log_level directly.
  56. _observer = None # for Twisted legacy logging support; see below
  57. _loggers = weakref.WeakSet() # weak-references of each logger we've created
  58. _log_level = 'info' # global log level; possibly changed in start_logging()
  59. _started_logging = False
  60. _categories = {}
  61. IFailedFuture.register(Failure)
  62. ILogger.register(_Logger)
  63. def _no_op(*args, **kwargs):
  64. pass
  65. def add_log_categories(categories):
  66. _categories.update(categories)
  67. def with_config(loop=None):
  68. global config
  69. if loop is not None:
  70. if config.loop is not None and config.loop is not loop:
  71. raise RuntimeError(
  72. "Twisted has only a single, global reactor. You passed in "
  73. "a reactor different from the one already configured "
  74. "in txaio.config.loop"
  75. )
  76. return _TxApi(config)
  77. # NOTE: beware that twisted.logger._logger.Logger copies itself via an
  78. # overriden __get__ method when used as recommended as a class
  79. # descriptor. So, we override __get__ to just return ``self`` which
  80. # means ``log_source`` will be wrong, but we don't document that as a
  81. # key that you can depend on anyway :/
  82. class Logger(object):
  83. def __init__(self, level=None, logger=None, namespace=None, observer=None):
  84. assert logger, "Should not be instantiated directly."
  85. self._logger = logger(observer=observer, namespace=namespace)
  86. self._log_level_set_explicitly = False
  87. if level:
  88. self.set_log_level(level)
  89. else:
  90. self._set_log_level(_log_level)
  91. _loggers.add(self)
  92. def __get__(self, oself, type=None):
  93. # this causes the Logger to lie about the "source=", but
  94. # otherwise we create a new Logger instance every time we do
  95. # "self.log.info()" if we use it like:
  96. # class Foo:
  97. # log = make_logger
  98. return self
  99. def _log(self, level, *args, **kwargs):
  100. # Look for a log_category, switch it in if we have it
  101. if "log_category" in kwargs and kwargs["log_category"] in _categories:
  102. args = tuple()
  103. kwargs["format"] = _categories.get(kwargs["log_category"])
  104. self._logger.emit(level, *args, **kwargs)
  105. def emit(self, level, *args, **kwargs):
  106. if log_levels.index(self._log_level) < log_levels.index(level):
  107. return
  108. if level == "trace":
  109. return self._trace(*args, **kwargs)
  110. level = LogLevel.lookupByName(level)
  111. return self._log(level, *args, **kwargs)
  112. def set_log_level(self, level, keep=True):
  113. """
  114. Set the log level. If keep is True, then it will not change along with
  115. global log changes.
  116. """
  117. self._set_log_level(level)
  118. self._log_level_set_explicitly = keep
  119. def _set_log_level(self, level):
  120. # up to the desired level, we don't do anything, as we're a
  121. # "real" Twisted new-logger; for methods *after* the desired
  122. # level, we bind to the no_op method
  123. desired_index = log_levels.index(level)
  124. for (idx, name) in enumerate(log_levels):
  125. if name == 'none':
  126. continue
  127. if idx > desired_index:
  128. current = getattr(self, name, None)
  129. if not current == _no_op or current is None:
  130. setattr(self, name, _no_op)
  131. if name == 'error':
  132. setattr(self, 'failure', _no_op)
  133. else:
  134. if getattr(self, name, None) in (_no_op, None):
  135. if name == 'trace':
  136. setattr(self, "trace", self._trace)
  137. else:
  138. setattr(self, name,
  139. partial(self._log, LogLevel.lookupByName(name)))
  140. if name == 'error':
  141. setattr(self, "failure", self._failure)
  142. self._log_level = level
  143. def _failure(self, format=None, *args, **kw):
  144. return self._logger.failure(format, *args, **kw)
  145. def _trace(self, *args, **kw):
  146. # there is no "trace" level in Twisted -- but this whole
  147. # method will be no-op'd unless we are at the 'trace' level.
  148. self.debug(*args, txaio_trace=True, **kw)
  149. def make_logger(level=None, logger=_Logger, observer=None):
  150. # we want the namespace to be the calling context of "make_logger"
  151. # -- so we *have* to pass namespace kwarg to Logger (or else it
  152. # will always say the context is "make_logger")
  153. cf = inspect.currentframe().f_back
  154. if "self" in cf.f_locals:
  155. # We're probably in a class init or method
  156. cls = cf.f_locals["self"].__class__
  157. namespace = '{0}.{1}'.format(cls.__module__, cls.__name__)
  158. else:
  159. namespace = cf.f_globals["__name__"]
  160. if cf.f_code.co_name != "<module>":
  161. # If it's not the module, and not in a class instance, add the code
  162. # object's name.
  163. namespace = namespace + "." + cf.f_code.co_name
  164. logger = Logger(level=level, namespace=namespace, logger=logger,
  165. observer=observer)
  166. return logger
  167. @provider(ILogObserver)
  168. class _LogObserver(object):
  169. """
  170. Internal helper.
  171. An observer which formats events to a given file.
  172. """
  173. to_tx = {
  174. 'critical': LogLevel.critical,
  175. 'error': LogLevel.error,
  176. 'warn': LogLevel.warn,
  177. 'info': LogLevel.info,
  178. 'debug': LogLevel.debug,
  179. 'trace': LogLevel.debug,
  180. }
  181. def __init__(self, out):
  182. self._file = out
  183. self._encode = guess_stream_needs_encoding(out)
  184. self._levels = None
  185. def _acceptable_level(self, level):
  186. if self._levels is None:
  187. target_level = log_levels.index(_log_level)
  188. self._levels = [
  189. self.to_tx[lvl]
  190. for lvl in log_levels
  191. if log_levels.index(lvl) <= target_level and lvl != "none"
  192. ]
  193. return level in self._levels
  194. def __call__(self, event):
  195. # it seems if a twisted.logger.Logger() has .failure() called
  196. # on it, the log_format will be None for the traceback after
  197. # "Unhandled error in Deferred" -- perhaps this is a Twisted
  198. # bug?
  199. if event['log_format'] is None:
  200. msg = '{0} {1}{2}'.format(
  201. formatTime(event["log_time"]),
  202. failure_format_traceback(event['log_failure']),
  203. os.linesep,
  204. )
  205. if self._encode:
  206. msg = msg.encode('utf8')
  207. self._file.write(msg)
  208. else:
  209. # although Logger will already have filtered out unwanted
  210. # levels, bare Logger instances from Twisted code won't have.
  211. if 'log_level' in event and self._acceptable_level(event['log_level']):
  212. msg = '{0} {1}{2}'.format(
  213. formatTime(event["log_time"]),
  214. formatEvent(event),
  215. os.linesep,
  216. )
  217. if self._encode:
  218. msg = msg.encode('utf8')
  219. self._file.write(msg)
  220. def start_logging(out=_stdout, level='info'):
  221. """
  222. Start logging to the file-like object in ``out``. By default, this
  223. is stdout.
  224. """
  225. global _loggers, _observer, _log_level, _started_logging
  226. if level not in log_levels:
  227. raise RuntimeError(
  228. "Invalid log level '{0}'; valid are: {1}".format(
  229. level, ', '.join(log_levels)
  230. )
  231. )
  232. if _started_logging:
  233. return
  234. _started_logging = True
  235. _log_level = level
  236. set_global_log_level(_log_level)
  237. if out:
  238. _observer = _LogObserver(out)
  239. _observers = []
  240. if _observer:
  241. _observers.append(_observer)
  242. globalLogBeginner.beginLoggingTo(_observers)
  243. _unspecified = object()
  244. class _TxApi(object):
  245. def __init__(self, config):
  246. self._config = config
  247. def failure_message(self, fail):
  248. """
  249. :param fail: must be an IFailedFuture
  250. returns a unicode error-message
  251. """
  252. try:
  253. return '{0}: {1}'.format(
  254. fail.value.__class__.__name__,
  255. fail.getErrorMessage(),
  256. )
  257. except Exception:
  258. return 'Failed to produce failure message for "{0}"'.format(fail)
  259. def failure_traceback(self, fail):
  260. """
  261. :param fail: must be an IFailedFuture
  262. returns a traceback instance
  263. """
  264. return fail.tb
  265. def failure_format_traceback(self, fail):
  266. """
  267. :param fail: must be an IFailedFuture
  268. returns a string
  269. """
  270. try:
  271. f = io.StringIO()
  272. fail.printTraceback(file=f)
  273. return f.getvalue()
  274. except Exception:
  275. return "Failed to format failure traceback for '{0}'".format(fail)
  276. def create_future(self, result=_unspecified, error=_unspecified, canceller=None):
  277. if result is not _unspecified and error is not _unspecified:
  278. raise ValueError("Cannot have both result and error.")
  279. f = Deferred(canceller=canceller)
  280. if result is not _unspecified:
  281. resolve(f, result)
  282. elif error is not _unspecified:
  283. reject(f, error)
  284. return f
  285. def create_future_success(self, result):
  286. return succeed(result)
  287. def create_future_error(self, error=None):
  288. return fail(create_failure(error))
  289. def as_future(self, fun, *args, **kwargs):
  290. # Twisted doesn't automagically deal with coroutines on Py3
  291. if iscoroutinefunction(fun):
  292. try:
  293. return ensureDeferred(fun(*args, **kwargs))
  294. except TypeError as e:
  295. return create_future_error(e)
  296. return maybeDeferred(fun, *args, **kwargs)
  297. def is_future(self, obj):
  298. return isinstance(obj, Deferred)
  299. def call_later(self, delay, fun, *args, **kwargs):
  300. return IReactorTime(self._get_loop()).callLater(delay, fun, *args, **kwargs)
  301. def make_batched_timer(self, bucket_seconds, chunk_size=100):
  302. """
  303. Creates and returns an object implementing
  304. :class:`txaio.IBatchedTimer`.
  305. :param bucket_seconds: the number of seconds in each bucket. That
  306. is, a value of 5 means that any timeout within a 5 second
  307. window will be in the same bucket, and get notified at the
  308. same time. This is only accurate to "milliseconds".
  309. :param chunk_size: when "doing" the callbacks in a particular
  310. bucket, this controls how many we do at once before yielding to
  311. the reactor.
  312. """
  313. def get_seconds():
  314. return self._get_loop().seconds()
  315. def create_delayed_call(delay, fun, *args, **kwargs):
  316. return self._get_loop().callLater(delay, fun, *args, **kwargs)
  317. return _BatchedTimer(
  318. bucket_seconds * 1000.0, chunk_size,
  319. seconds_provider=get_seconds,
  320. delayed_call_creator=create_delayed_call,
  321. )
  322. def is_called(self, future):
  323. return future.called
  324. def resolve(self, future, result=None):
  325. future.callback(result)
  326. def reject(self, future, error=None):
  327. if error is None:
  328. error = create_failure()
  329. elif isinstance(error, Exception):
  330. error = Failure(error)
  331. else:
  332. if not isinstance(error, Failure):
  333. raise RuntimeError("reject requires a Failure or Exception")
  334. future.errback(error)
  335. def cancel(self, future, msg=None):
  336. future.cancel()
  337. def create_failure(self, exception=None):
  338. """
  339. Create a Failure instance.
  340. if ``exception`` is None (the default), we MUST be inside an
  341. "except" block. This encapsulates the exception into an object
  342. that implements IFailedFuture
  343. """
  344. if exception:
  345. return Failure(exception)
  346. return Failure()
  347. def add_callbacks(self, future, callback, errback):
  348. """
  349. callback or errback may be None, but at least one must be
  350. non-None.
  351. """
  352. assert future is not None
  353. if callback is None:
  354. assert errback is not None
  355. future.addErrback(errback)
  356. else:
  357. # Twisted allows errback to be None here
  358. future.addCallbacks(callback, errback)
  359. return future
  360. def gather(self, futures, consume_exceptions=True):
  361. def completed(res):
  362. rtn = []
  363. for (ok, value) in res:
  364. rtn.append(value)
  365. if not ok and not consume_exceptions:
  366. value.raiseException()
  367. return rtn
  368. # XXX if consume_exceptions is False in asyncio.gather(), it will
  369. # abort on the first raised exception -- should we set
  370. # fireOnOneErrback=True (if consume_exceptions=False?) -- but then
  371. # we'll have to wrap the errback() to extract the "real" failure
  372. # from the FirstError that gets thrown if you set that ...
  373. dl = DeferredList(list(futures), consumeErrors=consume_exceptions)
  374. # we unpack the (ok, value) tuples into just a list of values, so
  375. # that the callback() gets the same value in asyncio and Twisted.
  376. add_callbacks(dl, completed, None)
  377. return dl
  378. def sleep(self, delay):
  379. """
  380. Inline sleep for use in co-routines.
  381. :param delay: Time to sleep in seconds.
  382. :type delay: float
  383. """
  384. d = Deferred()
  385. self._get_loop().callLater(delay, d.callback, None)
  386. return d
  387. def _get_loop(self):
  388. """
  389. internal helper
  390. """
  391. # we import and assign the default here (and not, e.g., when
  392. # making Config) so as to delay importing reactor as long as
  393. # possible in case someone is installing a custom one.
  394. if self._config.loop is None:
  395. from twisted.internet import reactor
  396. self._config.loop = reactor
  397. return self._config.loop
  398. def set_global_log_level(level):
  399. """
  400. Set the global log level on all loggers instantiated by txaio.
  401. """
  402. for item in _loggers:
  403. if not item._log_level_set_explicitly:
  404. item._set_log_level(level)
  405. global _log_level
  406. _log_level = level
  407. def get_global_log_level():
  408. return _log_level
  409. _default_api = _TxApi(config)
  410. failure_message = _default_api.failure_message
  411. failure_traceback = _default_api.failure_traceback
  412. failure_format_traceback = _default_api.failure_format_traceback
  413. create_future = _default_api.create_future
  414. create_future_success = _default_api.create_future_success
  415. create_future_error = _default_api.create_future_error
  416. as_future = _default_api.as_future
  417. is_future = _default_api.is_future
  418. call_later = _default_api.call_later
  419. make_batched_timer = _default_api.make_batched_timer
  420. is_called = _default_api.is_called
  421. resolve = _default_api.resolve
  422. reject = _default_api.reject
  423. cancel = _default_api.cancel
  424. create_failure = _default_api.create_failure
  425. add_callbacks = _default_api.add_callbacks
  426. gather = _default_api.gather
  427. sleep = _default_api.sleep
  428. time_ns = _util.time_ns
  429. perf_counter_ns = _util.perf_counter_ns