commit 577f318089c335e057efe6d5c72b9daedb1f6ce0 Author: Filip Pytloun Date: Thu Aug 11 11:30:06 2016 +0200 Import python-click-threading_0.4.0.orig.tar.gz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce68b0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +*.pyc +*.pyo +*.egg-ignore +*.egg-info +dist +build/ +docs/_build +click.egg-info +.tox +.cache diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..dfb9017 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python + +python: + - 2.7 + - pypy + - 3.3 + - 3.4 + +install: pip install tox +script: tox diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab35c0e --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2015 Markus Unterwaditzer & contributors + +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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d257e7b --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +release: + python setup.py sdist bdist_wheel upload diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b61d024 --- /dev/null +++ b/README.rst @@ -0,0 +1,15 @@ +click-threading +=============== + +.. image:: https://travis-ci.org/click-contrib/click-threading.svg?branch=master + :target: https://travis-ci.org/click-contrib/click-threading + + +Utilities for multithreading in `click `_. + +*This is rather experimental. See tests for usage for now.* + +License +======= + +Licensed under the MIT, see ``LICENSE``. diff --git a/click_threading/__init__.py b/click_threading/__init__.py new file mode 100644 index 0000000..16cde06 --- /dev/null +++ b/click_threading/__init__.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +import sys +import threading +import functools +import contextlib +import click + +from ._compat import reraise + +try: + import queue +except ImportError: + import Queue as queue + +# The docs state that "Future should not be instantiated directly, only by +# Executors", but since I'm basically implementing my own executor here, I +# think we're fine. +try: + from concurrent.futures import Future +except ImportError: + from futures import Future + +__version__ = '0.4.0' + +_CTX_WORKER_KEY = __name__ + '.uiworker' + + +def _is_main_thread(thread=None): + thread = thread or threading.current_thread() + return type(thread).__name__ == '_MainThread' + + +class Thread(threading.Thread): + '''A thread that automatically pushes the parent thread's context in the + new thread.''' + + def __init__(self, *args, **kwargs): + self._click_context = click.get_current_context() + super(Thread, self).__init__(*args, **kwargs) + + def run(self): + with self._click_context: + return super(Thread, self).run() + + +class UiWorker(object): + SHUTDOWN = object() + + def __init__(self): + if not _is_main_thread(): + raise RuntimeError('The UiWorker can only run on the main thread.') + + self.tasks = queue.Queue() + + def shutdown(self): + self.put(self.SHUTDOWN, wait=False) + + def run(self): + while True: + func, future = self.tasks.get() + if func is self.SHUTDOWN: + return + + try: + result = func() + except BaseException as e: + future.set_exception(e) + else: + future.set_result(result) + + def put(self, func, wait=True): + if _is_main_thread(): + return func() + + future = Future() + self.tasks.put((func, future)) + if not wait: + return + + return future.result() + + @contextlib.contextmanager + def patch_click(self): + from .monkey import patch_ui_functions + + def wrapper(f, info): + @functools.wraps(f) + def inner(*a, **kw): + return get_ui_worker() \ + .put(lambda: f(*a, **kw), wait=info.interactive) + return inner + + ctx = click.get_current_context() + with patch_ui_functions(wrapper): + ctx.meta[_CTX_WORKER_KEY] = self + try: + yield + finally: + assert ctx.meta.pop(_CTX_WORKER_KEY) is self + + +def get_ui_worker(): + try: + ctx = click.get_current_context() + return ctx.meta[_CTX_WORKER_KEY] + except (RuntimeError, KeyError): + raise RuntimeError('UI worker not found.') diff --git a/click_threading/_compat.py b/click_threading/_compat.py new file mode 100644 index 0000000..9f1e82d --- /dev/null +++ b/click_threading/_compat.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +import sys + +PY2 = sys.version_info[0] == 2 + +if PY2: + exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') +else: + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value diff --git a/click_threading/monkey.py b/click_threading/monkey.py new file mode 100644 index 0000000..93f426f --- /dev/null +++ b/click_threading/monkey.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +import contextlib + +class FunctionInfo(object): + def __init__(self, interactive): + self.interactive = interactive + +_ui_functions = { + 'echo_via_pager': FunctionInfo(interactive=True), + 'prompt': FunctionInfo(interactive=True), + 'confirm': FunctionInfo(interactive=True), + 'clear': FunctionInfo(interactive=False), + 'echo': FunctionInfo(interactive=False), + 'edit': FunctionInfo(interactive=True), + 'launch': FunctionInfo(interactive=True), + 'getchar': FunctionInfo(interactive=True), + 'pause': FunctionInfo(interactive=True), +} + + +@contextlib.contextmanager +def patch_ui_functions(wrapper): + '''Wrap all termui functions with a custom decorator.''' + NONE = object() + saved = {} + import click + + for name, info in _ui_functions.items(): + orig = getattr(click, name, NONE) + if orig is not NONE: + saved[name] = orig + setattr(click, name, wrapper(orig, info)) + + try: + yield + finally: + for name, orig in saved.items(): + setattr(click, name, orig) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b090585 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[wheel] +universal = 1 + +[flake8] +# W503: Line break before operator +ignore = W503 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1b3d84f --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +import ast +import re + +from setuptools import setup + +_version_re = re.compile(r'__version__\s+=\s+(.*)') + +with open('click_threading/__init__.py', 'rb') as f: + version = str(ast.literal_eval(_version_re.search( + f.read().decode('utf-8')).group(1))) + +setup( + name='click-threading', + version=version, + description='Multithreaded Click apps made easy', + author='Markus Unterwaditzer', + author_email='markus@unterwaditzer.net', + url='https://github.com/click-contrib/click-threading', + license='MIT', + packages=['click_threading'], + install_requires=[ + 'click>=5.0', + ], + extras_require={ + ':python_version < "3.2"': 'futures' + } +) diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..d705e52 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,56 @@ +import pytest + +import click_threading + +import click +from click.testing import CliRunner + + +@pytest.fixture +def runner(): + return CliRunner() + + +def test_context_pushing_thread(runner): + @click.command() + @click.pass_context + def cli(ctx): + contexts = [] + + def check_ctx(): + contexts.append(click.get_current_context()) + + t = click_threading.Thread(target=check_ctx) + t.start() + t.join() + + assert contexts == [ctx] + + runner.invoke(cli, catch_exceptions=False) + + +def test_ui_worker_basic(runner): + orig_click_prompt = click.prompt + + @click.command() + def cli(): + + ui = click_threading.UiWorker() + + def target(): + assert click.prompt is not orig_click_prompt + click.prompt('two') + ui.shutdown() + + click.prompt('one') + + with ui.patch_click(): + t = click_threading.Thread(target=target) + t.start() + ui.run() + + click.prompt('three') + t.join() + + result = runner.invoke(cli, catch_exceptions=False, input='y\n' * 3) + assert result.output.splitlines() == ['one: y', 'two: y', 'three: y'] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..131e0fe --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[testenv] +passenv = LANG +deps = + pytest + git+https://github.com/mitsuhiko/click +commands = py.test