diff -Nru crochet-1.4.0/crochet/_eventloop.py crochet-2.0.0/crochet/_eventloop.py --- crochet-1.4.0/crochet/_eventloop.py 2015-04-18 22:10:31.000000000 +0000 +++ crochet-2.0.0/crochet/_eventloop.py 2021-05-10 20:34:24.000000000 +0000 @@ -8,41 +8,25 @@ import threading import weakref import warnings +from inspect import iscoroutinefunction from functools import wraps -import imp - from twisted.python import threadable from twisted.python.runtime import platform from twisted.python.failure import Failure from twisted.python.log import PythonLoggingObserver, err -from twisted.internet.defer import maybeDeferred +from twisted.internet.defer import maybeDeferred, ensureDeferred from twisted.internet.task import LoopingCall +import wrapt + from ._util import synchronized from ._resultstore import ResultStore _store = ResultStore() -if hasattr(weakref, "WeakSet"): - WeakSet = weakref.WeakSet -else: - class WeakSet(object): - """ - Minimal WeakSet emulation. - """ - def __init__(self): - self._items = weakref.WeakKeyDictionary() - - def add(self, value): - self._items[value] = True - - def __iter__(self): - return iter(self._items) - - -class TimeoutError(Exception): +class TimeoutError(Exception): # pylint: disable=redefined-builtin """ A timeout has been hit. """ @@ -67,8 +51,9 @@ ReactorStopped exception to unblock any remaining EventualResult.wait() calls. """ - def __init__(self, reactor): - self._results = WeakSet() + + def __init__(self): + self._results = weakref.WeakSet() self._stopped = False self._lock = threading.Lock() @@ -127,6 +112,7 @@ Should only be run in Twisted thread, and only called once. """ self._deferred = deferred + # Because we use __del__, we need to make sure there are no cycles # involving this object, which is why we use a weakref: def put(result, eventual=weakref.ref(self)): @@ -135,6 +121,7 @@ eventual._set_result(result) else: err(result, "Unhandled error in EventualResult") + deferred.addBoth(put) def _set_result(self, result): @@ -168,7 +155,7 @@ """ self._reactor.callFromThread(lambda: self._deferred.cancel()) - def _result(self, timeout=None): + def _result(self, timeout): """ Return the result, if available. @@ -181,11 +168,6 @@ returned on one call, additional calls will return/raise the same result. """ - if timeout is None: - warnings.warn("Unlimited timeouts are deprecated.", - DeprecationWarning, stacklevel=3) - # Queue.get(None) won't get interrupted by Ctrl-C... - timeout = 2 ** 31 self._result_set.wait(timeout) # In Python 2.6 we can't rely on the return result of wait(), so we # have to check manually: @@ -194,7 +176,7 @@ self._result_retrieved = True return self._value - def wait(self, timeout=None): + def wait(self, timeout): """ Return the result, or throw the exception if result is a failure. @@ -211,21 +193,6 @@ raise RuntimeError( "EventualResult.wait() must not be run in the reactor thread.") - if imp.lock_held(): - try: - imp.release_lock() - except RuntimeError: - # The lock is held by some other thread. We should be safe - # to continue. - pass - else: - # If EventualResult.wait() is run during module import, if the - # Twisted code that is being run also imports something the result - # will be a deadlock. Even if that is not an issue it would - # prevent importing in other threads until the call returns. - raise RuntimeError( - "EventualResult.wait() must not be run at module import time.") - result = self._result(timeout) if isinstance(result, Failure): result.raiseException() @@ -266,10 +233,17 @@ In particular, used to wrap PythonLoggingObserver, so that blocking logging.py Handlers don't block the event loop. + + Once Python 3.6 support is dropped, this can use a queue.SimpleQueue object + instead of a whole 'nother event loop. """ + def __init__(self, observer): self._observer = observer - if getattr(select, "poll", None): + if getattr(select, "epoll", None): + from twisted.internet.epollreactor import EPollReactor + reactorFactory = EPollReactor + elif getattr(select, "poll", None): from twisted.internet.pollreactor import PollReactor reactorFactory = PollReactor else: @@ -277,8 +251,8 @@ reactorFactory = SelectReactor self._logWritingReactor = reactorFactory() self._logWritingReactor._registerAsIOThread = False - self._thread = threading.Thread(target=self._reader, - name="CrochetLogWriter") + self._thread = threading.Thread( + target=self._reader, name="CrochetLogWriter") self._thread.start() def _reader(self): @@ -298,17 +272,31 @@ """ A log observer that writes to a queue. """ - self._logWritingReactor.callFromThread(self._observer, msg) + + def log(): + try: + self._observer(msg) + except Exception: + # Lower-level logging system blew up, nothing we can do, so + # just drop on the floor. + pass + + self._logWritingReactor.callFromThread(log) class EventLoop(object): """ Initialization infrastructure for running a reactor in a thread. """ - def __init__(self, reactorFactory, atexit_register, - startLoggingWithObserver=None, - watchdog_thread=None, - reapAllProcesses=None): + + def __init__( + self, + reactorFactory, + atexit_register, + startLoggingWithObserver=None, + watchdog_thread=None, + reapAllProcesses=None + ): """ reactorFactory: Zero-argument callable that returns a reactor. atexit_register: atexit.register, or look-alike. @@ -340,7 +328,7 @@ """ self._started = True self._reactor = self._reactorFactory() - self._registry = ResultRegistry(self._reactor) + self._registry = ResultRegistry() # We want to unblock EventualResult regardless of how the reactor is # run, so we always register this: self._reactor.addSystemEventTrigger( @@ -364,6 +352,7 @@ self._reactor.callFromThread(self._startReapingProcesses) if self._startLoggingWithObserver: observer = ThreadLogObserver(PythonLoggingObserver().emit) + def start(): # Twisted is going to override warnings.showwarning; let's # make sure that has no effect: @@ -372,18 +361,18 @@ log.showwarning = warnings.showwarning self._startLoggingWithObserver(observer, False) log.showwarning = original + self._reactor.callFromThread(start) # We only want to stop the logging thread once the reactor has # shut down: - self._reactor.addSystemEventTrigger("after", "shutdown", - observer.stop) + self._reactor.addSystemEventTrigger( + "after", "shutdown", observer.stop) t = threading.Thread( target=lambda: self._reactor.run(installSignalHandlers=False), name="CrochetReactor") t.start() - self._atexit_register(self._reactor.callFromThread, - self._reactor.stop) + self._atexit_register(self._reactor.callFromThread, self._reactor.stop) self._atexit_register(_store.log_errors) if self._watchdog_thread is not None: self._watchdog_thread.start() @@ -401,44 +390,51 @@ If no_setup() is called after setup(), a RuntimeError is raised. """ if self._started: - raise RuntimeError("no_setup() is intended to be called once, by a" - " Twisted application, before any libraries " - "using crochet are imported and call setup().") + raise RuntimeError( + "no_setup() is intended to be called once, by a" + " Twisted application, before any libraries " + "using crochet are imported and call setup().") self._common_setup() def run_in_reactor(self, function): """ - A decorator that ensures the wrapped function runs in the reactor thread. + A decorator that ensures the wrapped function runs in the + reactor thread. When the wrapped function is called, an EventualResult is returned. """ - def runs_in_reactor(result, args, kwargs): - d = maybeDeferred(function, *args, **kwargs) - result._connect_deferred(d) + def _run_in_reactor(wrapped, _, args, kwargs): + """ + Implementation: A decorator that ensures the wrapped function runs in + the reactor thread. + + When the wrapped function is called, an EventualResult is returned. + """ + + if iscoroutinefunction(wrapped): + def runs_in_reactor(result, args, kwargs): + d = ensureDeferred(wrapped(*args, **kwargs)) + result._connect_deferred(d) + else: + def runs_in_reactor(result, args, kwargs): + d = maybeDeferred(wrapped, *args, **kwargs) + result._connect_deferred(d) - @wraps(function) - def wrapper(*args, **kwargs): result = EventualResult(None, self._reactor) self._registry.register(result) self._reactor.callFromThread(runs_in_reactor, result, args, kwargs) return result - wrapper.wrapped_function = function - return wrapper - - def wait_for_reactor(self, function): - """ - DEPRECATED, use wait_for(timeout) instead. - A decorator that ensures the wrapped function runs in the reactor thread. + if iscoroutinefunction(function): + # Create a non-async wrapper with same signature. + @wraps(function) + def non_async_wrapper(): + pass + else: + # Just use default behavior of looking at underlying object. + non_async_wrapper = None - When the wrapped function is called, its result is returned or its - exception raised. Deferreds are handled transparently. - """ - warnings.warn("@wait_for_reactor is deprecated, use @wait_for instead", - DeprecationWarning, stacklevel=2) - # This will timeout, in theory. In practice the process will be dead - # long before that. - return self.wait_for(2 ** 31)(function) + return wrapt.decorator(_run_in_reactor, adapter=non_async_wrapper)(function) def wait_for(self, timeout): """ @@ -450,38 +446,33 @@ timeout after the given number of seconds (a float), raising a crochet.TimeoutError, and cancelling the Deferred being waited on. """ + def decorator(function): - @wraps(function) - def wrapper(*args, **kwargs): + def wrapper(function, _, args, kwargs): @self.run_in_reactor def run(): - return function(*args, **kwargs) + if iscoroutinefunction(function): + return ensureDeferred(function(*args, **kwargs)) + else: + return function(*args, **kwargs) + eventual_result = run() try: return eventual_result.wait(timeout) except TimeoutError: eventual_result.cancel() raise - wrapper.wrapped_function = function - return wrapper - return decorator - - def in_reactor(self, function): - """ - DEPRECATED, use run_in_reactor. - A decorator that ensures the wrapped function runs in the reactor thread. + if iscoroutinefunction(function): + # Create a non-async wrapper with same signature. + @wraps(function) + def non_async_wrapper(): + pass + else: + # Just use default behavior of looking at underlying object. + non_async_wrapper = None - The wrapped function will get the reactor passed in as a first - argument, in addition to any arguments it is called with. + wrapper = wrapt.decorator(wrapper, adapter=non_async_wrapper) + return wrapper(function) - When the wrapped function is called, an EventualResult is returned. - """ - warnings.warn("@in_reactor is deprecated, use @run_in_reactor", - DeprecationWarning, stacklevel=2) - @self.run_in_reactor - @wraps(function) - def add_reactor(*args, **kwargs): - return function(self._reactor, *args, **kwargs) - - return add_reactor + return decorator diff -Nru crochet-1.4.0/crochet/__init__.py crochet-2.0.0/crochet/__init__.py --- crochet-1.4.0/crochet/__init__.py 2014-05-31 19:55:36.000000000 +0000 +++ crochet-2.0.0/crochet/__init__.py 2021-05-10 20:34:24.000000000 +0000 @@ -2,30 +2,23 @@ Crochet: Use Twisted Anywhere! """ -from __future__ import absolute_import - -import sys - from twisted.python.log import startLoggingWithObserver from twisted.python.runtime import platform + +from ._shutdown import _watchdog, register +from ._eventloop import ( + EventualResult, EventLoop, _store, ReactorStopped +) +from ._eventloop import TimeoutError # pylint: disable=redefined-builtin +from ._version import get_versions + if platform.type == "posix": - try: - from twisted.internet.process import reapAllProcesses - except (SyntaxError, ImportError): - if sys.version_info < (3, 3, 0): - raise - else: - # Process support is still not ported to Python 3 on some versions - # of Twisted. - reapAllProcesses = lambda: None + from twisted.internet.process import reapAllProcesses else: # waitpid() is only necessary on POSIX: - reapAllProcesses = lambda: None + def reapAllProcesses(): pass + -from ._shutdown import _watchdog, register -from ._eventloop import (EventualResult, TimeoutError, EventLoop, _store, - ReactorStopped) -from ._version import get_versions __version__ = get_versions()['version'] del get_versions @@ -33,24 +26,26 @@ def _importReactor(): from twisted.internet import reactor return reactor -_main = EventLoop(_importReactor, register, startLoggingWithObserver, - _watchdog, reapAllProcesses) + + +_main = EventLoop( + _importReactor, register, startLoggingWithObserver, _watchdog, + reapAllProcesses) setup = _main.setup no_setup = _main.no_setup run_in_reactor = _main.run_in_reactor wait_for = _main.wait_for retrieve_result = _store.retrieve -# Backwards compatibility with 0.5.0: -in_reactor = _main.in_reactor -DeferredResult = EventualResult - -# Backwards compatibility with 1.1.0 and earlier: -wait_for_reactor = _main.wait_for_reactor - -__all__ = ["setup", "run_in_reactor", "EventualResult", "TimeoutError", - "retrieve_result", "no_setup", "wait_for", - "ReactorStopped", "__version__", - # Backwards compatibility: - "DeferredResult", "in_reactor", "wait_for_reactor", - ] + +__all__ = [ + "setup", + "run_in_reactor", + "EventualResult", + "TimeoutError", + "retrieve_result", + "no_setup", + "wait_for", + "ReactorStopped", + "__version__", +] diff -Nru crochet-1.4.0/crochet/__init__.pyi crochet-2.0.0/crochet/__init__.pyi --- crochet-1.4.0/crochet/__init__.pyi 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet/__init__.pyi 2021-05-10 20:34:24.000000000 +0000 @@ -0,0 +1,29 @@ +import sys + +from typing import Any, Callable, Generic, Optional, TypeVar +from twisted.python.failure import Failure + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_F = TypeVar("_F", bound=Callable[..., Any]) + +def setup() -> None: ... +def run_in_reactor( + function: Callable[..., _T] +) -> Callable[..., EventualResult[_T]]: ... + +class EventualResult(Generic[_T_co]): + def cancel(self) -> None: ... + def wait(self, timeout: float) -> _T_co: ... + def stash(self) -> int: ... + def original_failure(self) -> Optional[Failure]: ... + +class TimeoutError(Exception): ... + +def retrieve_result(result_id: int) -> EventualResult[object]: ... +def no_setup() -> None: ... +def wait_for(timeout: float) -> Callable[[_F], _F]: ... + +class ReactorStopped(Exception): ... + +__version__: str diff -Nru crochet-1.4.0/crochet/mypy.py crochet-2.0.0/crochet/mypy.py --- crochet-1.4.0/crochet/mypy.py 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet/mypy.py 2021-05-10 20:34:24.000000000 +0000 @@ -0,0 +1,56 @@ +""" +Mypy plugin to aid with typechecking code that uses Crochet. +""" + +import typing +from typing import Callable, Optional + +from mypy.plugin import FunctionContext, Plugin # pylint: disable=no-name-in-module +from mypy.types import CallableType, Type, get_proper_type # pylint: disable=no-name-in-module + + +def plugin(_version: str) -> typing.Type[Plugin]: + return CrochetMypyPlugin + + +class CrochetMypyPlugin(Plugin): + """ + Assists mypy with type checking APIs not (yet) fully covered by Python's + type hint annotation types, by copying run_in_reactor decorated function's + argument types to the type mypy deduces for the wrapped function. + """ + + def get_function_hook( + self, + fullname: str, + ) -> Optional[Callable[[FunctionContext], Type]]: + if fullname == "crochet.run_in_reactor": + return _copyargs_callback + return None + + +def _copyargs_callback(ctx: FunctionContext) -> Type: + """ + Copy the parameters from the signature of the type of the argument of the + call to the signature of the return type. + """ + original_return_type = ctx.default_return_type + if not ctx.arg_types or len(ctx.arg_types[0]) != 1: + return original_return_type + + arg_type = get_proper_type(ctx.arg_types[0][0]) + default_return_type = get_proper_type(original_return_type) + + if not ( + isinstance(arg_type, CallableType) + and isinstance(default_return_type, CallableType) + ): + return original_return_type + + return default_return_type.copy_modified( + arg_types=arg_type.arg_types, + arg_kinds=arg_type.arg_kinds, + arg_names=arg_type.arg_names, + variables=arg_type.variables, + is_ellipsis_args=arg_type.is_ellipsis_args, + ) diff -Nru crochet-1.4.0/crochet/_resultstore.py crochet-2.0.0/crochet/_resultstore.py --- crochet-1.4.0/crochet/_resultstore.py 2013-09-16 15:56:42.000000000 +0000 +++ crochet-2.0.0/crochet/_resultstore.py 2017-08-09 20:33:23.000000000 +0000 @@ -20,6 +20,7 @@ EventualResults that are not retrieved by shutdown will be logged if they have an error result. """ + def __init__(self): self._counter = 0 self._stored = {} @@ -53,4 +54,3 @@ failure = result.original_failure() if failure is not None: log.err(failure, "Unhandled error in stashed EventualResult:") - diff -Nru crochet-1.4.0/crochet/_shutdown.py crochet-2.0.0/crochet/_shutdown.py --- crochet-1.4.0/crochet/_shutdown.py 2015-05-06 00:26:28.000000000 +0000 +++ crochet-2.0.0/crochet/_shutdown.py 2019-06-07 18:56:32.000000000 +0000 @@ -33,6 +33,7 @@ """ A registry of functions that can be called all at once. """ + def __init__(self): self._functions = [] @@ -49,17 +50,14 @@ for f in reversed(self._functions): try: f() - except: + except Exception: log.err() # This is... fragile. Not sure how else to do it though. _registry = FunctionRegistry() _watchdog = Watchdog( - [ - t for t in threading.enumerate() - if isinstance(t, threading._MainThread) - ][0], - _registry.run, -) + [t for t in threading.enumerate() + if isinstance(t, threading._MainThread)][0], + _registry.run, ) register = _registry.register diff -Nru crochet-1.4.0/crochet/tests/test_api.py crochet-2.0.0/crochet/tests/test_api.py --- crochet-1.4.0/crochet/tests/test_api.py 2015-04-25 17:26:04.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_api.py 2021-05-10 20:34:24.000000000 +0000 @@ -13,12 +13,23 @@ import tempfile import os import imp +import inspect +from unittest import SkipTest from twisted.trial.unittest import TestCase from twisted.internet.defer import succeed, Deferred, fail, CancelledError from twisted.python.failure import Failure from twisted.python import threadable from twisted.python.runtime import platform + +from .._eventloop import ( + EventLoop, EventualResult, TimeoutError, ResultRegistry, ReactorStopped) +from .test_setup import FakeReactor +from .. import ( + _main, setup, retrieve_result, _store, no_setup, + run_in_reactor, wait_for) +from ..tests import crochet_directory + if platform.type == "posix": try: from twisted.internet.process import reapAllProcesses @@ -33,24 +44,18 @@ # waitpid() is only necessary on POSIX: reapAllProcesses = None -from .._eventloop import (EventLoop, EventualResult, TimeoutError, - ResultRegistry, ReactorStopped) -from .test_setup import FakeReactor -from .. import (_main, setup, in_reactor, retrieve_result, _store, no_setup, - run_in_reactor, wait_for_reactor, wait_for) -from ..tests import crochet_directory - class ResultRegistryTests(TestCase): """ Tests for ResultRegistry. """ + def test_stopped_registered(self): """ ResultRegistery.stop() fires registered EventualResult with ReactorStopped. """ - registry = ResultRegistry(FakeReactor()) + registry = ResultRegistry() er = EventualResult(None, None) registry.register(er) registry.stop() @@ -61,7 +66,7 @@ After ResultRegistery.stop() is called subsequent register() calls raise ReactorStopped. """ - registry = ResultRegistry(FakeReactor()) + registry = ResultRegistry() er = EventualResult(None, None) registry.stop() self.assertRaises(ReactorStopped, registry.register, er) @@ -71,20 +76,20 @@ ResultRegistery.stop() has no impact on registered EventualResult which already have a result. """ - registry = ResultRegistry(FakeReactor()) + registry = ResultRegistry() er = EventualResult(succeed(123), None) registry.register(er) registry.stop() - self.assertEqual(er.wait(), 123) - self.assertEqual(er.wait(), 123) - self.assertEqual(er.wait(), 123) + self.assertEqual(er.wait(0.1), 123) + self.assertEqual(er.wait(0.1), 123) + self.assertEqual(er.wait(0.1), 123) def test_weakref(self): """ Registering an EventualResult with a ResultRegistry does not prevent it from being garbage collected. """ - registry = ResultRegistry(FakeReactor()) + registry = ResultRegistry() er = EventualResult(None, None) registry.register(er) ref = weakref.ref(er) @@ -101,6 +106,32 @@ self.assertTrue(ResultRegistry.register.synchronized) +def append_in_thread(a_list, f, *args, **kwargs): + """ + Call a function in a thread, append its result to the given list. + + Only return once the thread has actually started. + + Will return a threading.Event that will be set when the action is done. + """ + started = threading.Event() + done = threading.Event() + + def go(): + started.set() + try: + result = f(*args, **kwargs) + except Exception as e: + a_list.extend([False, e]) + else: + a_list.extend([True, result]) + done.set() + + threading.Thread(target=go).start() + started.wait() + return done + + class EventualResultTests(TestCase): """ Tests for EventualResult. @@ -114,7 +145,7 @@ wait() returns the value the Deferred fired with. """ dr = EventualResult(succeed(123), None) - self.assertEqual(dr.wait(), 123) + self.assertEqual(dr.wait(0.1), 123) def test_later_success_result(self): """ @@ -122,27 +153,29 @@ the Deferred is fired after wait() is called. """ d = Deferred() - def fireSoon(): - import time; time.sleep(0.01) - d.callback(345) - threading.Thread(target=fireSoon).start() dr = EventualResult(d, None) - self.assertEqual(dr.wait(), 345) + result_list = [] + done = append_in_thread(result_list, dr.wait, 100) + time.sleep(0.1) + # At this point dr.wait() should have started: + d.callback(345) + done.wait(100) + self.assertEqual(result_list, [True, 345]) def test_success_result_twice(self): """ A second call to wait() returns same value as the first call. """ dr = EventualResult(succeed(123), None) - self.assertEqual(dr.wait(), 123) - self.assertEqual(dr.wait(), 123) + self.assertEqual(dr.wait(0.1), 123) + self.assertEqual(dr.wait(0.1), 123) def test_failure_result(self): """ wait() raises the exception the Deferred fired with. """ dr = EventualResult(fail(RuntimeError()), None) - self.assertRaises(RuntimeError, dr.wait) + self.assertRaises(RuntimeError, dr.wait, 0.1) def test_later_failure_result(self): """ @@ -150,20 +183,22 @@ where the Deferred is fired after wait() is called. """ d = Deferred() - def fireSoon(): - time.sleep(0.01) - d.errback(RuntimeError()) - threading.Thread(target=fireSoon).start() dr = EventualResult(d, None) - self.assertRaises(RuntimeError, dr.wait) + result_list = [] + done = append_in_thread(result_list, dr.wait, 100) + time.sleep(0.1) + d.errback(RuntimeError()) + done.wait(100) + self.assertEqual( + (result_list[0], result_list[1].__class__), (False, RuntimeError)) def test_failure_result_twice(self): """ A second call to wait() raises same value as the first call. """ dr = EventualResult(fail(ZeroDivisionError()), None) - self.assertRaises(ZeroDivisionError, dr.wait) - self.assertRaises(ZeroDivisionError, dr.wait) + self.assertRaises(ZeroDivisionError, dr.wait, 0.1) + self.assertRaises(ZeroDivisionError, dr.wait, 0.1) def test_timeout(self): """ @@ -192,8 +227,8 @@ dr = EventualResult(d, None) self.assertRaises(TimeoutError, dr.wait, timeout=0.01) d.callback(u"value") - self.assertEqual(dr.wait(), u"value") - self.assertEqual(dr.wait(), u"value") + self.assertEqual(dr.wait(0.1), u"value") + self.assertEqual(dr.wait(0.1), u"value") def test_reactor_thread_disallowed(self): """ @@ -211,6 +246,7 @@ """ reactor = FakeReactor() cancelled = [] + def error(f): cancelled.append(reactor.in_call_from_thread) cancelled.append(f) @@ -235,8 +271,8 @@ wrapped by the EventualResult. """ try: - 1/0 - except: + 1 / 0 + except ZeroDivisionError: f = Failure() dr = EventualResult(fail(f), None) self.assertIdentical(dr.original_failure(), f) @@ -303,6 +339,8 @@ If you're wait()ing on an EventualResult in main thread, make sure the KeyboardInterrupt happens in timely manner. """ + if platform.type != "posix": + raise SkipTest("I don't have the energy to fight Windows semantics.") program = """\ import os, threading, signal, time, sys import crochet @@ -325,21 +363,18 @@ # Still running, test shall fail... os.kill(os.getpid(), sig_kill) -t = threading.Thread(target=interrupt) -t.setDaemon(True) +t = threading.Thread(target=interrupt, daemon=True) t.start() d = Deferred() e = crochet.EventualResult(d, None) try: - # Queue.get() has special non-interruptible behavior if not given timeout, - # so don't give timeout here. - e.wait() + e.wait(10000) except KeyboardInterrupt: sys.exit(23) """ - kw = { 'cwd': crochet_directory } + kw = {'cwd': crochet_directory} # on Windows the only way to interrupt a subprocess reliably is to # create a new process group: # http://docs.python.org/2/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP @@ -360,7 +395,7 @@ er._connect_deferred(d) self.assertRaises(TimeoutError, er.wait, 0) d.callback(123) - self.assertEqual(er.wait(), 123) + self.assertEqual(er.wait(0.1), 123) def test_reactor_stop_unblocks_EventualResult(self): """ @@ -454,8 +489,9 @@ else: sys.exit(3) """ - process = subprocess.Popen([sys.executable, "-c", program], - cwd=crochet_directory,) + process = subprocess.Popen( + [sys.executable, "-c", program], + cwd=crochet_directory, ) self.assertEqual(process.wait(), 23) def test_noWaitingDuringImport(self): @@ -470,15 +506,18 @@ """ if sys.version_info[0] > 2: from unittest import SkipTest - raise SkipTest("This test is too fragile (and insufficient) on " - "Python 3 - see " - "https://github.com/itamarst/crochet/issues/43") + raise SkipTest( + "This test is too fragile (and insufficient) on " + "Python 3 - see " + "https://github.com/itamarst/crochet/issues/43") directory = tempfile.mktemp() os.mkdir(directory) sys.path.append(directory) self.addCleanup(sys.path.remove, directory) - with open(os.path.join(directory, "shouldbeunimportable.py"), "w") as f: - f.write("""\ + with open(os.path.join(directory, "shouldbeunimportable.py"), + "w") as f: + f.write( + """\ from crochet import EventualResult from twisted.internet.defer import Deferred @@ -518,7 +557,7 @@ # we want to run .wait while the other thread has the lock acquired assertions.append((imp.lock_held(), True)) try: - assertions.append((er.wait(), 123)) + assertions.append((er.wait(0.1), 123)) finally: test_complete.set() @@ -531,106 +570,113 @@ [self.assertEqual(result, expected) for result, expected in assertions] -class InReactorTests(TestCase): +class RunInReactorTests(TestCase): """ - Tests for the deprecated in_reactor decorator. + Tests for the run_in_reactor decorator. """ + def test_signature(self): + """ + The function decorated with the run_in_reactor decorator has the same + signature as the original function. + """ + c = EventLoop(lambda: FakeReactor(), lambda f, g: None) + + def some_name(arg1, arg2, karg1=2, *args, **kw): + pass + decorated = c.run_in_reactor(some_name) + self.assertEqual(inspect.signature(some_name), + inspect.signature(decorated)) + def test_name(self): """ - The function decorated with in_reactor has the same name as the + The function decorated with run_in_reactor has the same name as the original function. """ c = EventLoop(lambda: FakeReactor(), lambda f, g: None) - @c.in_reactor - def some_name(reactor): + @c.run_in_reactor + def some_name(): pass + self.assertEqual(some_name.__name__, "some_name") - def test_in_reactor_thread(self): + def test_run_in_reactor_thread(self): """ - The function decorated with in_reactor is run in the reactor - thread, and takes the reactor as its first argument. + The function decorated with run_in_reactor is run in the reactor + thread. """ myreactor = FakeReactor() c = EventLoop(lambda: myreactor, lambda f, g: None) c.no_setup() - calls = [] - @c.in_reactor - def func(reactor, a, b, c): - self.assertIdentical(reactor, myreactor) - self.assertTrue(reactor.in_call_from_thread) + @c.run_in_reactor + def func(a, b, c): + self.assertTrue(myreactor.in_call_from_thread) calls.append((a, b, c)) func(1, 2, c=3) self.assertEqual(calls, [(1, 2, 3)]) - def test_run_in_reactor_wrapper(self): + def test_method(self): """ - in_reactor is implemented on top of run_in_reactor. + The function decorated with the wait decorator can be a method. """ - wrapped = [False] - - def fake_run_in_reactor(function): - def wrapper(*args, **kwargs): - wrapped[0] = True - result = function(*args, **kwargs) - wrapped[0] = False - return result - return wrapper - myreactor = FakeReactor() c = EventLoop(lambda: myreactor, lambda f, g: None) c.no_setup() - c.run_in_reactor = fake_run_in_reactor - - - @c.in_reactor - def func(reactor): - self.assertTrue(wrapped[0]) - return 17 - - result = func() - self.assertFalse(wrapped[0]) - self.assertEqual(result, 17) + calls = [] + class C(object): + @c.run_in_reactor + def func(self, a, b, c): + calls.append((self, a, b, c)) + + o = C() + o.func(1, 2, c=3) + self.assertEqual(calls, [(o, 1, 2, 3)]) -class RunInReactorTests(TestCase): - """ - Tests for the run_in_reactor decorator. - """ - def test_name(self): + def test_classmethod(self): """ - The function decorated with run_in_reactor has the same name as the - original function. + The function decorated with the wait decorator can be a classmethod. """ - c = EventLoop(lambda: FakeReactor(), lambda f, g: None) + myreactor = FakeReactor() + c = EventLoop(lambda: myreactor, lambda f, g: None) + c.no_setup() + calls = [] - @c.run_in_reactor - def some_name(): - pass - self.assertEqual(some_name.__name__, "some_name") + class C(object): + @c.run_in_reactor + @classmethod + def func(cls, a, b, c): + calls.append((cls, a, b, c)) + + @classmethod + @c.run_in_reactor + def func2(cls, a, b, c): + calls.append((cls, a, b, c)) + + C.func(1, 2, c=3) + C.func2(1, 2, c=3) + self.assertEqual(calls, [(C, 1, 2, 3), (C, 1, 2, 3)]) - def test_run_in_reactor_thread(self): + def test_wrap_method(self): """ - The function decorated with run_in_reactor is run in the reactor - thread. + The object decorated with the wait decorator can be a method object """ myreactor = FakeReactor() c = EventLoop(lambda: myreactor, lambda f, g: None) c.no_setup() calls = [] - @c.run_in_reactor - def func(a, b, c): - self.assertTrue(myreactor.in_call_from_thread) - calls.append((a, b, c)) - - func(1, 2, c=3) - self.assertEqual(calls, [(1, 2, 3)]) + class C(object): + def func(self, a, b, c): + calls.append((a, b, c)) + + f = c.run_in_reactor(C().func) + f(4, 5, c=6) + self.assertEqual(calls, [(4, 5, 6)]) def make_wrapped_function(self): """ @@ -644,6 +690,7 @@ @c.run_in_reactor def passthrough(argument): return argument + return passthrough def test_deferred_success_result(self): @@ -654,7 +701,7 @@ passthrough = self.make_wrapped_function() result = passthrough(succeed(123)) self.assertIsInstance(result, EventualResult) - self.assertEqual(result.wait(), 123) + self.assertEqual(result.wait(0.1), 123) def test_deferred_failure_result(self): """ @@ -665,7 +712,7 @@ passthrough = self.make_wrapped_function() result = passthrough(fail(ZeroDivisionError())) self.assertIsInstance(result, EventualResult) - self.assertRaises(ZeroDivisionError, result.wait) + self.assertRaises(ZeroDivisionError, result.wait, 0.1) def test_regular_result(self): """ @@ -675,7 +722,7 @@ passthrough = self.make_wrapped_function() result = passthrough(123) self.assertIsInstance(result, EventualResult) - self.assertEqual(result.wait(), 123) + self.assertEqual(result.wait(0.1), 123) def test_exception_result(self): """ @@ -688,11 +735,11 @@ @c.run_in_reactor def raiser(): - 1/0 + 1 / 0 result = raiser() self.assertIsInstance(result, EventualResult) - self.assertRaises(ZeroDivisionError, result.wait) + self.assertRaises(ZeroDivisionError, result.wait, 0.1) def test_registry(self): """ @@ -712,30 +759,50 @@ def test_wrapped_function(self): """ The function wrapped by @run_in_reactor can be accessed via the - `wrapped_function` attribute. + `__wrapped__` attribute. """ c = EventLoop(lambda: None, lambda f, g: None) + def func(): pass + wrapper = c.run_in_reactor(func) - self.assertIdentical(wrapper.wrapped_function, func) + self.assertIdentical(wrapper.__wrapped__, func) + def test_async_function(self): + """ + Async functions can be wrapped with @run_in_reactor. + """ + myreactor = FakeReactor() + c = EventLoop(lambda: myreactor, lambda f, g: None) + c.no_setup() + calls = [] + + @c.run_in_reactor + async def go(): + self.assertTrue(myreactor.in_call_from_thread) + calls.append(1) + return 23 -class WaitTestsMixin(object): + self.assertEqual((go().wait(0.1), go().wait(0.1)), (23, 23)) + self.assertEqual(len(calls), 2) + self.assertFalse(inspect.iscoroutinefunction(go)) + + +class WaitTests(TestCase): """ - Tests mixin for the wait_for_reactor/wait_for decorators. + Tests for wait_for decorators. """ + def setUp(self): self.reactor = FakeReactor() self.eventloop = EventLoop(lambda: self.reactor, lambda f, g: None) self.eventloop.no_setup() + DECORATOR_CALL = "wait_for(timeout=5)" + def decorator(self): - """ - Return a callable that decorates a function, using the decorator being - tested. - """ - raise NotImplementedError() + return lambda func: self.eventloop.wait_for(timeout=5)(func) def make_wrapped_function(self): """ @@ -743,11 +810,13 @@ its first argument, or raises it if it's an exception. """ decorator = self.decorator() + @decorator def passthrough(argument): if isinstance(argument, Exception): raise argument return argument + return passthrough def test_name(self): @@ -756,21 +825,38 @@ original function. """ decorator = self.decorator() + @decorator def some_name(argument): pass + self.assertEqual(some_name.__name__, "some_name") + def test_signature(self): + """ + The function decorated with the wait decorator has the same signature + as the original function. + """ + decorator = self.decorator() + + def some_name(arg1, arg2, karg1=2, *args, **kw): + pass + decorated = decorator(some_name) + self.assertEqual(inspect.signature(some_name), + inspect.signature(decorated)) + def test_wrapped_function(self): """ The function wrapped by the wait decorator can be accessed via the - `wrapped_function` attribute. + `__wrapped__` attribute. """ decorator = self.decorator() + def func(): pass + wrapper = decorator(func) - self.assertIdentical(wrapper.wrapped_function, func) + self.assertIdentical(wrapper.__wrapped__, func) def test_reactor_thread_disallowed(self): """ @@ -813,6 +899,28 @@ func(1, 2, c=3) self.assertEqual(calls, [(1, 2, 3)]) + def test_classmethod(self): + """ + The function decorated with the wait decorator can be a classmethod. + """ + calls = [] + decorator = self.decorator() + + class C(object): + @decorator + @classmethod + def func(cls, a, b, c): + calls.append((a, b, c)) + + @classmethod + @decorator + def func2(cls, a, b, c): + calls.append((a, b, c)) + + C.func(1, 2, c=3) + C.func2(1, 2, c=3) + self.assertEqual(calls, [(1, 2, 3), (1, 2, 3)]) + def test_deferred_success_result(self): """ If the underlying function returns a Deferred, the wrapper returns a @@ -853,6 +961,8 @@ A call to a decorated function responds to a Ctrl-C (i.e. with a KeyboardInterrupt) in a timely manner. """ + if platform.type != "posix": + raise SkipTest("I don't have the energy to fight Windows semantics.") program = """\ import os, threading, signal, time, sys import crochet @@ -875,8 +985,7 @@ # Still running, test shall fail... os.kill(os.getpid(), sig_kill) -t = threading.Thread(target=interrupt) -t.setDaemon(True) +t = threading.Thread(target=interrupt, daemon=True) t.start() @crochet.%s @@ -887,8 +996,8 @@ wait() except KeyboardInterrupt: sys.exit(23) -""" % (self.DECORATOR_CALL,) - kw = { 'cwd': crochet_directory } +""" % (self.DECORATOR_CALL, ) + kw = {'cwd': crochet_directory} if platform.type.startswith('win'): kw['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP process = subprocess.Popen([sys.executable, "-c", program], **kw) @@ -917,36 +1026,17 @@ er = run() except crochet.ReactorStopped: sys.exit(23) -""" % (self.DECORATOR_CALL,) +""" % (self.DECORATOR_CALL, ) process = subprocess.Popen([sys.executable, "-c", program], cwd=crochet_directory) self.assertEqual(process.wait(), 23) - -class WaitForReactorTests(WaitTestsMixin, TestCase): - """ - Tests for the wait_for_reactor decorator. - """ - DECORATOR_CALL = "wait_for_reactor" - - def decorator(self): - return self.eventloop.wait_for_reactor - - -class WaitForTests(WaitTestsMixin, TestCase): - """ - Tests for the wait_for_reactor decorator. - """ - DECORATOR_CALL = "wait_for(timeout=5)" - - def decorator(self): - return lambda func: self.eventloop.wait_for(timeout=5)(func) - def test_timeoutRaises(self): """ If a function wrapped with wait_for hits the timeout, it raises TimeoutError. """ + @self.eventloop.wait_for(timeout=0.5) def times_out(): return Deferred().addErrback(lambda f: f.trap(CancelledError)) @@ -967,21 +1057,39 @@ @self.eventloop.wait_for(timeout=0.0) def times_out(): return result + self.assertRaises(TimeoutError, times_out) self.assertIsInstance(error[0].value, CancelledError) + def test_async_function(self): + """ + Async functions can be wrapped with @wait_for. + """ + @self.eventloop.wait_for(timeout=0.1) + async def go(): + self.assertTrue(self.reactor.in_call_from_thread) + return 17 + + self.assertEqual((go(), go()), (17, 17)) + self.assertFalse(inspect.iscoroutinefunction(go)) + class PublicAPITests(TestCase): """ Tests for the public API. """ + def test_no_sideeffects(self): """ Creating an EventLoop object, as is done in crochet.__init__, does not call any methods on the objects it is created with. """ - c = EventLoop(lambda: None, lambda f, g: 1/0, lambda *args: 1/0, - watchdog_thread=object(), reapAllProcesses=lambda: 1/0) + c = EventLoop( + lambda: None, + lambda f, g: 1 / 0, + lambda *args: 1 / 0, + watchdog_thread=object(), + reapAllProcesses=lambda: 1 / 0) del c def test_eventloop_api(self): @@ -994,18 +1102,17 @@ self.assertIsInstance(_main, EventLoop) self.assertEqual(_main.setup, setup) self.assertEqual(_main.no_setup, no_setup) - self.assertEqual(_main.in_reactor, in_reactor) self.assertEqual(_main.run_in_reactor, run_in_reactor) - self.assertEqual(_main.wait_for_reactor, wait_for_reactor) self.assertEqual(_main.wait_for, wait_for) self.assertIdentical(_main._atexit_register, _shutdown.register) - self.assertIdentical(_main._startLoggingWithObserver, - startLoggingWithObserver) + self.assertIdentical( + _main._startLoggingWithObserver, startLoggingWithObserver) self.assertIdentical(_main._watchdog_thread, _shutdown._watchdog) def test_eventloop_api_reactor(self): """ - The publicly exposed EventLoop will, when setup, use the global reactor. + The publicly exposed EventLoop will, when setup, use the global + reactor. """ from twisted.internet import reactor _main.no_setup() @@ -1025,6 +1132,7 @@ plaforms. """ self.assertIdentical(_main._reapAllProcesses, reapAllProcesses) + if platform.type != "posix": test_reapAllProcesses.skip = "Only relevant on POSIX platforms" if reapAllProcesses is None: diff -Nru crochet-1.4.0/crochet/tests/test_logging.py crochet-2.0.0/crochet/tests/test_logging.py --- crochet-1.4.0/crochet/tests/test_logging.py 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_logging.py 2017-08-09 20:33:23.000000000 +0000 @@ -0,0 +1,90 @@ +"""Tests for the logging bridge.""" + +from __future__ import absolute_import + +from twisted.trial.unittest import SynchronousTestCase +import threading + +from twisted.python import threadable + +from .._eventloop import ThreadLogObserver + + +class ThreadLogObserverTest(SynchronousTestCase): + """ + Tests for ThreadLogObserver. + + We use Twisted's SyncTestCase to ensure that unhandled logged errors get + reported as errors, in particular for test_error. + """ + + def test_stop(self): + """ + ThreadLogObserver.stop() stops the thread started in __init__. + """ + threadLog = ThreadLogObserver(None) + self.assertTrue(threadLog._thread.is_alive()) + threadLog.stop() + threadLog._thread.join() + self.assertFalse(threadLog._thread.is_alive()) + + def test_emit(self): + """ + ThreadLogObserver.emit runs the wrapped observer's in its thread, with + the given message. + """ + messages = [] + + def observer(msg): + messages.append((threading.current_thread().ident, msg)) + + threadLog = ThreadLogObserver(observer) + ident = threadLog._thread.ident + msg1 = {} + msg2 = {"a": "b"} + threadLog(msg1) + threadLog(msg2) + threadLog.stop() + # Wait for writing to finish: + threadLog._thread.join() + self.assertEqual(messages, [(ident, msg1), (ident, msg2)]) + + def test_errors(self): + """ + ThreadLogObserver.emit catches and silently drops exceptions from its + observer. + """ + messages = [] + counter = [] + + def observer(msg): + counter.append(1) + if len(counter) == 2: + raise RuntimeError("ono a bug") + messages.append(msg) + + threadLog = ThreadLogObserver(observer) + msg1 = {"m": "1"} + msg2 = {"m": "2"} + msg3 = {"m": "3"} + threadLog(msg1) + threadLog(msg2) + threadLog(msg3) + threadLog.stop() + # Wait for writing to finish: + threadLog._thread.join() + self.assertEqual(messages, [msg1, msg3]) + + def test_ioThreadUnchanged(self): + """ + ThreadLogObserver does not change the Twisted I/O thread (which is + supposed to match the thread the main reactor is running in.) + """ + threadLog = ThreadLogObserver(None) + threadLog.stop() + threadLog._thread.join() + self.assertIn( + threadable.ioThread, + # Either reactor was never run, or run in thread running + # the tests: + (None, threading.current_thread().ident)) diff -Nru crochet-1.4.0/crochet/tests/test_mypy.py crochet-2.0.0/crochet/tests/test_mypy.py --- crochet-1.4.0/crochet/tests/test_mypy.py 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_mypy.py 2021-05-10 20:34:24.000000000 +0000 @@ -0,0 +1,497 @@ +""" +Tests for crochet.mypy. +""" +from tempfile import NamedTemporaryFile +from textwrap import dedent, indent +from unittest import TestCase, skipUnless + +try: + import mypy.api + MYPY_AVAILABLE = True +except ImportError: + MYPY_AVAILABLE = False + +MYPY_CONFIG = dedent( + """\ + [mypy] + plugins = crochet.mypy + """ +) + + +@skipUnless(MYPY_AVAILABLE, "Tests require mypy to be installed.") +class MypyTests(TestCase): + def test_mypy_working(self) -> None: + """ + mypy's API is able to function and produce errors when expected. + """ + _assert_mypy(True, "ivar: int = 1\n") + _assert_mypy(False, "ivar: int = 'bad'\n") + + def test_setup_no_args(self) -> None: + """ + setup() and no_setup() take no arguments. + """ + _assert_mypy( + True, + dedent( + r""" + from crochet import setup + setup() + """ + ), + ) + _assert_mypy( + True, + dedent( + r""" + from crochet import no_setup + no_setup() + """ + ), + ) + + def test_run_in_reactor_func_takes_same_args(self) -> None: + """ + The mypy plugin correctly passes the wrapped parameter signature through the + @run_in_reactor decorator. + """ + template = dedent( + """\ + from crochet import run_in_reactor + + @run_in_reactor + def foo({params}) -> None: + pass + + foo({args}) + """ + ) + for params, args, good in ( + ( + "x: int, y: str, z: float, *a: int, **kw: str", + "1, 'something', -1, 4, 5, 6, k1='x', k2='y'", + True, + ), + ( + "", + "1", + False, + ), + ( + "x: int", + "", + False, + ), + ( + "x: int", + "1, 2", + False, + ), + ( + "x: int", + "'something'", + False, + ), + ( + "*x: int", + "1, 2, 3", + True, + ), + ( + "*x: int", + "'something'", + False, + ), + ( + "**x: int", + "k1=16, k2=-5", + True, + ), + ( + "**x: int", + "k1='something'", + False, + ), + ( + "x: int, y: str", + "1, 'ok'", + True, + ), + ( + "x: int, y: str", + "'not ok', 1", + False, + ), + ( + "x: str, y: int", + "'ok', 1", + True, + ), + ( + "x: str, y: int", + "1, 'not ok'", + False, + ), + ): + with self.subTest(params=params, args=args): + _assert_mypy(good, template.format(params=params, args=args)) + + def test_run_in_reactor_func_returns_typed_eventual(self) -> None: + """ + run_in_reactor preserves the decorated function's return type indirectly + through an EventualResult. + """ + template = dedent( + """\ + from typing import Optional + from crochet import EventualResult, run_in_reactor + + @run_in_reactor + def foo() -> {return_type}: + return {return_value} + + eventual_result: {receiver_type} = foo() + final_result: {final_type} = eventual_result.wait(1) + """ + ) + for return_type, return_value, receiver_type, final_type, good in ( + ( + "int", + "1", + "EventualResult[int]", + "int", + True, + ), + ( + "int", + "'str'", + "EventualResult[int]", + "int", + False, + ), + ( + "int", + "1", + "EventualResult[str]", + "int", + False, + ), + ( + "int", + "1", + "EventualResult[str]", + "str", + False, + ), + ( + "int", + "1", + "int", + "int", + False, + ), + ( + "int", + "1", + "EventualResult[int]", + "Optional[int]", + True, + ), + ( + "Optional[int]", + "1", + "EventualResult[Optional[int]]", + "Optional[int]", + True, + ), + ( + "Optional[int]", + "None", + "EventualResult[Optional[int]]", + "Optional[int]", + True, + ), + ( + "Optional[int]", + "1", + "EventualResult[int]", + "Optional[int]", + False, + ), + ( + "Optional[int]", + "1", + "EventualResult[Optional[int]]", + "int", + False, + ), + ): + with self.subTest( + return_type=return_type, + return_value=return_value, + receiver_type=receiver_type, + final_type=final_type, + ): + _assert_mypy( + good, + template.format( + return_type=return_type, + return_value=return_value, + receiver_type=receiver_type, + final_type=final_type, + ), + ) + + def test_run_in_reactor_func_signature_transform(self) -> None: + """ + The mypy plugin correctly passes the wrapped signature though the + @run_in_reactor decorator with an EventualResult-wrapped return type. + """ + template = dedent( + """\ + from typing import Callable + from crochet import EventualResult, run_in_reactor + + class Thing: + pass + + @run_in_reactor + def foo(x: int, y: str, z: float) -> Thing: + return Thing() + + re_foo: {result_type} = foo + """ + ) + for result_type, good in ( + ("Callable[[int, str, float], EventualResult[Thing]]", True), + ("Callable[[int, str, float], EventualResult[object]]", True), + ("Callable[[int, str, float], EventualResult[int]]", False), + ("Callable[[int, str, float], Thing]", False), + ("Callable[[int, str, float], int]", False), + ("Callable[[int, str], EventualResult[Thing]]", False), + ("Callable[[int], EventualResult[Thing]]", False), + ("Callable[[], EventualResult[Thing]]", False), + ("Callable[[float, int, str], EventualResult[Thing]]", False), + ): + with self.subTest(result_type=result_type): + _assert_mypy(good, template.format(result_type=result_type)) + + def test_eventual_result_cancel_signature(self) -> None: + """ + EventualResult's cancel() method takes no arguments. + """ + _assert_mypy( + True, + dedent( + """\ + from crochet import EventualResult + def foo(er: EventualResult[object]) -> None: + er.cancel() + """ + ), + ) + + def test_eventual_result_wait_signature(self) -> None: + """ + EventualResult's wait() method takes one timeout float argument. + """ + _assert_mypy( + True, + dedent( + """\ + from crochet import EventualResult + def foo(er: EventualResult[object]) -> object: + return er.wait(2.0) + """ + ), + ) + _assert_mypy( + True, + dedent( + """\ + from crochet import EventualResult + def foo(er: EventualResult[object]) -> object: + return er.wait(timeout=2.0) + """ + ), + ) + + def test_eventual_result_stash_signature(self) -> None: + """ + EventualResult's stash() method takes no arguments and returns the same type + retrieve_result's one result_id parameter takes. + """ + _assert_mypy( + True, + dedent( + """\ + from crochet import EventualResult, retrieve_result + def foo(er: EventualResult[object]) -> None: + retrieve_result(er.stash()) + retrieve_result(result_id=er.stash()) + """ + ), + ) + + def test_eventual_result_original_failure_signature(self) -> None: + """ + EventualResult's original_failure() method takes no arguments and returns an + optional Failure. + """ + _assert_mypy( + True, + dedent( + """\ + from typing import Optional + from twisted.python.failure import Failure + from crochet import EventualResult + def foo(er: EventualResult[object]) -> Optional[Failure]: + return er.original_failure() + """ + ), + ) + _assert_mypy( + False, + dedent( + """\ + from twisted.python.failure import Failure + from crochet import EventualResult + def foo(er: EventualResult[object]) -> Failure: + return er.original_failure() + """ + ), + ) + + def test_exceptions(self) -> None: + """ + ReactorStopped and TimeoutError are Exception types. + """ + _assert_mypy( + True, + dedent( + """\ + from crochet import ReactorStopped, TimeoutError + e1: Exception = ReactorStopped() + e2: Exception = TimeoutError() + """ + ), + ) + + def test_retrieve_result_returns_untyped_eventual_result(self) -> None: + """ + retrieve_result() returns an untyped EventualResult. + """ + _assert_mypy( + True, + dedent( + """\ + from crochet import EventualResult, retrieve_result + r: EventualResult[object] = retrieve_result(3) + """ + ), + ) + _assert_mypy( + False, + dedent( + """\ + from crochet import EventualResult, retrieve_result + r: EventualResult[int] = retrieve_result(3) + """ + ), + ) + + def test_wait_for_signature(self) -> None: + """ + The @wait_for decorator takes a timeout float. + """ + _assert_mypy( + True, + dedent( + """\ + from crochet import wait_for + + @wait_for(1.5) + def foo() -> None: + pass + """ + ), + ) + _assert_mypy( + True, + dedent( + """\ + from crochet import wait_for + + @wait_for(timeout=1.5) + def foo() -> None: + pass + """ + ), + ) + + def test_wait_for_func_signature_unchanged(self) -> None: + """ + The @wait_for(timeout) decorator preserves the wrapped function's signature. + """ + template = dedent( + """\ + from typing import Callable + from crochet import wait_for + + class Thing: + pass + + @wait_for(1) + def foo(x: int, y: str, z: float) -> Thing: + return Thing() + + re_foo: {result_type} = foo + """ + ) + for result_type, good in ( + ("Callable[[int, str, float], Thing]", True), + ("Callable[[int, str, float], object]", True), + ("Callable[[int, str, float], int]", False), + ("Callable[[int, str, float], EventualResult[Thing]]", False), + ("Callable[[int, str, float], None]", False), + ("Callable[[int, str], Thing]", False), + ("Callable[[int], Thing]", False), + ("Callable[[], Thing]", False), + ("Callable[[float, int, str], Thing]", False), + ): + with self.subTest(result_type=result_type): + _assert_mypy(good, template.format(result_type=result_type)) + + def test_version_string(self) -> None: + """ + __version__ is a string. + """ + _assert_mypy( + True, + dedent( + """\ + import crochet + x: str = crochet.__version__ + """ + ), + ) + + +def _assert_mypy(expect_success: bool, source_code: str) -> None: + with NamedTemporaryFile(mode="w+t", delete=False) as config_file: + config_file.write(MYPY_CONFIG) + + out, err, status = mypy.api.run( + ["--config-file", config_file.name, "-c", source_code] + ) + if status not in (0, 1): + raise RuntimeError( + f"Unexpected mypy error (status {status}):\n{indent(err, ' ' * 2)}" + ) + if expect_success: + assert ( + status == 0 + ), f"Unexpected mypy failure (status {status}):\n{indent(out, ' ' * 2)}" + else: + assert status == 1, f"Unexpected mypy success: stdout: {out}\nstderr: {err}\n" diff -Nru crochet-1.4.0/crochet/tests/test_process.py crochet-2.0.0/crochet/tests/test_process.py --- crochet-1.4.0/crochet/tests/test_process.py 2014-01-11 21:20:43.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_process.py 2017-08-09 20:33:23.000000000 +0000 @@ -10,10 +10,12 @@ from ..tests import crochet_directory + class ProcessTests(TestCase): """ Tests for process support. """ + def test_processExit(self): """ A Crochet-managed reactor notice when a process it started exits. @@ -61,7 +63,6 @@ stdout=subprocess.PIPE) result = process.stdout.read() self.assertEqual(result, b"abc") + if platform.type != "posix": test_processExit.skip = "SIGCHLD is a POSIX-specific issue" - if sys.version_info >= (3, 0, 0): - test_processExit.skip = "Twisted does not support processes on Python 3 yet" diff -Nru crochet-1.4.0/crochet/tests/test_setup.py crochet-2.0.0/crochet/tests/test_setup.py --- crochet-1.4.0/crochet/tests/test_setup.py 2014-05-31 19:55:32.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_setup.py 2021-05-10 20:34:24.000000000 +0000 @@ -8,12 +8,12 @@ import warnings import subprocess import sys +from unittest import SkipTest, TestCase -from twisted.trial.unittest import TestCase +import twisted from twisted.python.log import PythonLoggingObserver from twisted.python import log from twisted.python.runtime import platform -from twisted.python import threadable from twisted.internet.task import Clock from .._eventloop import EventLoop, ThreadLogObserver, _store @@ -72,7 +72,8 @@ EventLoop(lambda: reactor, lambda f, *g: None).setup() reactor.started.wait(5) self.assertNotEqual(reactor.thread_id, None) - self.assertNotEqual(reactor.thread_id, threading.current_thread().ident) + self.assertNotEqual( + reactor.thread_id, threading.current_thread().ident) self.assertFalse(reactor.installSignalHandlers) def test_second_does_nothing(self): @@ -93,19 +94,20 @@ """ atexit = [] reactor = FakeReactor() - s = EventLoop(lambda: reactor, lambda f, *args: atexit.append((f, args))) + s = EventLoop( + lambda: reactor, lambda f, *args: atexit.append((f, args))) s.setup() self.assertEqual(len(atexit), 2) self.assertFalse(reactor.stopping) f, args = atexit[0] self.assertEqual(f, reactor.callFromThread) - self.assertEqual(args, (reactor.stop,)) + self.assertEqual(args, (reactor.stop, )) f(*args) self.assertTrue(reactor.stopping) f, args = atexit[1] self.assertEqual(f, _store.log_errors) self.assertEqual(args, ()) - f(*args) # make sure it doesn't throw an exception + f(*args) # make sure it doesn't throw an exception def test_runs_with_lock(self): """ @@ -120,32 +122,36 @@ ThreadLogObserver, removing the default log observer. """ logging = [] + def fakeStartLoggingWithObserver(observer, setStdout=1): self.assertIsInstance(observer, ThreadLogObserver) wrapped = observer._observer expected = PythonLoggingObserver.emit # Python 3 and 2 differ in value of __func__: expected = getattr(expected, "__func__", expected) - self.assertIdentical(wrapped.__func__, expected) + self.assertIs(wrapped.__func__, expected) self.assertEqual(setStdout, False) self.assertTrue(reactor.in_call_from_thread) logging.append(observer) reactor = FakeReactor() - loop = EventLoop(lambda: reactor, lambda f, *g: None, - fakeStartLoggingWithObserver) + loop = EventLoop( + lambda: reactor, lambda f, *g: None, fakeStartLoggingWithObserver) loop.setup() self.assertTrue(logging) logging[0].stop() def test_stop_logging_on_exit(self): """ - setup() registers a reactor shutdown event that stops the logging thread. + setup() registers a reactor shutdown event that stops the logging + thread. """ observers = [] reactor = FakeReactor() - s = EventLoop(lambda: reactor, lambda f, *arg: None, - lambda observer, setStdout=1: observers.append(observer)) + s = EventLoop( + lambda: reactor, + lambda f, *arg: None, + lambda observer, setStdout=1: observers.append(observer)) s.setup() self.addCleanup(observers[0].stop) self.assertIn(("after", "shutdown", observers[0].stop), reactor.events) @@ -155,15 +161,17 @@ setup() ensure the warnings module's showwarning is unmodified, overriding the change made by normal Twisted logging setup. """ + def fakeStartLoggingWithObserver(observer, setStdout=1): warnings.showwarning = log.showwarning self.addCleanup(observer.stop) + original = warnings.showwarning reactor = FakeReactor() - loop = EventLoop(lambda: reactor, lambda f, *g: None, - fakeStartLoggingWithObserver) + loop = EventLoop( + lambda: reactor, lambda f, *g: None, fakeStartLoggingWithObserver) loop.setup() - self.assertIdentical(warnings.showwarning, original) + self.assertIs(warnings.showwarning, original) def test_start_watchdog_thread(self): """ @@ -171,8 +179,8 @@ """ thread = FakeThread() reactor = FakeReactor() - loop = EventLoop(lambda: reactor, lambda *args: None, - watchdog_thread=thread) + loop = EventLoop( + lambda: reactor, lambda *args: None, watchdog_thread=thread) loop.setup() self.assertTrue(thread.started) @@ -185,9 +193,11 @@ atexit = [] thread = FakeThread() reactor = FakeReactor() - loop = EventLoop(lambda: reactor, lambda f, *arg: atexit.append(f), - lambda observer, *a, **kw: observers.append(observer), - watchdog_thread=thread) + loop = EventLoop( + lambda: reactor, + lambda f, *arg: atexit.append(f), + lambda observer, *a, **kw: observers.append(observer), + watchdog_thread=thread) loop.no_setup() loop.setup() @@ -213,9 +223,8 @@ reactor = FakeReactor() s = EventLoop(lambda: reactor, lambda f, *g: None) s.setup() - self.assertEqual(reactor.events, - [("before", "shutdown", s._registry.stop)]) - + self.assertEqual( + reactor.events, [("before", "shutdown", s._registry.stop)]) def test_no_setup_registry_shutdown(self): """ @@ -225,23 +234,28 @@ reactor = FakeReactor() s = EventLoop(lambda: reactor, lambda f, *g: None) s.no_setup() - self.assertEqual(reactor.events, - [("before", "shutdown", s._registry.stop)]) + self.assertEqual( + reactor.events, [("before", "shutdown", s._registry.stop)]) class ProcessSetupTests(TestCase): """ setup() enables support for IReactorProcess on POSIX plaforms. """ + def test_posix(self): """ On POSIX systems, setup() installs a LoopingCall that runs t.i.process.reapAllProcesses() 10 times a second. """ + if platform.type != "posix": + raise SkipTest("SIGCHLD is a POSIX-specific issue") reactor = FakeReactor() reaps = [] - s = EventLoop(lambda: reactor, lambda f, *g: None, - reapAllProcesses=lambda: reaps.append(1)) + s = EventLoop( + lambda: reactor, + lambda f, *g: None, + reapAllProcesses=lambda: reaps.append(1)) s.setup() reactor.advance(0.1) self.assertEquals(reaps, [1]) @@ -249,71 +263,18 @@ self.assertEquals(reaps, [1, 1]) reactor.advance(0.1) self.assertEquals(reaps, [1, 1, 1]) - if platform.type != "posix": - test_posix.skip = "SIGCHLD is a POSIX-specific issue" def test_non_posix(self): """ - On POSIX systems, setup() does not install a LoopingCall. + On non-POSIX systems, setup() does not install a LoopingCall. """ + if platform.type == "posix": + raise SkipTest("This test is for non-POSIX systems.") reactor = FakeReactor() s = EventLoop(lambda: reactor, lambda f, *g: None) s.setup() self.assertFalse(reactor.getDelayedCalls()) - if platform.type == "posix": - test_non_posix.skip = "SIGCHLD is a POSIX-specific issue" - - -class ThreadLogObserverTest(TestCase): - """ - Tests for ThreadLogObserver. - """ - def test_stop(self): - """ - ThreadLogObserver.stop() stops the thread started in __init__. - """ - threadLog = ThreadLogObserver(None) - self.assertTrue(threadLog._thread.is_alive()) - threadLog.stop() - threadLog._thread.join() - self.assertFalse(threadLog._thread.is_alive()) - - def test_emit(self): - """ - ThreadLogObserver.emit runs the wrapped observer's in its thread, with - the given message. - """ - messages = [] - def observer(msg): - messages.append((threading.current_thread().ident, msg)) - - threadLog = ThreadLogObserver(observer) - ident = threadLog._thread.ident - msg1 = {} - msg2 = {"a": "b"} - threadLog(msg1) - threadLog(msg2) - threadLog.stop() - # Wait for writing to finish: - threadLog._thread.join() - self.assertEqual(messages, [(ident, msg1), (ident, msg2)]) - - - def test_ioThreadUnchanged(self): - """ - ThreadLogObserver does not change the Twisted I/O thread (which is - supposed to match the thread the main reactor is running in.) - """ - threadLog = ThreadLogObserver(None) - threadLog.stop() - threadLog._thread.join() - self.assertIn(threadable.ioThread, - # Either reactor was never run, or run in thread running - # the tests: - (None, threading.current_thread().ident)) - - class ReactorImportTests(TestCase): """ @@ -324,6 +285,7 @@ doesn't work if reactor is imported (https://twistedmatrix.com/trac/ticket/7105). """ + def test_crochet_import_no_reactor(self): """ Importing crochet should not import the reactor. @@ -338,3 +300,78 @@ process = subprocess.Popen([sys.executable, "-c", program], cwd=crochet_directory) self.assertEqual(process.wait(), 23) + + +LOGGING_PROGRAM = """\ +import sys +from logging import StreamHandler, Formatter, getLogger, DEBUG +handler = StreamHandler(sys.stdout) +handler.setFormatter(Formatter("%%(levelname)s %%(message)s")) +l = getLogger("twisted") +l.addHandler(handler) +l.setLevel(DEBUG) + +import crochet +crochet.setup() +from twisted.python import log +%s +log.msg("log-info") +log.msg("log-error", isError=True) +""" + + +class LoggingTests(TestCase): + """ + End-to-end tests for Twisted->stdlib logging bridge. + """ + maxDiff = None + + def test_old_logging(self): + """ + Messages from the old Twisted logging API are emitted to Python + standard library logging. + """ + if tuple(map(int, twisted.__version__.split("."))) >= (15, 2, 0): + raise SkipTest("This test is for Twisted < 15.2.") + + program = LOGGING_PROGRAM % ("", ) + output = subprocess.check_output([sys.executable, "-u", "-c", program], + cwd=crochet_directory) + self.assertTrue( + output.startswith( + """\ +INFO Log opened. +INFO log-info +ERROR log-error +""")) + + def test_new_logging(self): + """ + Messages from both new and old Twisted logging APIs are emitted to + Python standard library logging. + """ + if tuple(map(int, twisted.__version__.split("."))) < (15, 2, 0): + raise SkipTest("This test is for Twisted 15.2 and later.") + + program = LOGGING_PROGRAM % ( + """\ +from twisted.logger import Logger +l2 = Logger() +import time +time.sleep(1) # workaround, there is race condition... somewhere +l2.info("logger-info") +l2.critical("logger-critical") +l2.warn("logger-warning") +l2.debug("logger-debug") +""", ) + output = subprocess.check_output([sys.executable, "-u", "-c", program], + cwd=crochet_directory) + self.assertIn( + """\ +INFO logger-info +CRITICAL logger-critical +WARNING logger-warning +DEBUG logger-debug +INFO log-info +CRITICAL log-error +""", output.decode("utf-8").replace("\r\n", "\n")) diff -Nru crochet-1.4.0/crochet/tests/test_shutdown.py crochet-2.0.0/crochet/tests/test_shutdown.py --- crochet-1.4.0/crochet/tests/test_shutdown.py 2014-01-11 21:21:02.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_shutdown.py 2017-08-09 20:33:23.000000000 +0000 @@ -10,8 +10,8 @@ from twisted.trial.unittest import TestCase -from crochet._shutdown import (Watchdog, FunctionRegistry, _watchdog, register, - _registry) +from crochet._shutdown import ( + Watchdog, FunctionRegistry, _watchdog, register, _registry) from ..tests import crochet_directory @@ -19,6 +19,7 @@ """ Tests for shutdown registration. """ + def test_shutdown(self): """ A function registered with _shutdown.register() is called when the @@ -116,7 +117,7 @@ result = [] registry = FunctionRegistry() registry.register(lambda: result.append(2)) - registry.register(lambda: 1/0) + registry.register(lambda: 1 / 0) registry.register(lambda: result.append(1)) registry.run() self.assertEqual(result, [1, 2]) diff -Nru crochet-1.4.0/crochet/tests/test_util.py crochet-2.0.0/crochet/tests/test_util.py --- crochet-1.4.0/crochet/tests/test_util.py 2013-04-03 13:01:11.000000000 +0000 +++ crochet-2.0.0/crochet/tests/test_util.py 2017-08-09 20:33:23.000000000 +0000 @@ -11,8 +11,10 @@ class FakeLock(object): locked = False + def __enter__(self): self.locked = True + def __exit__(self, type, value, traceback): self.locked = False @@ -38,6 +40,7 @@ """ Tests for the synchronized decorator. """ + def test_return(self): """ A method wrapped with @synchronized is called with the lock acquired, diff -Nru crochet-1.4.0/crochet/_util.py crochet-2.0.0/crochet/_util.py --- crochet-1.4.0/crochet/_util.py 2013-04-03 13:01:11.000000000 +0000 +++ crochet-2.0.0/crochet/_util.py 2017-08-09 20:33:23.000000000 +0000 @@ -2,16 +2,20 @@ Utility functions and classes. """ -from functools import wraps +import wrapt + + +@wrapt.decorator +def _synced(method, self, args, kwargs): + """Underlying synchronized wrapper.""" + with self._lock: + return method(*args, **kwargs) def synchronized(method): """ Decorator that wraps a method with an acquire/release of self._lock. """ - @wraps(method) - def synced(self, *args, **kwargs): - with self._lock: - return method(self, *args, **kwargs) - synced.synchronized = True - return synced + result = _synced(method) + result.synchronized = True + return result diff -Nru crochet-1.4.0/crochet/_version.py crochet-2.0.0/crochet/_version.py --- crochet-1.4.0/crochet/_version.py 2015-05-07 00:57:20.000000000 +0000 +++ crochet-2.0.0/crochet/_version.py 2021-05-10 20:34:43.542455400 +0000 @@ -1,11 +1,21 @@ -# This file was generated by 'versioneer.py' (0.10) from +# This file was generated by 'versioneer.py' (0.16) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. -version_version = '1.4.0' -version_full = 'ecc17a072aa8804c73e4ab8af1b3528cdd206f61' -def get_versions(default={}, verbose=False): - return {'version': version_version, 'full': version_full} +import json +import sys +version_json = ''' +{ + "dirty": false, + "error": null, + "full-revisionid": "d03b0ea69de77e74000452bdaf499557f5cece01", + "version": "2.0.0" +} +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) diff -Nru crochet-1.4.0/crochet.egg-info/dependency_links.txt crochet-2.0.0/crochet.egg-info/dependency_links.txt --- crochet-1.4.0/crochet.egg-info/dependency_links.txt 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet.egg-info/dependency_links.txt 2021-05-10 20:34:43.000000000 +0000 @@ -0,0 +1 @@ + diff -Nru crochet-1.4.0/crochet.egg-info/PKG-INFO crochet-2.0.0/crochet.egg-info/PKG-INFO --- crochet-1.4.0/crochet.egg-info/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet.egg-info/PKG-INFO 2021-05-10 20:34:43.000000000 +0000 @@ -0,0 +1,328 @@ +Metadata-Version: 1.2 +Name: crochet +Version: 2.0.0 +Summary: Use Twisted anywhere! +Home-page: https://github.com/itamarst/crochet +Maintainer: Itamar Turner-Trauring +Maintainer-email: itamar@itamarst.org +License: MIT +Description: Crochet: Use Twisted anywhere! + ============================== + + Crochet is an MIT-licensed library that makes it easier to use Twisted from + regular blocking code. Some use cases include: + + * Easily use Twisted from a blocking framework like Django or Flask. + * Write a library that provides a blocking API, but uses Twisted for its + implementation. + * Port blocking code to Twisted more easily, by keeping a backwards + compatibility layer. + * Allow normal Twisted programs that use threads to interact with Twisted more + cleanly from their threaded parts. For example, this can be useful when using + Twisted as a `WSGI container`_. + + .. _WSGI container: https://twistedmatrix.com/documents/current/web/howto/web-in-60/wsgi.html + + Crochet is maintained by Itamar Turner-Trauring. + + **Note:** Crochet development is pretty slow these days because mostly it **Just Works**. PyPI shows about 30,000 downloads a month, so existing users seem happy: https://pypistats.org/packages/crochet + + You can install Crochet by running:: + + $ pip install crochet + + Downloads are available on `PyPI`_. + + Documentation can be found on `Read The Docs`_. + + Bugs and feature requests should be filed at the project `Github page`_. + + .. _Read the Docs: https://crochet.readthedocs.org/ + .. _Github page: https://github.com/itamarst/crochet/ + .. _PyPI: https://pypi.python.org/pypi/crochet + + + API and features + ================ + + Crochet supports Python 3.6, 3.7, 3.8, and 3.9 as well as PyPy3. + Python 2.7 and 3.5 support is available in older releases. + + Crochet provides the following basic APIs: + + * Allow blocking code to call into Twisted and block until results are available + or a timeout is hit, using the ``crochet.wait_for`` decorator. + * A lower-level API (``crochet.run_in_reactor``) allows blocking code to run + code "in the background" in the Twisted thread, with the ability to repeatedly + check if it's done. + + Crochet will do the following on your behalf in order to enable these APIs: + + * Transparently start Twisted's reactor in a thread it manages. + * Shut down the reactor automatically when the process' main thread finishes. + * Hook up Twisted's log system to the Python standard library ``logging`` + framework. Unlike Twisted's built-in ``logging`` bridge, this includes + support for blocking `Handler` instances. + + What's New + ========== + + 2.0.0 + ^^^^^ + + New features: + + * It's possible to decorate ``async/await`` Twisted functions with ``@wait_for`` and ``@run_in_reactor``, thanks to Árni Már Jónsson. + * Added type hints, thanks to Merlin Davis. + * Added formal support for Python 3.9. + + + Removed features: + + * Dropped the deprecated APIs ``@wait_for_reactor``, ``@in_reactor``, ``DeferredResult``, the ``wrapped_function`` attribute, and unlimited timeouts on ``EventualResult.wait()``. + * Dropped support for Python 2.7 and 3.5. + + 1.12.0 + ^^^^^^ + + Bug fixes: + + * Fix a timeout overflow bug in 32-bit machines. + + + 1.11.0 + ^^^^^^ + + New features: + + * Added support for Python 3.8 and PyPy 3. + + Backwards incompatibility: + + * Dropped support for Python 3.4, since latest Twisted doesn't support it. + + 1.10.0 + ^^^^^^ + + New features: + + * Added support for Python 3.7. Thanks to Jeremy Cline for the patch. + + 1.9.0 + ^^^^^ + + New features: + + * The underlying callable wrapped ``@run_in_reactor`` and ``@wait_for`` is now available via the more standard ``__wrapped__`` attribute. + + Backwards incompatibility (in tests): + + * This was actually introduced in 1.8.0: ``wrapped_function`` may not always be available on decorated callables. + You should use ``__wrapped__`` instead. + + Bug fixes: + + * Fixed regression in 1.8.0 where bound method couldn't be wrapped. + Thanks to 2mf for the bug report. + + 1.8.0 + ^^^^^ + + New features: + + * Signatures on decorated functions now match the original functions. + Thanks to Mikhail Terekhov for the original patch. + * Documentation improvements, including an API reference. + + Bug fixes: + + * Switched to EPoll reactor for logging thread. + Anecdotal evidence suggests this fixes some issues on AWS Lambda, but it's not clear why. + Thanks to Rolando Espinoza for the patch. + * It's now possible to call ``@run_in_reactor`` and ``@wait_for`` above a ``@classmethod``. + Thanks to vak for the bug report. + + 1.7.0 + ^^^^^ + + Bug fixes: + + * If the Python ``logging.Handler`` throws an exception Crochet no longer goes into a death spiral. + Thanks to Michael Schlenker for the bug report. + + Removed features: + + * Versions of Twisted < 16.0 are no longer supported (i.e. no longer tested in CI.) + + 1.6.0 + ^^^^^ + + New features: + + * Added support for Python 3.6. + + 1.5.0 + ^^^^^ + + New features: + + * Added support for Python 3.5. + + Removed features: + + * Python 2.6, Python 3.3, and versions of Twisted < 15.0 are no longer supported. + + 1.4.0 + ^^^^^ + + New features: + + * Added support for Python 3.4. + + Documentation: + + * Added a section on known issues and workarounds. + + Bug fixes: + + * Main thread detection (used to determine when Crochet should shutdown) is now less fragile. + This means Crochet now supports more environments, e.g. uWSGI. + Thanks to Ben Picolo for the patch. + + 1.3.0 + ^^^^^ + + Bug fixes: + + * It is now possible to call ``EventualResult.wait()`` (or functions + wrapped in ``wait_for``) at import time if another thread holds the + import lock. Thanks to Ken Struys for the patch. + + 1.2.0 + ^^^^^ + New features: + + * ``crochet.wait_for`` implements the timeout/cancellation pattern documented + in previous versions of Crochet. ``crochet.wait_for_reactor`` and + ``EventualResult.wait(timeout=None)`` are now deprecated, since lacking + timeouts they could potentially block forever. + * Functions wrapped with ``wait_for`` and ``run_in_reactor`` can now be accessed + via the ``wrapped_function`` attribute, to ease unit testing of the underlying + Twisted code. + + API changes: + + * It is no longer possible to call ``EventualResult.wait()`` (or functions + wrapped with ``wait_for``) at import time, since this can lead to deadlocks + or prevent other threads from importing. Thanks to Tom Prince for the bug + report. + + Bug fixes: + + * ``warnings`` are no longer erroneously turned into Twisted log messages. + * The reactor is now only imported when ``crochet.setup()`` or + ``crochet.no_setup()`` are called, allowing daemonization if only ``crochet`` + is imported (http://tm.tl/7105). Thanks to Daniel Nephin for the bug report. + + Documentation: + + * Improved motivation, added contact info and news to the documentation. + * Better example of using Crochet from a normal Twisted application. + + 1.1.0 + ^^^^^ + Bug fixes: + + * ``EventualResult.wait()`` can now be used safely from multiple threads, + thanks to Gavin Panella for reporting the bug. + * Fixed reentrancy deadlock in the logging code caused by + http://bugs.python.org/issue14976, thanks to Rod Morehead for reporting the + bug. + * Crochet now installs on Python 3.3 again, thanks to Ben Cordero. + * Crochet should now work on Windows, thanks to Konstantinos Koukopoulos. + * Crochet tests can now run without adding its absolute path to PYTHONPATH or + installing it first. + + Documentation: + + * ``EventualResult.original_failure`` is now documented. + + 1.0.0 + ^^^^^ + Documentation: + + * Added section on use cases and alternatives. Thanks to Tobias Oberstein for + the suggestion. + + Bug fixes: + + * Twisted does not have to be pre-installed to run ``setup.py``, thanks to + Paul Weaver for bug report and Chris Scutcher for patch. + * Importing Crochet does not have side-effects (installing reactor event) + any more. + * Blocking calls are interrupted earlier in the shutdown process, to reduce + scope for deadlocks. Thanks to rmorehead for bug report. + + 0.9.0 + ^^^^^ + New features: + + * Expanded and much improved documentation, including a new section with + design suggestions. + * New decorator ``@wait_for_reactor`` added, a simpler alternative to + ``@run_in_reactor``. + * Refactored ``@run_in_reactor``, making it a bit more responsive. + * Blocking operations which would otherwise never finish due to reactor having + stopped (``EventualResult.wait()`` or ``@wait_for_reactor`` decorated call) + will be interrupted with a ``ReactorStopped`` exception. Thanks to rmorehead + for the bug report. + + Bug fixes: + + * ``@run_in_reactor`` decorated functions (or rather, their generated wrapper) + are interrupted by Ctrl-C. + * On POSIX platforms, a workaround is installed to ensure processes started by + `reactor.spawnProcess` have their exit noticed. See `Twisted ticket 6378`_ + for more details about the underlying issue. + + .. _Twisted ticket 6378: http://tm.tl/6738 + + 0.8.1 + ^^^^^ + * ``EventualResult.wait()`` now raises error if called in the reactor thread, + thanks to David Buchmann. + * Unittests are now included in the release tarball. + * Allow Ctrl-C to interrupt ``EventualResult.wait(timeout=None)``. + + 0.7.0 + ^^^^^ + * Improved documentation. + + 0.6.0 + ^^^^^ + * Renamed ``DeferredResult`` to ``EventualResult``, to reduce confusion with + Twisted's ``Deferred`` class. The old name still works, but is deprecated. + * Deprecated ``@in_reactor``, replaced with ``@run_in_reactor`` which doesn't + change the arguments to the wrapped function. The deprecated API still works, + however. + * Unhandled exceptions in ``EventualResult`` objects are logged. + * Added more examples. + * ``setup.py sdist`` should work now. + + 0.5.0 + ^^^^^ + * Initial release. + +Keywords: twisted threading +Platform: UNKNOWN +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.6.0 diff -Nru crochet-1.4.0/crochet.egg-info/requires.txt crochet-2.0.0/crochet.egg-info/requires.txt --- crochet-1.4.0/crochet.egg-info/requires.txt 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet.egg-info/requires.txt 2021-05-10 20:34:43.000000000 +0000 @@ -0,0 +1,2 @@ +Twisted>=16.0 +wrapt diff -Nru crochet-1.4.0/crochet.egg-info/SOURCES.txt crochet-2.0.0/crochet.egg-info/SOURCES.txt --- crochet-1.4.0/crochet.egg-info/SOURCES.txt 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet.egg-info/SOURCES.txt 2021-05-10 20:34:43.000000000 +0000 @@ -0,0 +1,50 @@ +LICENSE +MANIFEST.in +README.rst +requirements-dev.txt +setup.cfg +setup.py +versioneer.py +crochet/__init__.py +crochet/__init__.pyi +crochet/_eventloop.py +crochet/_resultstore.py +crochet/_shutdown.py +crochet/_util.py +crochet/_version.py +crochet/mypy.py +crochet/py.typed +crochet.egg-info/PKG-INFO +crochet.egg-info/SOURCES.txt +crochet.egg-info/dependency_links.txt +crochet.egg-info/requires.txt +crochet.egg-info/top_level.txt +crochet/tests/__init__.py +crochet/tests/test_api.py +crochet/tests/test_logging.py +crochet/tests/test_mypy.py +crochet/tests/test_process.py +crochet/tests/test_resultstore.py +crochet/tests/test_setup.py +crochet/tests/test_shutdown.py +crochet/tests/test_util.py +docs/Makefile +docs/api-reference.rst +docs/api.rst +docs/async.rst +docs/conf.py +docs/index.rst +docs/introduction.rst +docs/make.bat +docs/news.rst +docs/type-checking.rst +docs/using.rst +docs/workarounds.rst +examples/async.py +examples/blockingdns.py +examples/downloader.py +examples/fromtwisted.py +examples/mxquery.py +examples/scheduling.py +examples/ssh.py +examples/testing.py \ No newline at end of file diff -Nru crochet-1.4.0/crochet.egg-info/top_level.txt crochet-2.0.0/crochet.egg-info/top_level.txt --- crochet-1.4.0/crochet.egg-info/top_level.txt 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/crochet.egg-info/top_level.txt 2021-05-10 20:34:43.000000000 +0000 @@ -0,0 +1 @@ +crochet diff -Nru crochet-1.4.0/debian/changelog crochet-2.0.0/debian/changelog --- crochet-1.4.0/debian/changelog 2020-03-27 14:56:50.000000000 +0000 +++ crochet-2.0.0/debian/changelog 2021-12-03 00:42:36.000000000 +0000 @@ -1,40 +1,20 @@ -crochet (1.4.0-0ubuntu4) focal; urgency=medium +crochet (2.0.0-2) unstable; urgency=medium - * Stop building the Python2 module. + * d/control: Update my email. + * d/control: Bump Standards-Version to 4.6.0. - -- Matthias Klose Fri, 27 Mar 2020 15:56:50 +0100 + -- Sergio de Almeida Cipriano Junior Thu, 02 Dec 2021 21:42:36 -0300 -crochet (1.4.0-0ubuntu3) focal; urgency=medium +crochet (2.0.0-1) unstable; urgency=medium - * No-change rebuild to generate dependencies on python2. + * New upstream version 2.0.0. + * d/patches: Add async example in patch 0001 to fix lintian + 'example-unusual-interpreter' by changing python shebang to python3. - -- Matthias Klose Tue, 17 Dec 2019 12:31:54 +0000 + -- Sergio de Almeida Cipriano Junior Thu, 19 Aug 2021 21:26:40 -0300 -crochet (1.4.0-0ubuntu2) xenial; urgency=medium +crochet (1.12.0-1) experimental; urgency=medium - * debian/control: Depends on python3-twisted instead of - python3-twisted-experimental. + * Initial release (Closes: #986715). - -- Andres Rodriguez Tue, 15 Dec 2015 11:43:47 -0500 - -crochet (1.4.0-0ubuntu1) xenial; urgency=medium - - * New upstream release - * Build with python3 - * debian/patches/ubuntu_fix_tests.patch: Updated. Add new fixes to tests. - * debian/watch: update. - - -- Andres Rodriguez Fri, 06 Nov 2015 16:55:21 +0000 - -crochet (1.0.0-0ubuntu2) trusty; urgency=low - - * debian/rules: Run unittests. - * debian/patches/ubuntu_fix_tests.patch: Fix tests. - - -- Andres Rodriguez Tue, 11 Feb 2014 11:25:56 -0500 - -crochet (1.0.0-0ubuntu1) trusty; urgency=low - - * Initial release - - -- Gavin Panella Thu, 30 Jan 2014 12:12:21 -0500 + -- Sergio de Almeida Cipriano Junior Sat, 10 Apr 2021 12:01:12 -0300 diff -Nru crochet-1.4.0/debian/compat crochet-2.0.0/debian/compat --- crochet-1.4.0/debian/compat 2020-03-27 14:56:50.000000000 +0000 +++ crochet-2.0.0/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -9 diff -Nru crochet-1.4.0/debian/control crochet-2.0.0/debian/control --- crochet-1.4.0/debian/control 2020-03-27 14:56:50.000000000 +0000 +++ crochet-2.0.0/debian/control 2021-12-03 00:42:36.000000000 +0000 @@ -1,34 +1,38 @@ Source: crochet Section: python Priority: optional -Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: MAAS Maintainers -Build-Depends: debhelper (>= 9), +Maintainer: Debian Python Team +Uploaders: Sergio de Almeida Cipriano Junior +Build-Depends: debhelper-compat (= 13), dh-python, - python3, python3-setuptools, - python3-twisted -Standards-Version: 3.9.4 -Homepage: https://pypi.python.org/pypi/crochet -X-Python-Version: >= 2.7 -X-Python3-Version: >= 3.3 + python3-all, + python3-twisted, + python3-wrapt, +Standards-Version: 4.6.0 +Homepage: https://github.com/itamarst/crochet +Vcs-Browser: https://salsa.debian.org/python-team/packages/crochet +Vcs-Git: https://salsa.debian.org/python-team/packages/crochet.git +Rules-Requires-Root: no Package: python3-crochet Architecture: all -Depends: ${misc:Depends}, - ${python3:Depends}, - python3-twisted -Description: Use Twisted Anywhere! (Python 3) - Crochet is an MIT-licensed library that makes it easier for blocking or - threaded applications like Flask or Django to use the Twisted - networking framework. Crochet provides the following features: +Depends: ${python3:Depends}, + ${misc:Depends} +Description: Library that makes it easier to use Twisted from regular blocking code + Crochet is a library that makes it easier to use Twisted from regular blocking + code. Some use cases include: . - * Runs Twisted's reactor in a thread it manages. - * The reactor shuts down automatically when the process' main thread - finishes. - * Hooks up Twisted's log system to the Python standard library logging - framework. Unlike Twisted's built-in logging bridge, this includes - support for blocking Handler instances. - * A blocking API to eventual results (i.e. Deferred instances). This - last feature can be used separately, so Crochet is also useful for - normal Twisted applications that use threads. + Easily use Twisted from a blocking framework like Django or Flask. + . + Write a library that provides a blocking API, but uses Twisted for + its implementation. + . + Port blocking code to Twisted more easily, by keeping a backwards + compatibility layer. + . + Allow normal Twisted programs that use threads to interact with + Twisted more cleanly from their threaded parts. For example, this + can be useful when using Twisted as a WSGI container. + . + This package installs the library for Python 3. diff -Nru crochet-1.4.0/debian/copyright crochet-2.0.0/debian/copyright --- crochet-1.4.0/debian/copyright 2014-01-30 17:40:35.000000000 +0000 +++ crochet-2.0.0/debian/copyright 2021-12-03 00:42:00.000000000 +0000 @@ -1,45 +1,46 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: python-crochet -Source: https://pypi.python.org/pypi/crochet +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: crochet +Upstream-Contact: Itamar Turner-Trauring +Source: https://pypi.org/project/crochet/ Files: * Copyright: 2013 Itamar Turner-Trauring 2013 Twisted Matrix Labs -License: MIT - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. +License: Expat + +Files: versioneer.py +Copyright: n/a +License: public-domain Files: debian/* -Copyright: 2014 Gavin Panella - 2014 Canonical Ltd. -License: GPL-2+ - This package is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. +Copyright: 2021 Sergio de Almeida Cipriano Junior +License: Expat + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: . - This package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. . - You should have received a copy of the GNU General Public License - along with this program. If not, see + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License: public-domain + The file tagged with this license contains the following paragraph: . - On Debian systems, the complete text of the GNU General - Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + To make Versioneer easier to embed, all its code is dedicated to the public + domain. The `_version.py` that it creates is also in the public domain. + Specifically, both are released under the Creative Commons "Public Domain + Dedication" license (CC0-1.0), as described in + https://creativecommons.org/publicdomain/zero/1.0/ . diff -Nru crochet-1.4.0/debian/docs crochet-2.0.0/debian/docs --- crochet-1.4.0/debian/docs 2014-01-30 17:29:03.000000000 +0000 +++ crochet-2.0.0/debian/docs 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -README.rst -requirements-dev.txt diff -Nru crochet-1.4.0/debian/patches/0001-Make-examples-use-python3-interpreter-always.patch crochet-2.0.0/debian/patches/0001-Make-examples-use-python3-interpreter-always.patch --- crochet-1.4.0/debian/patches/0001-Make-examples-use-python3-interpreter-always.patch 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/debian/patches/0001-Make-examples-use-python3-interpreter-always.patch 2021-12-03 00:42:00.000000000 +0000 @@ -0,0 +1,99 @@ +From: Sergio de Almeida Cipriano Junior +Date: Sat, 10 Apr 2021 15:49:09 -0300 +Subject: Make examples use python3 interpreter always +Forwarded: not-needed + +Fix lintian 'example-unusual-interpreter' by changing python +shebang to python3. + +--- + examples/async.py | 2 +- + examples/blockingdns.py | 2 +- + examples/downloader.py | 2 +- + examples/fromtwisted.py | 2 +- + examples/mxquery.py | 2 +- + examples/scheduling.py | 2 +- + examples/ssh.py | 2 +- + examples/testing.py | 2 +- + 8 files changed, 8 insertions(+), 8 deletions(-) + +diff --git a/examples/async.py b/examples/async.py +index 89f4aef..c28b4a0 100644 +--- a/examples/async.py ++++ b/examples/async.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + Async/await DNS lookup using Twisted's APIs. + """ +diff --git a/examples/blockingdns.py b/examples/blockingdns.py +index 5239215..24a9f4b 100644 +--- a/examples/blockingdns.py ++++ b/examples/blockingdns.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + Do a DNS lookup using Twisted's APIs. + """ +diff --git a/examples/downloader.py b/examples/downloader.py +index 923b1bf..e829ed5 100644 +--- a/examples/downloader.py ++++ b/examples/downloader.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + A flask web application that downloads a page in the background. + """ +diff --git a/examples/fromtwisted.py b/examples/fromtwisted.py +index 87efcb9..641720c 100644 +--- a/examples/fromtwisted.py ++++ b/examples/fromtwisted.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + An example of using Crochet from a normal Twisted application. + """ +diff --git a/examples/mxquery.py b/examples/mxquery.py +index a438680..72f187c 100644 +--- a/examples/mxquery.py ++++ b/examples/mxquery.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + A command-line application that uses Twisted to do an MX DNS query. + """ +diff --git a/examples/scheduling.py b/examples/scheduling.py +index 6606e29..35f2884 100644 +--- a/examples/scheduling.py ++++ b/examples/scheduling.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + An example of scheduling time-based events in the background. + +diff --git a/examples/ssh.py b/examples/ssh.py +index d71b582..a37427d 100644 +--- a/examples/ssh.py ++++ b/examples/ssh.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + A demonstration of Conch, allowing you to SSH into a running Python server and + inspect objects at a Python prompt. +diff --git a/examples/testing.py b/examples/testing.py +index 7db5696..b1dee0d 100644 +--- a/examples/testing.py ++++ b/examples/testing.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/python ++#!/usr/bin/python3 + """ + Demonstration of accessing wrapped functions for testing. + """ diff -Nru crochet-1.4.0/debian/patches/0002-The-raise-syntax-no-longer-accepts-comma-separated-a.patch crochet-2.0.0/debian/patches/0002-The-raise-syntax-no-longer-accepts-comma-separated-a.patch --- crochet-1.4.0/debian/patches/0002-The-raise-syntax-no-longer-accepts-comma-separated-a.patch 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/debian/patches/0002-The-raise-syntax-no-longer-accepts-comma-separated-a.patch 2021-12-03 00:42:00.000000000 +0000 @@ -0,0 +1,22 @@ +From: Sergio de Almeida Cipriano Junior +Date: Fri, 16 Apr 2021 23:36:09 -0300 +Subject: The raise syntax no longer accepts comma-separated arguments +Forwarded: not-needed + +--- + examples/fromtwisted.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/examples/fromtwisted.py b/examples/fromtwisted.py +index 641720c..f80dcef 100644 +--- a/examples/fromtwisted.py ++++ b/examples/fromtwisted.py +@@ -21,7 +21,7 @@ def application(environ, start_response): + try: + ip = gethostbyname('twistedmatrix.com') + return "%s has IP %s" % ('twistedmatrix.com', ip) +- except Exception, e: ++ except Exception as e: + return 'Error doing lookup: %s' % (e,) + + # A blocking API that will be called from the WSGI application, but actually diff -Nru crochet-1.4.0/debian/patches/series crochet-2.0.0/debian/patches/series --- crochet-1.4.0/debian/patches/series 2015-11-06 17:12:42.000000000 +0000 +++ crochet-2.0.0/debian/patches/series 2021-12-03 00:42:00.000000000 +0000 @@ -1 +1,2 @@ -ubuntu_fix_tests.patch +0001-Make-examples-use-python3-interpreter-always.patch +0002-The-raise-syntax-no-longer-accepts-comma-separated-a.patch diff -Nru crochet-1.4.0/debian/patches/ubuntu_fix_tests.patch crochet-2.0.0/debian/patches/ubuntu_fix_tests.patch --- crochet-1.4.0/debian/patches/ubuntu_fix_tests.patch 2015-11-06 17:34:08.000000000 +0000 +++ crochet-2.0.0/debian/patches/ubuntu_fix_tests.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,99 +0,0 @@ -Index: crochet-1.4.0/crochet/tests/test_resultstore.py -=================================================================== ---- crochet-1.4.0.orig/crochet/tests/test_resultstore.py -+++ crochet-1.4.0/crochet/tests/test_resultstore.py -@@ -5,8 +5,8 @@ Tests for _resultstore. - from twisted.trial.unittest import TestCase - from twisted.internet.defer import Deferred, fail, succeed - --from .._resultstore import ResultStore --from .._eventloop import EventualResult -+from crochet._resultstore import ResultStore -+from crochet._eventloop import EventualResult - - - class ResultStoreTests(TestCase): -Index: crochet-1.4.0/crochet/tests/test_util.py -=================================================================== ---- crochet-1.4.0.orig/crochet/tests/test_util.py -+++ crochet-1.4.0/crochet/tests/test_util.py -@@ -6,7 +6,7 @@ from __future__ import absolute_import - - from twisted.trial.unittest import TestCase - --from .._util import synchronized -+from crochet._util import synchronized - - - class FakeLock(object): -Index: crochet-1.4.0/crochet/tests/test_shutdown.py -=================================================================== ---- crochet-1.4.0.orig/crochet/tests/test_shutdown.py -+++ crochet-1.4.0/crochet/tests/test_shutdown.py -@@ -2,7 +2,7 @@ - Tests for _shutdown. - """ - --from __future__ import absolute_import -+from __future__ import absolute_import, division - - import sys - import subprocess -@@ -12,7 +12,7 @@ from twisted.trial.unittest import TestC - - from crochet._shutdown import (Watchdog, FunctionRegistry, _watchdog, register, - _registry) --from ..tests import crochet_directory -+from crochet.tests import crochet_directory - - - class ShutdownTests(TestCase): -Index: crochet-1.4.0/crochet/tests/test_api.py -=================================================================== ---- crochet-1.4.0.orig/crochet/tests/test_api.py -+++ crochet-1.4.0/crochet/tests/test_api.py -@@ -33,12 +33,12 @@ else: - # waitpid() is only necessary on POSIX: - reapAllProcesses = None - --from .._eventloop import (EventLoop, EventualResult, TimeoutError, -+from crochet._eventloop import (EventLoop, EventualResult, TimeoutError, - ResultRegistry, ReactorStopped) --from .test_setup import FakeReactor --from .. import (_main, setup, in_reactor, retrieve_result, _store, no_setup, -+from test_setup import FakeReactor -+from crochet import (_main, setup, in_reactor, retrieve_result, _store, no_setup, - run_in_reactor, wait_for_reactor, wait_for) --from ..tests import crochet_directory -+from crochet.tests import crochet_directory - - - class ResultRegistryTests(TestCase): -Index: crochet-1.4.0/crochet/tests/test_setup.py -=================================================================== ---- crochet-1.4.0.orig/crochet/tests/test_setup.py -+++ crochet-1.4.0/crochet/tests/test_setup.py -@@ -16,8 +16,8 @@ from twisted.python.runtime import platf - from twisted.python import threadable - from twisted.internet.task import Clock - --from .._eventloop import EventLoop, ThreadLogObserver, _store --from ..tests import crochet_directory -+from crochet._eventloop import EventLoop, ThreadLogObserver, _store -+from crochet.tests import crochet_directory - - - class FakeReactor(Clock): -Index: crochet-1.4.0/crochet/tests/test_process.py -=================================================================== ---- crochet-1.4.0.orig/crochet/tests/test_process.py -+++ crochet-1.4.0/crochet/tests/test_process.py -@@ -8,7 +8,7 @@ import sys - from twisted.trial.unittest import TestCase - from twisted.python.runtime import platform - --from ..tests import crochet_directory -+from crochet.tests import crochet_directory - - class ProcessTests(TestCase): - """ diff -Nru crochet-1.4.0/debian/python3-crochet.examples crochet-2.0.0/debian/python3-crochet.examples --- crochet-1.4.0/debian/python3-crochet.examples 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/debian/python3-crochet.examples 2021-12-03 00:42:00.000000000 +0000 @@ -0,0 +1 @@ +examples/* diff -Nru crochet-1.4.0/debian/rules crochet-2.0.0/debian/rules --- crochet-1.4.0/debian/rules 2020-03-27 14:56:24.000000000 +0000 +++ crochet-2.0.0/debian/rules 2021-12-03 00:42:00.000000000 +0000 @@ -1,12 +1,7 @@ #!/usr/bin/make -f -# -*- makefile -*- +export DH_VERBOSE = 1 -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 -export PYBUILD_NAME = crochet +export PYBUILD_NAME=crochet %: dh $@ --with python3 --buildsystem=pybuild - -override_dh_auto_test: - python3 -m unittest discover crochet/tests diff -Nru crochet-1.4.0/debian/salsa-ci.yml crochet-2.0.0/debian/salsa-ci.yml --- crochet-1.4.0/debian/salsa-ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/debian/salsa-ci.yml 2021-12-03 00:42:00.000000000 +0000 @@ -0,0 +1,4 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml diff -Nru crochet-1.4.0/debian/tests/control crochet-2.0.0/debian/tests/control --- crochet-1.4.0/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/debian/tests/control 2021-12-03 00:42:00.000000000 +0000 @@ -0,0 +1,3 @@ +Tests: run-examples +Depends: @, python3-twisted, python3-flask +Restrictions: needs-internet diff -Nru crochet-1.4.0/debian/tests/run-examples crochet-2.0.0/debian/tests/run-examples --- crochet-1.4.0/debian/tests/run-examples 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/debian/tests/run-examples 2021-12-03 00:42:00.000000000 +0000 @@ -0,0 +1,10 @@ +#!/bin/sh + +set -eu + +cp -r examples "$AUTOPKGTEST_TMP"/. +cd "$AUTOPKGTEST_TMP"/examples + +python3 blockingdns.py localhost +python3 testing.py +python3 mxquery.py debian.org diff -Nru crochet-1.4.0/debian/watch crochet-2.0.0/debian/watch --- crochet-1.4.0/debian/watch 2015-11-06 17:57:57.000000000 +0000 +++ crochet-2.0.0/debian/watch 2021-12-03 00:42:00.000000000 +0000 @@ -1,3 +1,3 @@ -version=3 +version=4 opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -http://pypi.debian.net/crochet/crochet-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) +https://pypi.debian.net/crochet/crochet-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff -Nru crochet-1.4.0/docs/api-reference.rst crochet-2.0.0/docs/api-reference.rst --- crochet-1.4.0/docs/api-reference.rst 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/docs/api-reference.rst 2017-08-09 20:32:39.000000000 +0000 @@ -0,0 +1,12 @@ +API Reference +============= + +.. autofunction:: crochet.setup() +.. autofunction:: crochet.no_setup() +.. autofunction:: crochet.run_in_reactor(function) +.. autofunction:: crochet.wait_for(timeout) +.. autoclass:: crochet.EventualResult + :members: +.. autofunction:: crochet.retrieve_result(result_id) +.. autoexception:: crochet.TimeoutError +.. autoexception:: crochet.ReactorStopped diff -Nru crochet-1.4.0/docs/api.rst crochet-2.0.0/docs/api.rst --- crochet-1.4.0/docs/api.rst 2015-05-07 00:54:56.000000000 +0000 +++ crochet-2.0.0/docs/api.rst 2021-05-10 20:34:24.000000000 +0000 @@ -1,5 +1,5 @@ -The API -------- +Using Crochet +------------- Using Crochet involves three parts: reactor setup, defining functions that call into Twisted's reactor, and using those functions. @@ -22,7 +22,7 @@ will do anything. -@wait_for: Blocking Calls into Twisted +@wait_for: Blocking calls into Twisted ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now that you've got the reactor running, the next stage is defining some @@ -47,12 +47,6 @@ any unused resources, close outgoing connections etc., but cancellation is not guaranteed and should not be relied on. -.. note :: - ``wait_for`` was added to Crochet in v1.2.0. Prior releases provided a - similar API called ``wait_for_reactor`` which did not provide - timeouts. This older API still exists but is deprecated since waiting - indefinitely is a bad idea. - To see what this means, let's return to the first example in the documentation: @@ -79,8 +73,12 @@ File "", line 2, in raiseException twisted.names.error.DNSNameError: ]> +You can, similarly, wrap an ``async`` function: + +.. literalinclude:: ../examples/async.py + -@run_in_reactor: Asynchronous Results +@run_in_reactor: Asynchronous results ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``wait_for`` is implemented using ``run_in_reactor``, a more sophisticated and @@ -95,8 +93,9 @@ * When the function is called, the code will not run in the calling thread, but rather in the reactor thread. * The return result from a decorated function is an ``EventualResult`` - instance, wrapping the result of the underlying code, with particular - support for ``Deferred`` instances. + instance, wrapping the result of the underlying code, with built-in + support for functions that return ``Deferred`` instances as well as + ``async`` functions. ``EventualResult`` has the following basic methods: @@ -135,7 +134,7 @@ .. _Failure: https://twistedmatrix.com/documents/current/api/twisted.python.failure.Failure.html -Using Crochet from Twisted Applications +Using Crochet from Twisted applications ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If your application is already planning on running the Twisted reactor itself @@ -158,18 +157,18 @@ reactor.run() -Unit Testing +Unit testing ^^^^^^^^^^^^ Both ``@wait_for`` and ``@run_in_reactor`` expose the underlying Twisted -function via a ``wrapped_function`` attribute. This allows unit testing of the +function via a ``__wrapped__`` attribute. This allows unit testing of the Twisted code without having to go through the Crochet layer. .. literalinclude:: ../examples/testing.py When run, this gives the following output:: - add() returns EventualResult: + add(1, 2) returns EventualResult: - add.wrapped_function() returns result of underlying function: + add.__wrapped__(1, 2) returns result of underlying function: 3 diff -Nru crochet-1.4.0/docs/async.rst crochet-2.0.0/docs/async.rst --- crochet-1.4.0/docs/async.rst 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/docs/async.rst 2017-08-08 19:57:34.000000000 +0000 @@ -0,0 +1,10 @@ +Differences from async/await +============================ + +Python 3.6 introduces a new mechanism, ``async``/``await``, that allows integrating asynchronous code in a seemingly blocking way. +This mechanism is however quite different than Crochet. + +``await`` gives the illusion of blocking, but can only be used in functions that are marked as ``async``. +As such, this is not true blocking integration: the asyncess percolates throughout your program and cannot be restricted to just a single function. + +In contrast, Crochet allows you to truly block on an asynchronous event: it's just another blocking function call, and can be used in any normal Python function. diff -Nru crochet-1.4.0/docs/conf.py crochet-2.0.0/docs/conf.py --- crochet-1.4.0/docs/conf.py 2015-04-25 17:26:04.000000000 +0000 +++ crochet-2.0.0/docs/conf.py 2017-08-09 20:32:39.000000000 +0000 @@ -23,7 +23,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -51,8 +51,8 @@ # Versioneer adds -dirty suffix to version if checkout is dirty, and # therefore ReadTheDocs somehow ends up with this in its version, so strip # it out. -if version.endswith("-dirty"): - version = version[:-len("-dirty")] +if version.endswith(".dirty"): + version = version[:-len(".dirty")] # The full version, including alpha/beta/rc tags. release = version diff -Nru crochet-1.4.0/docs/index.rst crochet-2.0.0/docs/index.rst --- crochet-1.4.0/docs/index.rst 2015-05-07 00:54:56.000000000 +0000 +++ crochet-2.0.0/docs/index.rst 2021-05-10 20:34:24.000000000 +0000 @@ -1,5 +1,9 @@ Use Twisted Anywhere! ===================== +.. raw:: html + +
Star +
Crochet is an MIT-licensed library that makes it easier for blocking and threaded applications like Flask or Django to use the Twisted networking @@ -16,6 +20,15 @@ $ python blockingdns.py twistedmatrix.com twistedmatrix.com -> 66.35.39.66 +You can also wrap ``async`` functions. +Here is the equivalent code to the previous example, but using an ``async/await`` function: + +.. code-block:: python + + @wait_for(timeout=5.0) + async def gethostbyname(name): + result = await client.lookupAddress(name) + return result[0][0].payload.dottedQuad() Table of Contents ^^^^^^^^^^^^^^^^^ @@ -26,5 +39,8 @@ introduction api using + type-checking workarounds + async + api-reference news diff -Nru crochet-1.4.0/docs/introduction.rst crochet-2.0.0/docs/introduction.rst --- crochet-1.4.0/docs/introduction.rst 2014-05-31 18:26:34.000000000 +0000 +++ crochet-2.0.0/docs/introduction.rst 2017-08-08 20:00:04.000000000 +0000 @@ -7,7 +7,7 @@ Examples ======== -Background Scheduling +Background scheduling ^^^^^^^^^^^^^^^^^^^^^ You can use Crochet to schedule events that will run in the background without slowing down the page rendering of your web applications: @@ -15,7 +15,7 @@ .. literalinclude:: ../examples/scheduling.py -SSH into your Server +SSH into your server ^^^^^^^^^^^^^^^^^^^^ You can SSH into your Python process and get a Python prompt, allowing you to poke around in the internals of your running program: @@ -23,14 +23,14 @@ .. literalinclude:: ../examples/ssh.py -DNS Query +DNS query ^^^^^^^^^ Twisted also has a fully featured DNS library: .. literalinclude:: ../examples/mxquery.py -Using Crochet in Normal Twisted Code +Using Crochet in normal Twisted code ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can use Crochet's APIs for calling into the reactor thread from normal Twisted applications: diff -Nru crochet-1.4.0/docs/news.rst crochet-2.0.0/docs/news.rst --- crochet-1.4.0/docs/news.rst 2015-05-07 00:54:56.000000000 +0000 +++ crochet-2.0.0/docs/news.rst 2021-05-10 20:34:24.000000000 +0000 @@ -1,6 +1,111 @@ What's New ========== +2.0.0 +^^^^^ + +New features: + +* It's possible to decorate ``async/await`` Twisted functions with ``@wait_for`` and ``@run_in_reactor``, thanks to Árni Már Jónsson. +* Added type hints, thanks to Merlin Davis. +* Added formal support for Python 3.9. + + +Removed features: + +* Dropped the deprecated APIs ``@wait_for_reactor``, ``@in_reactor``, ``DeferredResult``, the ``wrapped_function`` attribute, and unlimited timeouts on ``EventualResult.wait()``. +* Dropped support for Python 2.7 and 3.5. + +1.12.0 +^^^^^^ + +Bug fixes: + +* Fix a timeout overflow bug in 32-bit machines. + + +1.11.0 +^^^^^^ + +New features: + +* Added support for Python 3.8 and PyPy 3. + +Backwards incompatibility: + +* Dropped support for Python 3.4, since latest Twisted doesn't support it. + +1.10.0 +^^^^^^ + +New features: + +* Added support for Python 3.7. Thanks to Jeremy Cline for the patch. + +1.9.0 +^^^^^ + +New features: + +* The underlying callable wrapped ``@run_in_reactor`` and ``@wait_for`` is now available via the more standard ``__wrapped__`` attribute. + +Backwards incompatibility (in tests): + +* This was actually introduced in 1.8.0: ``wrapped_function`` may not always be available on decorated callables. + You should use ``__wrapped__`` instead. + +Bug fixes: + +* Fixed regression in 1.8.0 where bound method couldn't be wrapped. + Thanks to 2mf for the bug report. + +1.8.0 +^^^^^ + +New features: + +* Signatures on decorated functions now match the original functions. + Thanks to Mikhail Terekhov for the original patch. +* Documentation improvements, including an API reference. + +Bug fixes: + +* Switched to EPoll reactor for logging thread. + Anecdotal evidence suggests this fixes some issues on AWS Lambda, but it's not clear why. + Thanks to Rolando Espinoza for the patch. +* It's now possible to call ``@run_in_reactor`` and ``@wait_for`` above a ``@classmethod``. + Thanks to vak for the bug report. + +1.7.0 +^^^^^ + +Bug fixes: + +* If the Python ``logging.Handler`` throws an exception Crochet no longer goes into a death spiral. + Thanks to Michael Schlenker for the bug report. + +Removed features: + +* Versions of Twisted < 16.0 are no longer supported (i.e. no longer tested in CI.) + +1.6.0 +^^^^^ + +New features: + +* Added support for Python 3.6. + +1.5.0 +^^^^^ + +New features: + +* Added support for Python 3.5. + +Removed features: + +* Python 2.6, Python 3.3, and versions of Twisted < 15.0 are no longer supported. + 1.4.0 ^^^^^ diff -Nru crochet-1.4.0/docs/type-checking.rst crochet-2.0.0/docs/type-checking.rst --- crochet-1.4.0/docs/type-checking.rst 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/docs/type-checking.rst 2021-05-10 20:34:24.000000000 +0000 @@ -0,0 +1,56 @@ +Static Type Checking +-------------------- + +Crochet comes with type hints for Python 3.6+. However, due to current +limitations in ``Callable`` generic construction (see +`PEP 612 — Parameter Specification Variables`_), the arguments of a call to +a ``@run_in_reactor``-decorated function or method cannot be checked without +giving type checkers some special help. Crochet ships with a plugin which +fills this role when the ``mypy`` static type checker is used. It resides in +``crochet.mypy`` and must be configured as described in +`Configuring mypy to use plugins`_. For example, in a ``mypy.ini`` +configuration file:: + + [mypy] + plugins = crochet.mypy + +This type checking is intended primarily for code which calls the decorated +function. As Twisted isn't fully type-hinted yet, and in particular Deferred +does not yet have a generic type argument so that the eventual result type can +vary, the analysis of the return type of a ``@run_in_reactor`` function/method +does not account for a Deferred result. This requires you to lie to the type +checker when returning a Deferred; just cast it to the known, eventual result +type using ``typing.cast``. For example:: + + @run_in_reactor + def get_time_in_x_seconds(delay: float) -> float: + def get_time() -> float: + return reactor.seconds() # type: ignore + + if delay < 0.001: + # Close enough; just return the current time. + return get_time() + else: + d = Deferred() + + def complete(): + d.callback(get_time()) + + reactor.callLater(delay, complete) # type: ignore + return typing.cast(float, d) + +If the mypy plugin is correctly installed, the client code will expect a float +from the ``wait()`` of the ``EventualResult`` returned by a call to this +function:: + + # OK + t1: float = get_time_in_x_seconds(2).wait(3) + print(f"The reactor time is {t1}") + + # mypy error: Incompatible types in assignment + # (expression has type "float", variable has type "str") + t2: str = get_time_in_x_seconds(2).wait(3) + print(f"The reactor time is {t2}") + +.. _PEP 612 — Parameter Specification Variables: https://www.python.org/dev/peps/pep-0612/ +.. _Configuring mypy to use plugins: https://mypy.readthedocs.io/en/latest/extending_mypy.html#configuring-mypy-to-use-plugins diff -Nru crochet-1.4.0/docs/using.rst crochet-2.0.0/docs/using.rst --- crochet-1.4.0/docs/using.rst 2015-05-07 00:54:56.000000000 +0000 +++ crochet-2.0.0/docs/using.rst 2017-08-08 20:00:44.000000000 +0000 @@ -38,7 +38,7 @@ .. literalinclude:: ../examples/scheduling.py -Minimize Decorated Code +Minimize decorated code ^^^^^^^^^^^^^^^^^^^^^^^ It's best to have as little code as possible in the diff -Nru crochet-1.4.0/docs/workarounds.rst crochet-2.0.0/docs/workarounds.rst --- crochet-1.4.0/docs/workarounds.rst 2015-05-07 00:54:56.000000000 +0000 +++ crochet-2.0.0/docs/workarounds.rst 2017-08-08 20:01:40.000000000 +0000 @@ -1,7 +1,13 @@ Known Issues and Workarounds ---------------------------- -Preventing Deadlocks on Shutdown +Don't Call Twisted APIs from non-Twisted threads +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As is the case in any Twisted program, you should never call Twisted APIs (e.g. ``reactor.callLater``) from non-Twisted threads. +Only call Twisted APIs from functions decorated by ``@wait_for`` and friends. + +Preventing deadlocks on shutdown ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To ensure a timely process exit, during reactor shutdown Crochet will try to @@ -17,7 +23,7 @@ firing or canceling any ``Deferred`` instances you are waiting on as part of your application shutdown, and do so before you stop any thread pools. -Reducing Twisted Log Messages +Reducing Twisted log messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Twisted can be rather verbose with its log messages. If you wish to reduce the @@ -29,7 +35,7 @@ logging.getLogger('twisted').setLevel(logging.ERROR) -Missing Tracebacks +Missing tracebacks ^^^^^^^^^^^^^^^^^^ In order to prevent massive memory leaks, Twisted currently wipes out the traceback from exceptions it captures (see https://tm.tl/7873 for ideas on improving this). diff -Nru crochet-1.4.0/examples/async.py crochet-2.0.0/examples/async.py --- crochet-1.4.0/examples/async.py 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/examples/async.py 2021-05-10 20:34:24.000000000 +0000 @@ -0,0 +1,34 @@ +#!/usr/bin/python +""" +Async/await DNS lookup using Twisted's APIs. +""" +from __future__ import print_function + +# The Twisted code we'll be using: +from twisted.names import client + +from crochet import setup, wait_for +setup() + + +# Crochet layer, wrapping Twisted's DNS library in a blocking call. +# Uses async/await. +@wait_for(timeout=5.0) +async def gethostbyname(name): + """Lookup the IP of a given hostname. + + Unlike socket.gethostbyname() which can take an arbitrary amount of time + to finish, this function will raise crochet.TimeoutError if more than 5 + seconds elapse without an answer being received. + """ + result = await client.lookupAddress(name) + return result[0][0].payload.dottedQuad() + + +if __name__ == '__main__': + # Application code using the public API - notice it works in a normal + # blocking manner, with no event loop visible: + import sys + name = sys.argv[1] + ip = gethostbyname(name) + print(name, "->", ip) diff -Nru crochet-1.4.0/examples/testing.py crochet-2.0.0/examples/testing.py --- crochet-1.4.0/examples/testing.py 2014-05-31 18:26:34.000000000 +0000 +++ crochet-2.0.0/examples/testing.py 2017-08-17 18:54:20.000000000 +0000 @@ -14,7 +14,7 @@ if __name__ == '__main__': - print("add() returns EventualResult:") + print("add(1, 2) returns EventualResult:") print(" ", add(1, 2)) - print("add.wrapped_function() returns result of underlying function:") - print(" ", add.wrapped_function(1, 2)) + print("add.__wrapped__(1, 2) is the result of the underlying function:") + print(" ", add.__wrapped__(1, 2)) diff -Nru crochet-1.4.0/MANIFEST.in crochet-2.0.0/MANIFEST.in --- crochet-1.4.0/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/MANIFEST.in 2014-05-31 19:55:36.000000000 +0000 @@ -0,0 +1,8 @@ +include LICENSE +include README.rst +include requirements-dev.txt +recursive-include docs * +prune docs/_build +recursive-include examples * +include versioneer.py +include crochet/_version.py diff -Nru crochet-1.4.0/PKG-INFO crochet-2.0.0/PKG-INFO --- crochet-1.4.0/PKG-INFO 2015-05-07 00:57:20.000000000 +0000 +++ crochet-2.0.0/PKG-INFO 2021-05-10 20:34:43.542455400 +0000 @@ -1,12 +1,12 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: crochet -Version: 1.4.0 +Version: 2.0.0 Summary: Use Twisted anywhere! Home-page: https://github.com/itamarst/crochet -Author: Itamar Turner-Trauring -Author-email: itamar@itamarst.org +Maintainer: Itamar Turner-Trauring +Maintainer-email: itamar@itamarst.org License: MIT -Description: Crochet: Use Twisted Anywhere! +Description: Crochet: Use Twisted anywhere! ============================== Crochet is an MIT-licensed library that makes it easier to use Twisted from @@ -18,13 +18,19 @@ * Port blocking code to Twisted more easily, by keeping a backwards compatibility layer. * Allow normal Twisted programs that use threads to interact with Twisted more - cleanly from their threaded parts. For example this can be useful when using + cleanly from their threaded parts. For example, this can be useful when using Twisted as a `WSGI container`_. .. _WSGI container: https://twistedmatrix.com/documents/current/web/howto/web-in-60/wsgi.html Crochet is maintained by Itamar Turner-Trauring. + **Note:** Crochet development is pretty slow these days because mostly it **Just Works**. PyPI shows about 30,000 downloads a month, so existing users seem happy: https://pypistats.org/packages/crochet + + You can install Crochet by running:: + + $ pip install crochet + Downloads are available on `PyPI`_. Documentation can be found on `Read The Docs`_. @@ -36,34 +42,136 @@ .. _PyPI: https://pypi.python.org/pypi/crochet - Features - ======== - - Crochet aims for 100% unit test coverage, and supports Python 2.6, 2.7, 3.3 and 3.4 as well as PyPy. + API and features + ================ - .. image:: https://travis-ci.org/itamarst/crochet.png?branch=master - :target: http://travis-ci.org/itamarst/crochet - :alt: Build Status + Crochet supports Python 3.6, 3.7, 3.8, and 3.9 as well as PyPy3. + Python 2.7 and 3.5 support is available in older releases. - Crochet provides the following general features: + Crochet provides the following basic APIs: * Allow blocking code to call into Twisted and block until results are available or a timeout is hit, using the ``crochet.wait_for`` decorator. * A lower-level API (``crochet.run_in_reactor``) allows blocking code to run - code "in the background" in the Twisted thread, with ability to repeatedly + code "in the background" in the Twisted thread, with the ability to repeatedly check if it's done. - Additionally Crochet can: + Crochet will do the following on your behalf in order to enable these APIs: * Transparently start Twisted's reactor in a thread it manages. - * The reactor shuts down automatically when the process' main thread finishes. - * Hooks up Twisted's log system to the Python standard library ``logging`` + * Shut down the reactor automatically when the process' main thread finishes. + * Hook up Twisted's log system to the Python standard library ``logging`` framework. Unlike Twisted's built-in ``logging`` bridge, this includes support for blocking `Handler` instances. What's New ========== + 2.0.0 + ^^^^^ + + New features: + + * It's possible to decorate ``async/await`` Twisted functions with ``@wait_for`` and ``@run_in_reactor``, thanks to Árni Már Jónsson. + * Added type hints, thanks to Merlin Davis. + * Added formal support for Python 3.9. + + + Removed features: + + * Dropped the deprecated APIs ``@wait_for_reactor``, ``@in_reactor``, ``DeferredResult``, the ``wrapped_function`` attribute, and unlimited timeouts on ``EventualResult.wait()``. + * Dropped support for Python 2.7 and 3.5. + + 1.12.0 + ^^^^^^ + + Bug fixes: + + * Fix a timeout overflow bug in 32-bit machines. + + + 1.11.0 + ^^^^^^ + + New features: + + * Added support for Python 3.8 and PyPy 3. + + Backwards incompatibility: + + * Dropped support for Python 3.4, since latest Twisted doesn't support it. + + 1.10.0 + ^^^^^^ + + New features: + + * Added support for Python 3.7. Thanks to Jeremy Cline for the patch. + + 1.9.0 + ^^^^^ + + New features: + + * The underlying callable wrapped ``@run_in_reactor`` and ``@wait_for`` is now available via the more standard ``__wrapped__`` attribute. + + Backwards incompatibility (in tests): + + * This was actually introduced in 1.8.0: ``wrapped_function`` may not always be available on decorated callables. + You should use ``__wrapped__`` instead. + + Bug fixes: + + * Fixed regression in 1.8.0 where bound method couldn't be wrapped. + Thanks to 2mf for the bug report. + + 1.8.0 + ^^^^^ + + New features: + + * Signatures on decorated functions now match the original functions. + Thanks to Mikhail Terekhov for the original patch. + * Documentation improvements, including an API reference. + + Bug fixes: + + * Switched to EPoll reactor for logging thread. + Anecdotal evidence suggests this fixes some issues on AWS Lambda, but it's not clear why. + Thanks to Rolando Espinoza for the patch. + * It's now possible to call ``@run_in_reactor`` and ``@wait_for`` above a ``@classmethod``. + Thanks to vak for the bug report. + + 1.7.0 + ^^^^^ + + Bug fixes: + + * If the Python ``logging.Handler`` throws an exception Crochet no longer goes into a death spiral. + Thanks to Michael Schlenker for the bug report. + + Removed features: + + * Versions of Twisted < 16.0 are no longer supported (i.e. no longer tested in CI.) + + 1.6.0 + ^^^^^ + + New features: + + * Added support for Python 3.6. + + 1.5.0 + ^^^^^ + + New features: + + * Added support for Python 3.5. + + Removed features: + + * Python 2.6, Python 3.3, and versions of Twisted < 15.0 are no longer supported. + 1.4.0 ^^^^^ @@ -211,9 +319,10 @@ Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.6.0 diff -Nru crochet-1.4.0/README.rst crochet-2.0.0/README.rst --- crochet-1.4.0/README.rst 2015-04-25 17:26:04.000000000 +0000 +++ crochet-2.0.0/README.rst 2021-05-10 20:34:24.000000000 +0000 @@ -1,4 +1,4 @@ -Crochet: Use Twisted Anywhere! +Crochet: Use Twisted anywhere! ============================== Crochet is an MIT-licensed library that makes it easier to use Twisted from @@ -10,13 +10,19 @@ * Port blocking code to Twisted more easily, by keeping a backwards compatibility layer. * Allow normal Twisted programs that use threads to interact with Twisted more - cleanly from their threaded parts. For example this can be useful when using + cleanly from their threaded parts. For example, this can be useful when using Twisted as a `WSGI container`_. .. _WSGI container: https://twistedmatrix.com/documents/current/web/howto/web-in-60/wsgi.html Crochet is maintained by Itamar Turner-Trauring. + **Note:** Crochet development is pretty slow these days because mostly it **Just Works**. PyPI shows about 30,000 downloads a month, so existing users seem happy: https://pypistats.org/packages/crochet + +You can install Crochet by running:: + + $ pip install crochet + Downloads are available on `PyPI`_. Documentation can be found on `Read The Docs`_. @@ -28,27 +34,24 @@ .. _PyPI: https://pypi.python.org/pypi/crochet -Features -======== - -Crochet aims for 100% unit test coverage, and supports Python 2.6, 2.7, 3.3 and 3.4 as well as PyPy. +API and features +================ -.. image:: https://travis-ci.org/itamarst/crochet.png?branch=master - :target: http://travis-ci.org/itamarst/crochet - :alt: Build Status +Crochet supports Python 3.6, 3.7, 3.8, and 3.9 as well as PyPy3. +Python 2.7 and 3.5 support is available in older releases. -Crochet provides the following general features: +Crochet provides the following basic APIs: * Allow blocking code to call into Twisted and block until results are available or a timeout is hit, using the ``crochet.wait_for`` decorator. * A lower-level API (``crochet.run_in_reactor``) allows blocking code to run - code "in the background" in the Twisted thread, with ability to repeatedly + code "in the background" in the Twisted thread, with the ability to repeatedly check if it's done. -Additionally Crochet can: +Crochet will do the following on your behalf in order to enable these APIs: * Transparently start Twisted's reactor in a thread it manages. -* The reactor shuts down automatically when the process' main thread finishes. -* Hooks up Twisted's log system to the Python standard library ``logging`` +* Shut down the reactor automatically when the process' main thread finishes. +* Hook up Twisted's log system to the Python standard library ``logging`` framework. Unlike Twisted's built-in ``logging`` bridge, this includes support for blocking `Handler` instances. diff -Nru crochet-1.4.0/requirements-dev.txt crochet-2.0.0/requirements-dev.txt --- crochet-1.4.0/requirements-dev.txt 2013-09-22 17:15:56.000000000 +0000 +++ crochet-2.0.0/requirements-dev.txt 2021-05-10 20:34:24.000000000 +0000 @@ -1,2 +1,6 @@ -Twisted>=11.1.0 sphinx +tox +tox-gh-actions +mypy +flake8 +pylint diff -Nru crochet-1.4.0/setup.cfg crochet-2.0.0/setup.cfg --- crochet-1.4.0/setup.cfg 1970-01-01 00:00:00.000000000 +0000 +++ crochet-2.0.0/setup.cfg 2021-05-10 20:34:43.542455400 +0000 @@ -0,0 +1,12 @@ +[versioneer] +vcs = git +style = pep440 +versionfile_source = crochet/_version.py +versionfile_build = crochet/_version.py +tag_prefix = +parentdir_prefix = crochet- + +[egg_info] +tag_build = +tag_date = 0 + diff -Nru crochet-1.4.0/setup.py crochet-2.0.0/setup.py --- crochet-1.4.0/setup.py 2015-04-25 17:26:04.000000000 +0000 +++ crochet-2.0.0/setup.py 2021-05-10 20:34:24.000000000 +0000 @@ -1,34 +1,29 @@ -import os - try: from setuptools import setup except ImportError: from distutils.core import setup import versioneer -versioneer.versionfile_source = 'crochet/_version.py' -versioneer.versionfile_build = 'crochet/_version.py' -versioneer.tag_prefix = '' # tags are like 1.2.0 -versioneer.parentdir_prefix = 'crochet-' def read(path): """ Read the contents of a file. """ - with open(path) as f: + with open(path, encoding="utf-8") as f: return f.read() + setup( classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], @@ -36,11 +31,14 @@ version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), description="Use Twisted anywhere!", + python_requires=">=3.6.0", install_requires=[ - "Twisted>=11.1", + "Twisted>=16.0", + "wrapt", ], keywords="twisted threading", license="MIT", + package_data={"crochet": ["py.typed", "*.pyi"]}, packages=["crochet", "crochet.tests"], url="https://github.com/itamarst/crochet", maintainer='Itamar Turner-Trauring', diff -Nru crochet-1.4.0/versioneer.py crochet-2.0.0/versioneer.py --- crochet-1.4.0/versioneer.py 2014-05-31 19:55:36.000000000 +0000 +++ crochet-2.0.0/versioneer.py 2016-04-13 20:04:38.000000000 +0000 @@ -1,7 +1,8 @@ -# Version: 0.10 +# Version: 0.16 + +"""The Versioneer - like a rocketeer, but for versions. -""" The Versioneer ============== @@ -9,9 +10,13 @@ * https://github.com/warner/python-versioneer * Brian Warner * License: Public Domain -* Compatible With: python2.6, 2.7, and 3.2, 3.3 - -[![Build Status](https://travis-ci.org/warner/python-versioneer.png?branch=master)](https://travis-ci.org/warner/python-versioneer) +* Compatible With: python2.6, 2.7, 3.3, 3.4, 3.5, and pypy +* [![Latest Version] +(https://pypip.in/version/versioneer/badge.svg?style=flat) +](https://pypi.python.org/pypi/versioneer/) +* [![Build Status] +(https://travis-ci.org/warner/python-versioneer.png?branch=master) +](https://travis-ci.org/warner/python-versioneer) This is a tool for managing a recorded version number in distutils-based python projects. The goal is to remove the tedious and error-prone "update @@ -23,8 +28,8 @@ ## Quick Install * `pip install versioneer` to somewhere to your $PATH -* run `versioneer-installer` in your source tree: this installs `versioneer.py` -* follow the instructions below (also in the `versioneer.py` docstring) +* add a `[versioneer]` section to your setup.cfg (see below) +* run `versioneer install` in your source tree, commit the results ## Version Identifiers @@ -42,7 +47,7 @@ * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows about recent "tags" and an absolute revision-id * the name of the directory into which the tarball was unpacked -* an expanded VCS variable ($Id$, etc) +* an expanded VCS keyword ($Id$, etc) * a `_version.py` created by some earlier build step For released software, the version identifier is closely related to a VCS @@ -53,7 +58,7 @@ enough information to help developers recreate the same tree, while also giving them an idea of roughly how old the tree is (after version 1.2, before version 1.3). Many VCS systems can report a description that captures this, -for example 'git describe --tags --dirty --always' reports things like +for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has uncommitted changes. @@ -67,33 +72,50 @@ Versioneer works by adding a special `_version.py` file into your source tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. However, -when you use "setup.py build" or "setup.py sdist", `_version.py` in the new -copy is replaced by a small static file that contains just the generated -version data. +dynamically ask the VCS tool for version information at import time. `_version.py` also contains `$Revision$` markers, and the installation process marks `_version.py` to have this marker rewritten with a tag name -during the "git archive" command. As a result, generated tarballs will +during the `git archive` command. As a result, generated tarballs will contain enough information to get the proper version. +To allow `setup.py` to compute a version too, a `versioneer.py` is added to +the top level of your source tree, next to `setup.py` and the `setup.cfg` +that configures it. This overrides several distutils/setuptools commands to +compute the version when invoked, and changes `setup.py build` and `setup.py +sdist` to replace `_version.py` with a small static file that contains just +the generated version data. ## Installation First, decide on values for the following configuration variables: +* `VCS`: the version control system you use. Currently accepts "git". + +* `style`: the style of version string to be produced. See "Styles" below for + details. Defaults to "pep440", which looks like + `TAG[+DISTANCE.gSHORTHASH[.dirty]]`. + * `versionfile_source`: A project-relative pathname into which the generated version strings should be written. This is usually a `_version.py` next to your project's main - `__init__.py` file. If your project uses `src/myproject/__init__.py`, this - should be `src/myproject/_version.py`. This file should be checked in to - your VCS as usual: the copy created below by `setup.py versioneer` will - include code that parses expanded VCS keywords in generated tarballs. The - 'build' and 'sdist' commands will replace it with a copy that has just the - calculated version string. + `__init__.py` file, so it can be imported at runtime. If your project uses + `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. + This file should be checked in to your VCS as usual: the copy created below + by `setup.py setup_versioneer` will include code that parses expanded VCS + keywords in generated tarballs. The 'build' and 'sdist' commands will + replace it with a copy that has just the calculated version string. + + This must be set even if your project does not have any modules (and will + therefore never import `_version.py`), since "setup.py sdist" -based trees + still need somewhere to record the pre-calculated version strings. Anywhere + in the source tree should do. If there is a `__init__.py` next to your + `_version.py`, the `setup.py setup_versioneer` command (described below) + will append some `__version__`-setting assignments, if they aren't already + present. -* `versionfile_build`: +* `versionfile_build`: Like `versionfile_source`, but relative to the build directory instead of the source directory. These will differ when your setup.py uses @@ -101,49 +123,71 @@ then you will probably have `versionfile_build='myproject/_version.py'` and `versionfile_source='src/myproject/_version.py'`. + If this is set to None, then `setup.py build` will not attempt to rewrite + any `_version.py` in the built tree. If your project does not have any + libraries (e.g. if it only builds a script), then you should use + `versionfile_build = None`. To actually use the computed version string, + your `setup.py` will need to override `distutils.command.build_scripts` + with a subclass that explicitly inserts a copy of + `versioneer.get_version()` into your script file. See + `test/demoapp-script-only/setup.py` for an example. + * `tag_prefix`: a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. If your tags look like 'myproject-1.2.0', then you should use tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this - should be an empty string. + should be an empty string, using either `tag_prefix=` or `tag_prefix=''`. * `parentdir_prefix`: - a string, frequently the same as tag_prefix, which appears at the start of - all unpacked tarball filenames. If your tarball unpacks into - 'myproject-1.2.0', this should be 'myproject-'. - -This tool provides one script, named `versioneer-installer`. That script does -one thing: write a copy of `versioneer.py` into the current directory. + a optional string, frequently the same as tag_prefix, which appears at the + start of all unpacked tarball filenames. If your tarball unpacks into + 'myproject-1.2.0', this should be 'myproject-'. To disable this feature, + just omit the field from your `setup.cfg`. + +This tool provides one script, named `versioneer`. That script has one mode, +"install", which writes a copy of `versioneer.py` into the current directory +and runs `versioneer.py setup` to finish the installation. To versioneer-enable your project: -* 1: Run `versioneer-installer` to copy `versioneer.py` into the top of your - source tree. - -* 2: add the following lines to the top of your `setup.py`, with the - configuration values you decided earlier: +* 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and + populating it with the configuration values you decided earlier (note that + the option names are not case-sensitive): + + ```` + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = + parentdir_prefix = myproject- + ```` + +* 2: Run `versioneer install`. This will do the following: + + * copy `versioneer.py` into the top of your source tree + * create `_version.py` in the right place (`versionfile_source`) + * modify your `__init__.py` (if one exists next to `_version.py`) to define + `__version__` (by calling a function from `_version.py`) + * modify your `MANIFEST.in` to include both `versioneer.py` and the + generated `_version.py` in sdist tarballs + + `versioneer install` will complain about any problems it finds with your + `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all + the problems. - import versioneer - versioneer.versionfile_source = 'src/myproject/_version.py' - versioneer.versionfile_build = 'myproject/_version.py' - versioneer.tag_prefix = '' # tags are like 1.2.0 - versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' - -* 3: add the following arguments to the setup() call in your setup.py: +* 3: add a `import versioneer` to your setup.py, and add the following + arguments to the setup() call: version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), -* 4: now run `setup.py versioneer`, which will create `_version.py`, and - will modify your `__init__.py` to define `__version__` (by calling a - function from `_version.py`). It will also modify your `MANIFEST.in` to - include both `versioneer.py` and the generated `_version.py` in sdist - tarballs. - -* 5: commit these changes to your VCS. To make sure you won't forget, - `setup.py versioneer` will mark everything it touched for addition. +* 4: commit these changes to your VCS. To make sure you won't forget, + `versioneer install` will mark everything it touched for addition using + `git add`. Don't forget to add `setup.py` and `setup.cfg` too. ## Post-Installation Usage @@ -163,9 +207,8 @@ * 1: git tag 1.0 * 2: git push; git push --tags -Currently, all version strings must be based upon a tag. Versioneer will -report "unknown" until your tree has at least one tag in its history. This -restriction will be fixed eventually (see issue #12). +Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at +least one tag in its history. ## Version-String Flavors @@ -174,52 +217,113 @@ `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can import the top-level `versioneer.py` and run `get_versions()`. -Both functions return a dictionary with different keys for different flavors -of the version string: +Both functions return a dictionary with different flavors of version +information: -* `['version']`: condensed tag+distance+shortid+dirty identifier. For git, - this uses the output of `git describe --tags --dirty --always` but strips - the tag_prefix. For example "0.11-2-g1076c97-dirty" indicates that the tree - is like the "1076c97" commit but has uncommitted changes ("-dirty"), and - that this commit is two revisions ("-2-") beyond the "0.11" tag. For - released software (exactly equal to a known tag), the identifier will only - contain the stripped tag, e.g. "0.11". - -* `['full']`: detailed revision identifier. For Git, this is the full SHA1 - commit id, followed by "-dirty" if the tree contains uncommitted changes, - e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac-dirty". - -Some variants are more useful than others. Including `full` in a bug report -should allow developers to reconstruct the exact code being tested (or -indicate the presence of local changes that should be shared with the +* `['version']`: A condensed version string, rendered using the selected + style. This is the most commonly used value for the project's version + string. The default "pep440" style yields strings like `0.11`, + `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section + below for alternative styles. + +* `['full-revisionid']`: detailed revision identifier. For Git, this is the + full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". + +* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that + this is only accurate if run in a VCS checkout, otherwise it is likely to + be False or None + +* `['error']`: if the version string could not be computed, this will be set + to a string describing the problem, otherwise it will be None. It may be + useful to throw an exception in setup.py if this is set, to avoid e.g. + creating tarballs with a version string of "unknown". + +Some variants are more useful than others. Including `full-revisionid` in a +bug report should allow developers to reconstruct the exact code being tested +(or indicate the presence of local changes that should be shared with the developers). `version` is suitable for display in an "about" box or a CLI `--version` output: it can be easily compared against release notes and lists of bugs fixed in various releases. -In the future, this will also include a -[PEP-0440](http://legacy.python.org/dev/peps/pep-0440/) -compatible flavor -(e.g. `1.2.post0.dev123`). This loses a lot of information (and has no room -for a hash-based revision id), but is safe to use in a `setup.py` -"`version=`" argument. It also enables tools like *pip* to compare version -strings and evaluate compatibility constraint declarations. - -The `setup.py versioneer` command adds the following text to your -`__init__.py` to place a basic version in `YOURPROJECT.__version__`: +The installer adds the following text to your `__init__.py` to place a basic +version in `YOURPROJECT.__version__`: from ._version import get_versions - __version = get_versions()['version'] + __version__ = get_versions()['version'] del get_versions +## Styles + +The setup.cfg `style=` configuration controls how the VCS information is +rendered into a version string. + +The default style, "pep440", produces a PEP440-compliant string, equal to the +un-prefixed tag name for actual releases, and containing an additional "local +version" section with more detail for in-between builds. For Git, this is +TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags +--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the +tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and +that this commit is two revisions ("+2") beyond the "0.11" tag. For released +software (exactly equal to a known tag), the identifier will only contain the +stripped tag, e.g. "0.11". + +Other styles are available. See details.md in the Versioneer source tree for +descriptions. + +## Debugging + +Versioneer tries to avoid fatal errors: if something goes wrong, it will tend +to return a version of "0+unknown". To investigate the problem, run `setup.py +version`, which will run the version-lookup code in a verbose mode, and will +display the full contents of `get_versions()` (including the `error` string, +which may help identify what went wrong). + ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) -* re-run `versioneer-installer` in your source tree to replace `versioneer.py` -* edit `setup.py`, if necessary, to include any new configuration settings indicated by the release notes -* re-run `setup.py versioneer` to replace `SRC/_version.py` +* edit `setup.cfg`, if necessary, to include any new configuration settings + indicated by the release notes +* re-run `versioneer install` in your source tree, to replace + `SRC/_version.py` * commit any changed files +### Upgrading to 0.16 + +Nothing special. + +### Upgrading to 0.15 + +Starting with this version, Versioneer is configured with a `[versioneer]` +section in your `setup.cfg` file. Earlier versions required the `setup.py` to +set attributes on the `versioneer` module immediately after import. The new +version will refuse to run (raising an exception during import) until you +have provided the necessary `setup.cfg` section. + +In addition, the Versioneer package provides an executable named +`versioneer`, and the installation process is driven by running `versioneer +install`. In 0.14 and earlier, the executable was named +`versioneer-installer` and was run without an argument. + +### Upgrading to 0.14 + +0.14 changes the format of the version string. 0.13 and earlier used +hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a +plus-separated "local version" section strings, with dot-separated +components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old +format, but should be ok with the new one. + +### Upgrading from 0.11 to 0.12 + +Nothing special. + +### Upgrading from 0.10 to 0.11 + +You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running +`setup.py setup_versioneer`. This will enable the use of additional +version-control systems (SVN, etc) in the future. + ## Future Directions This tool is designed to make it easily extended to other version-control @@ -236,50 +340,223 @@ ## License -To make Versioneer easier to embed, all its code is hereby released into the -public domain. The `_version.py` that it creates is also in the public -domain. +To make Versioneer easier to embed, all its code is dedicated to the public +domain. The `_version.py` that it creates is also in the public domain. +Specifically, both are released under the Creative Commons "Public Domain +Dedication" license (CC0-1.0), as described in +https://creativecommons.org/publicdomain/zero/1.0/ . """ -import os, sys, re -from distutils.core import Command -from distutils.command.sdist import sdist as _sdist -from distutils.command.build import build as _build +from __future__ import print_function +try: + import configparser +except ImportError: + import ConfigParser as configparser +import errno +import json +import os +import re +import subprocess +import sys + -versionfile_source = None -versionfile_build = None -tag_prefix = None -parentdir_prefix = None +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" -VCS = "git" +def get_root(): + """Get the project root directory. -LONG_VERSION_PY = ''' + We require that all commands are run from the project root, i.e. the + directory that contains setup.py, setup.cfg, and versioneer.py . + """ + root = os.path.realpath(os.path.abspath(os.getcwd())) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + # allow 'python path/to/setup.py COMMAND' + root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") + raise VersioneerBadRootError(err) + try: + # Certain runtime workflows (setup.py install/develop in a setuptools + # tree) execute all dependencies in a single python process, so + # "versioneer" may be imported multiple times, and python's shared + # module-import table will cache the first one. So we can't use + # os.path.dirname(__file__), as that will find whichever + # versioneer.py was first imported, even in later projects. + me = os.path.realpath(os.path.abspath(__file__)) + if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]: + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(me), versioneer_py)) + except NameError: + pass + return root + + +def get_config_from_root(root): + """Read the project setup.cfg file to determine Versioneer config.""" + # This might raise EnvironmentError (if setup.cfg is missing), or + # configparser.NoSectionError (if it lacks a [versioneer] section), or + # configparser.NoOptionError (if it lacks "VCS="). See the docstring at + # the top of versioneer.py for instructions on writing your setup.cfg . + setup_cfg = os.path.join(root, "setup.cfg") + parser = configparser.SafeConfigParser() + with open(setup_cfg, "r") as f: + parser.readfp(f) + VCS = parser.get("versioneer", "VCS") # mandatory + + def get(parser, name): + if parser.has_option("versioneer", name): + return parser.get("versioneer", name) + return None + cfg = VersioneerConfig() + cfg.VCS = VCS + cfg.style = get(parser, "style") or "" + cfg.versionfile_source = get(parser, "versionfile_source") + cfg.versionfile_build = get(parser, "versionfile_build") + cfg.tag_prefix = get(parser, "tag_prefix") + if cfg.tag_prefix in ("''", '""'): + cfg.tag_prefix = "" + cfg.parentdir_prefix = get(parser, "parentdir_prefix") + cfg.verbose = get(parser, "verbose") + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + +# these dictionaries contain VCS-specific tools +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + return None + return stdout +LONG_VERSION_PY['git'] = ''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (build by setup.py sdist) and build +# feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.10 (https://github.com/warner/python-versioneer) - -# these strings will be replaced by git during git-archive -git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" -git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" +# versioneer-0.16 (https://github.com/warner/python-versioneer) +"""Git implementation of _version.py.""" +import errno +import os +import re import subprocess import sys -import errno + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" + git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "%(STYLE)s" + cfg.tag_prefix = "%(TAG_PREFIX)s" + cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" + cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: + dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr @@ -290,7 +567,7 @@ if e.errno == errno.ENOENT: continue if verbose: - print("unable to run %%s" %% args[0]) + print("unable to run %%s" %% dispcmd) print(e) return None else: @@ -298,47 +575,67 @@ print("unable to find command, tried %%s" %% (commands,)) return None stdout = p.communicate()[0].strip() - if sys.version >= '3': + if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: - print("unable to run %%s (error)" %% args[0]) + print("unable to run %%s (error)" %% dispcmd) return None return stdout -import sys -import re -import os.path +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes + both the project name and a version string. + """ + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%%s', but '%%s' doesn't start with " + "prefix '%%s'" %% (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + -def get_expanded_variables(versionfile_abs): +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these - # variables. When used from setup.py, we don't want to import - # _version.py, so we do it with a regexp instead. This function is not - # used from _version.py. - variables = {} + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} try: - f = open(versionfile_abs,"r") + f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["refnames"] = mo.group(1) + keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["full"] = mo.group(1) + keywords["full"] = mo.group(1) f.close() except EnvironmentError: pass - return variables + return keywords -def versions_from_expanded_variables(variables, tag_prefix, verbose=False): - refnames = variables["refnames"].strip() + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: - print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. @@ -363,158 +660,350 @@ r = ref[len(tag_prefix):] if verbose: print("picking %%s" %% r) - return { "version": r, - "full": variables["full"].strip() } - # no suitable tags, so we use the full revision id + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: - print("no suitable tags, using full revision id") - return { "version": variables["full"].strip(), - "full": variables["full"].strip() } - -def versions_from_vcs(tag_prefix, root, verbose=False): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' variables were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ if not os.path.exists(os.path.join(root, ".git")): if verbose: print("no .git in %%s" %% root) - return {} + raise NotThisMethod("no .git directory") GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], - cwd=root) - if stdout is None: - return {} - if not stdout.startswith(tag_prefix): - if verbose: - print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix)) - return {} - tag = stdout[len(tag_prefix):] - stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if stdout is None: - return {} - full = stdout.strip() - if tag.endswith("-dirty"): - full += "-dirty" - return {"version": tag, "full": full} - - -def versions_from_parentdir(parentdir_prefix, root, verbose=False): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %% - (root, dirname, parentdir_prefix)) - return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%%s*" %% tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%%s'" + %% describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%%s' doesn't start with prefix '%%s'" + print(fmt %% (full_tag, tag_prefix)) + pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" + %% (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%%d" %% pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%%d" %% pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%%s'" %% style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} -tag_prefix = "%(TAG_PREFIX)s" -parentdir_prefix = "%(PARENTDIR_PREFIX)s" -versionfile_source = "%(VERSIONFILE_SOURCE)s" -def get_versions(default={"version": "unknown", "full": ""}, verbose=False): +def get_versions(): + """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded variables. + # case we can only use expanded keywords. - variables = { "refnames": git_refnames, "full": git_full } - ver = versions_from_expanded_variables(variables, tag_prefix, verbose) - if ver: - return ver + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass try: - root = os.path.abspath(__file__) + root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in range(len(versionfile_source.split("/"))): + for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: - return default - - return (versions_from_vcs(tag_prefix, root, verbose) - or versions_from_parentdir(parentdir_prefix, root, verbose) - or default) - -''' - - -import subprocess -import sys -import errno + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): - assert isinstance(commands, list) - p = None - for c in commands: - try: - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % args[0]) - print(e) - return None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None - stdout = p.communicate()[0].strip() - if sys.version >= '3': - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % args[0]) - return None - return stdout + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} +''' -import sys -import re -import os.path -def get_expanded_variables(versionfile_abs): +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these - # variables. When used from setup.py, we don't want to import - # _version.py, so we do it with a regexp instead. This function is not - # used from _version.py. - variables = {} + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} try: - f = open(versionfile_abs,"r") + f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["refnames"] = mo.group(1) + keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["full"] = mo.group(1) + keywords["full"] = mo.group(1) f.close() except EnvironmentError: pass - return variables + return keywords + -def versions_from_expanded_variables(variables, tag_prefix, verbose=False): - refnames = variables["refnames"].strip() +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: - print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. @@ -539,87 +1028,119 @@ r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) - return { "version": r, - "full": variables["full"].strip() } - # no suitable tags, so we use the full revision id + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: - print("no suitable tags, using full revision id") - return { "version": variables["full"].strip(), - "full": variables["full"].strip() } - -def versions_from_vcs(tag_prefix, root, verbose=False): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' variables were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ if not os.path.exists(os.path.join(root, ".git")): if verbose: print("no .git in %s" % root) - return {} + raise NotThisMethod("no .git directory") GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], - cwd=root) - if stdout is None: - return {} - if not stdout.startswith(tag_prefix): - if verbose: - print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) - return {} - tag = stdout[len(tag_prefix):] - stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if stdout is None: - return {} - full = stdout.strip() - if tag.endswith("-dirty"): - full += "-dirty" - return {"version": tag, "full": full} - - -def versions_from_parentdir(parentdir_prefix, root, verbose=False): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % - (root, dirname, parentdir_prefix)) - return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} -import os.path -import sys + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces -# os.path.relpath only appeared in Python-2.6 . Define it here for 2.5. -def os_path_relpath(path, start=os.path.curdir): - """Return a relative version of a path""" - - if not path: - raise ValueError("no path specified") - - start_list = [x for x in os.path.abspath(start).split(os.path.sep) if x] - path_list = [x for x in os.path.abspath(path).split(os.path.sep) if x] - - # Work out how much of the filepath is shared by start and path. - i = len(os.path.commonprefix([start_list, path_list])) - - rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] - if not rel_list: - return os.path.curdir - return os.path.join(*rel_list) def do_vcs_install(manifest_in, versionfile_source, ipy): + """Git-specific installation logic for Versioneer. + + For Git, this means creating/changing .gitattributes to mark _version.py + for export-time keyword substitution. + """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source, ipy] + files = [manifest_in, versionfile_source] + if ipy: + files.append(ipy) try: me = __file__ if me.endswith(".pyc") or me.endswith(".pyo"): me = os.path.splitext(me)[0] + ".py" - versioneer_file = os_path_relpath(me) + versioneer_file = os.path.relpath(me) except NameError: versioneer_file = "versioneer.py" files.append(versioneer_file) @@ -632,7 +1153,7 @@ present = True f.close() except EnvironmentError: - pass + pass if not present: f = open(".gitattributes", "a+") f.write("%s export-subst\n" % versionfile_source) @@ -640,162 +1161,484 @@ files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes + both the project name and a version string. + """ + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with " + "prefix '%s'" % (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.10) from +# This file was generated by 'versioneer.py' (0.16) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. -version_version = '%(version)s' -version_full = '%(full)s' -def get_versions(default={}, verbose=False): - return {'version': version_version, 'full': version_full} +import json +import sys + +version_json = ''' +%s +''' # END VERSION_JSON + +def get_versions(): + return json.loads(version_json) """ -DEFAULT = {"version": "unknown", "full": "unknown"} def versions_from_file(filename): - versions = {} + """Try to determine the version from _version.py if present.""" try: - f = open(filename) + with open(filename) as f: + contents = f.read() except EnvironmentError: - return versions - for line in f.readlines(): - mo = re.match("version_version = '([^']+)'", line) - if mo: - versions["version"] = mo.group(1) - mo = re.match("version_full = '([^']+)'", line) - if mo: - versions["full"] = mo.group(1) - f.close() - return versions + raise NotThisMethod("unable to read _version.py") + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + raise NotThisMethod("no version_json in _version.py") + return json.loads(mo.group(1)) + def write_to_version_file(filename, versions): - f = open(filename, "w") - f.write(SHORT_VERSION_PY % versions) - f.close() + """Write the given version number to the given _version.py file.""" + os.unlink(filename) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) + with open(filename, "w") as f: + f.write(SHORT_VERSION_PY % contents) + print("set %s to '%s'" % (filename, versions["version"])) -def get_root(): - try: - return os.path.dirname(os.path.abspath(__file__)) - except NameError: - return os.path.dirname(os.path.abspath(sys.argv[0])) -def get_versions(default=DEFAULT, verbose=False): - # returns dict with two keys: 'version' and 'full' - assert versionfile_source is not None, "please set versioneer.versionfile_source" - assert tag_prefix is not None, "please set versioneer.tag_prefix" - assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" - # I am in versioneer.py, which must live at the top of the source tree, - # which we use to compute the root directory. py2exe/bbfreeze/non-CPython - # don't have __file__, in which case we fall back to sys.argv[0] (which - # ought to be the setup.py script). We prefer __file__ since that's more - # robust in cases where setup.py was invoked in some weird way (e.g. pip) +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +class VersioneerBadRootError(Exception): + """The project root directory is unknown or missing key files.""" + + +def get_versions(verbose=False): + """Get the project version from whatever source is available. + + Returns dict with two keys: 'version' and 'full'. + """ + if "versioneer" in sys.modules: + # see the discussion in cmdclass.py:get_cmdclass() + del sys.modules["versioneer"] + root = get_root() - versionfile_abs = os.path.join(root, versionfile_source) + cfg = get_config_from_root(root) - # extract version from first of _version.py, 'git describe', parentdir. - # This is meant to work for developers using a source checkout, for users - # of a tarball created by 'setup.py sdist', and for users of a - # tarball/zipball created by 'git archive' or github's download-from-tag - # feature. - - variables = get_expanded_variables(versionfile_abs) - if variables: - ver = versions_from_expanded_variables(variables, tag_prefix) - if ver: - if verbose: print("got version from expanded variable %s" % ver) + assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" + handlers = HANDLERS.get(cfg.VCS) + assert handlers, "unrecognized VCS '%s'" % cfg.VCS + verbose = verbose or cfg.verbose + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" + assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" + + versionfile_abs = os.path.join(root, cfg.versionfile_source) + + # extract version from first of: _version.py, VCS command (e.g. 'git + # describe'), parentdir. This is meant to work for developers using a + # source checkout, for users of a tarball created by 'setup.py sdist', + # and for users of a tarball/zipball created by 'git archive' or github's + # download-from-tag feature or the equivalent in other VCSes. + + get_keywords_f = handlers.get("get_keywords") + from_keywords_f = handlers.get("keywords") + if get_keywords_f and from_keywords_f: + try: + keywords = get_keywords_f(versionfile_abs) + ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) + if verbose: + print("got version from expanded keyword %s" % ver) return ver + except NotThisMethod: + pass - ver = versions_from_file(versionfile_abs) - if ver: - if verbose: print("got version from file %s %s" % (versionfile_abs,ver)) + try: + ver = versions_from_file(versionfile_abs) + if verbose: + print("got version from file %s %s" % (versionfile_abs, ver)) return ver + except NotThisMethod: + pass - ver = versions_from_vcs(tag_prefix, root, verbose) - if ver: - if verbose: print("got version from git %s" % ver) - return ver + from_vcs_f = handlers.get("pieces_from_vcs") + if from_vcs_f: + try: + pieces = from_vcs_f(cfg.tag_prefix, root, verbose) + ver = render(pieces, cfg.style) + if verbose: + print("got version from VCS %s" % ver) + return ver + except NotThisMethod: + pass - ver = versions_from_parentdir(parentdir_prefix, root, verbose) - if ver: - if verbose: print("got version from parentdir %s" % ver) - return ver + try: + if cfg.parentdir_prefix: + ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + if verbose: + print("got version from parentdir %s" % ver) + return ver + except NotThisMethod: + pass - if verbose: print("got version from default %s" % ver) - return default + if verbose: + print("unable to compute version") -def get_version(verbose=False): - return get_versions(verbose=verbose)["version"] + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version"} -class cmd_version(Command): - description = "report generated version string" - user_options = [] - boolean_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - ver = get_version(verbose=True) - print("Version is currently: %s" % ver) - - -class cmd_build(_build): - def run(self): - versions = get_versions(verbose=True) - _build.run(self) - # now locate _version.py in the new build/ directory and replace it - # with an updated value - target_versionfile = os.path.join(self.build_lib, versionfile_build) - print("UPDATING %s" % target_versionfile) - os.unlink(target_versionfile) - f = open(target_versionfile, "w") - f.write(SHORT_VERSION_PY % versions) - f.close() -if 'cx_Freeze' in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe +def get_version(): + """Get the short version string for this project.""" + return get_versions()["version"] + + +def get_cmdclass(): + """Get the custom setuptools/distutils subclasses used by Versioneer.""" + if "versioneer" in sys.modules: + del sys.modules["versioneer"] + # this fixes the "python setup.py develop" case (also 'install' and + # 'easy_install .'), in which subdependencies of the main project are + # built (using setup.py bdist_egg) in the same python process. Assume + # a main project A and a dependency B, which use different versions + # of Versioneer. A's setup.py imports A's Versioneer, leaving it in + # sys.modules by the time B's setup.py is executed, causing B to run + # with the wrong versioneer. Setuptools wraps the sub-dep builds in a + # sandbox that restores sys.modules to it's pre-build state, so the + # parent is protected against the child's "import versioneer". By + # removing ourselves from sys.modules here, before the child build + # happens, we protect the child from the parent's versioneer too. + # Also see https://github.com/warner/python-versioneer/issues/52 + + cmds = {} + + # we add "version" to both distutils and setuptools + from distutils.core import Command + + class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + vers = get_versions(verbose=True) + print("Version: %s" % vers["version"]) + print(" full-revisionid: %s" % vers.get("full-revisionid")) + print(" dirty: %s" % vers.get("dirty")) + if vers["error"]: + print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version + + # we override "build_py" in both distutils and setuptools + # + # most invocation pathways end up running build_py: + # distutils/build -> build_py + # distutils/install -> distutils/build ->.. + # setuptools/bdist_wheel -> distutils/install ->.. + # setuptools/bdist_egg -> distutils/install_lib -> build_py + # setuptools/install -> bdist_egg ->.. + # setuptools/develop -> ? + + # we override different "build_py" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.build_py import build_py as _build_py + else: + from distutils.command.build_py import build_py as _build_py + + class cmd_build_py(_build_py): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_py.run(self) + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if cfg.versionfile_build: + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py + + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe + + class cmd_build_exe(_build_exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _build_exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["build_exe"] = cmd_build_exe + del cmds["build_py"] + + # we override different "sdist" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.sdist import sdist as _sdist + else: + from distutils.command.sdist import sdist as _sdist - class cmd_build_exe(_build_exe): + class cmd_sdist(_sdist): def run(self): - versions = get_versions(verbose=True) - target_versionfile = versionfile_source + versions = get_versions() + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old + # version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + root = get_root() + cfg = get_config_from_root(root) + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory + # (remembering that it may be a hardlink) and replace it with an + # updated value + target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) - os.unlink(target_versionfile) - f = open(target_versionfile, "w") - f.write(SHORT_VERSION_PY % versions) - f.close() - _build_exe.run(self) - os.unlink(target_versionfile) - f = open(versionfile_source, "w") - f.write(LONG_VERSION_PY % {"DOLLAR": "$", - "TAG_PREFIX": tag_prefix, - "PARENTDIR_PREFIX": parentdir_prefix, - "VERSIONFILE_SOURCE": versionfile_source, - }) - f.close() - -class cmd_sdist(_sdist): - def run(self): - versions = get_versions(verbose=True) - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir, files): - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory (remembering - # that it may be a hardlink) and replace it with an updated value - target_versionfile = os.path.join(base_dir, versionfile_source) - print("UPDATING %s" % target_versionfile) - os.unlink(target_versionfile) - f = open(target_versionfile, "w") - f.write(SHORT_VERSION_PY % self._versioneer_generated_versions) - f.close() + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist + + return cmds + + +CONFIG_ERROR = """ +setup.cfg is missing the necessary Versioneer configuration. You need +a section like: + + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = + parentdir_prefix = myproject- + +You will also need to edit your setup.py to use the results: + + import versioneer + setup(version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ...) + +Please read the docstring in ./versioneer.py for configuration instructions, +edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. +""" + +SAMPLE_CONFIG = """ +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +#VCS = git +#style = pep440 +#versionfile_source = +#versionfile_build = +#tag_prefix = +#parentdir_prefix = + +""" INIT_PY_SNIPPET = """ from ._version import get_versions @@ -803,83 +1646,129 @@ del get_versions """ -class cmd_update_files(Command): - description = "install/upgrade Versioneer files: __init__.py SRC/_version.py" - user_options = [] - boolean_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - print(" creating %s" % versionfile_source) - f = open(versionfile_source, "w") - f.write(LONG_VERSION_PY % {"DOLLAR": "$", - "TAG_PREFIX": tag_prefix, - "PARENTDIR_PREFIX": parentdir_prefix, - "VERSIONFILE_SOURCE": versionfile_source, - }) - f.close() - ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py") +def do_setup(): + """Main VCS-independent setup function for installing Versioneer.""" + root = get_root() + try: + cfg = get_config_from_root(root) + except (EnvironmentError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) + with open(os.path.join(root, "setup.cfg"), "a") as f: + f.write(SAMPLE_CONFIG) + print(CONFIG_ERROR, file=sys.stderr) + return 1 + + print(" creating %s" % cfg.versionfile_source) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + if os.path.exists(ipy): try: - old = open(ipy, "r").read() + with open(ipy, "r") as f: + old = f.read() except EnvironmentError: old = "" if INIT_PY_SNIPPET not in old: print(" appending to %s" % ipy) - f = open(ipy, "a") - f.write(INIT_PY_SNIPPET) - f.close() + with open(ipy, "a") as f: + f.write(INIT_PY_SNIPPET) else: print(" %s unmodified" % ipy) + else: + print(" %s doesn't exist, ok" % ipy) + ipy = None - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(get_root(), "MANIFEST.in") - simple_includes = set() - try: - for line in open(manifest_in, "r").readlines(): + # Make sure both the top-level "versioneer.py" and versionfile_source + # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so + # they'll be copied into source distributions. Pip won't be able to + # install the package without this. + manifest_in = os.path.join(root, "MANIFEST.in") + simple_includes = set() + try: + with open(manifest_in, "r") as f: + for line in f: if line.startswith("include "): for include in line.split()[1:]: simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - f = open(manifest_in, "a") + except EnvironmentError: + pass + # That doesn't cover everything MANIFEST.in can do + # (http://docs.python.org/2/distutils/sourcedist.html#commands), so + # it might give some false negatives. Appending redundant 'include' + # lines is safe, though. + if "versioneer.py" not in simple_includes: + print(" appending 'versioneer.py' to MANIFEST.in") + with open(manifest_in, "a") as f: f.write("include versioneer.py\n") - f.close() - else: - print(" 'versioneer.py' already in MANIFEST.in") - if versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - versionfile_source) - f = open(manifest_in, "a") - f.write("include %s\n" % versionfile_source) - f.close() - else: - print(" versionfile_source already in MANIFEST.in") + else: + print(" 'versioneer.py' already in MANIFEST.in") + if cfg.versionfile_source not in simple_includes: + print(" appending versionfile_source ('%s') to MANIFEST.in" % + cfg.versionfile_source) + with open(manifest_in, "a") as f: + f.write("include %s\n" % cfg.versionfile_source) + else: + print(" versionfile_source already in MANIFEST.in") - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-time keyword - # substitution. - do_vcs_install(manifest_in, versionfile_source, ipy) + # Make VCS-specific changes. For git, this means creating/changing + # .gitattributes to mark _version.py for export-time keyword + # substitution. + do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + return 0 -def get_cmdclass(): - cmds = {'version': cmd_version, - 'versioneer': cmd_update_files, - 'build': cmd_build, - 'sdist': cmd_sdist, - } - if 'cx_Freeze' in sys.modules: # cx_freeze enabled? - cmds['build_exe'] = cmd_build_exe - del cmds['build'] - return cmds +def scan_setup_py(): + """Validate the contents of setup.py against Versioneer's expectations.""" + found = set() + setters = False + errors = 0 + with open("setup.py", "r") as f: + for line in f.readlines(): + if "import versioneer" in line: + found.add("import") + if "versioneer.get_cmdclass()" in line: + found.add("cmdclass") + if "versioneer.get_version()" in line: + found.add("get_version") + if "versioneer.VCS" in line: + setters = True + if "versioneer.versionfile_source" in line: + setters = True + if len(found) != 3: + print("") + print("Your setup.py appears to be missing some important items") + print("(but I might be wrong). Please make sure it has something") + print("roughly like the following:") + print("") + print(" import versioneer") + print(" setup( version=versioneer.get_version(),") + print(" cmdclass=versioneer.get_cmdclass(), ...)") + print("") + errors += 1 + if setters: + print("You should remove lines like 'versioneer.VCS = ' and") + print("'versioneer.versionfile_source = ' . This configuration") + print("now lives in setup.cfg, and should be removed from setup.py") + print("") + errors += 1 + return errors + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "setup": + errors = do_setup() + errors += scan_setup_py() + if errors: + sys.exit(1)