Import python-click-threading_0.4.0.orig.tar.gz
This commit is contained in:
commit
577f318089
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
.DS_Store
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.egg-ignore
|
||||
*.egg-info
|
||||
dist
|
||||
build/
|
||||
docs/_build
|
||||
click.egg-info
|
||||
.tox
|
||||
.cache
|
10
.travis.yml
Normal file
10
.travis.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- pypy
|
||||
- 3.3
|
||||
- 3.4
|
||||
|
||||
install: pip install tox
|
||||
script: tox
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -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.
|
1
MANIFEST.in
Normal file
1
MANIFEST.in
Normal file
|
@ -0,0 +1 @@
|
|||
include LICENSE
|
15
README.rst
Normal file
15
README.rst
Normal file
|
@ -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 <http://click.pocoo.org/>`_.
|
||||
|
||||
*This is rather experimental. See tests for usage for now.*
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Licensed under the MIT, see ``LICENSE``.
|
108
click_threading/__init__.py
Normal file
108
click_threading/__init__.py
Normal file
|
@ -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.')
|
13
click_threading/_compat.py
Normal file
13
click_threading/_compat.py
Normal file
|
@ -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
|
39
click_threading/monkey.py
Normal file
39
click_threading/monkey.py
Normal file
|
@ -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)
|
6
setup.cfg
Normal file
6
setup.cfg
Normal file
|
@ -0,0 +1,6 @@
|
|||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[flake8]
|
||||
# W503: Line break before operator
|
||||
ignore = W503
|
29
setup.py
Normal file
29
setup.py
Normal file
|
@ -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'
|
||||
}
|
||||
)
|
56
tests/test_basic.py
Normal file
56
tests/test_basic.py
Normal file
|
@ -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']
|
Loading…
Reference in a new issue