diff --git a/.travis.yml b/.travis.yml index 602bd3f..a58b177 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ sudo: false language: python os: linux python: +- 3.4 - 3.5 - 3.6 - &mainstream_python 3.7-dev diff --git a/README.rst b/README.rst index 200e1ce..4767cfd 100644 --- a/README.rst +++ b/README.rst @@ -37,12 +37,13 @@ Pros: :: + Python 3.4 Python 3.5 Python 3.6 Python 3.7 PyPy3 3.5+ -.. note:: For python 2.7/3.4/PyPy you can use versions 1.x.x +.. note:: For python 2.7/PyPy you can use versions 1.x.x Decorators: diff --git a/setup.py b/setup.py index 46fab67..1611f29 100644 --- a/setup.py +++ b/setup.py @@ -213,6 +213,7 @@ def get_simple_vars_from_src(src): 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', @@ -245,7 +246,7 @@ def get_simple_vars_from_src(src): long_description=long_description, classifiers=classifiers, keywords=keywords, - python_requires='>=3.5', + python_requires='>=3.4', # While setuptools cannot deal with pre-installed incompatible versions, # setting a lower bound is not harmful - it makes error messages cleaner. DO # NOT set an upper bound on setuptools, as that will lead to uninstallable diff --git a/test/async_syntax/conftest.py b/test/async_syntax/conftest.py new file mode 100644 index 0000000..5168846 --- /dev/null +++ b/test/async_syntax/conftest.py @@ -0,0 +1,7 @@ +"""Test rules.""" +import sys + + +def pytest_ignore_collect(path, config): + """Ignore sources for python 3.4.""" + return sys.version_info < (3, 5) diff --git a/test/async_syntax/test_gevent_threadpooled_async.py b/test/async_syntax/test_gevent_threadpooled_async.py new file mode 100644 index 0000000..087c08d --- /dev/null +++ b/test/async_syntax/test_gevent_threadpooled_async.py @@ -0,0 +1,38 @@ +# Copyright 2017 Alexey Stepanov aka penguinolog +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import threading +import unittest + +try: + import gevent + import gevent.threadpool +except ImportError: + gevent = None + +import threaded + + +@unittest.skipIf(gevent is None, 'No gevent') +class TestThreadPooled(unittest.TestCase): + def tearDown(self): + threaded.GThreadPooled.shutdown() + + def test_thread_pooled_default_async(self): + @threaded.gthreadpooled + async def test(): + return threading.current_thread().name + + pooled_name = test().wait() + self.assertNotEqual(pooled_name, threading.current_thread().name) diff --git a/test/async_syntax/test_pooled_async.py b/test/async_syntax/test_pooled_async.py new file mode 100644 index 0000000..3a40e13 --- /dev/null +++ b/test/async_syntax/test_pooled_async.py @@ -0,0 +1,99 @@ +# Copyright 2017 Alexey Stepanov aka penguinolog +## +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import asyncio +import concurrent.futures +import threading +import unittest + +import threaded + + +class TestThreadPooled(unittest.TestCase): + def tearDown(self): + threaded.ThreadPooled.shutdown() + + def test_thread_pooled_default(self): + @threaded.threadpooled + async def test(): + return threading.current_thread().name + + pooled_name = concurrent.futures.wait([test()]) + self.assertNotEqual(pooled_name, threading.current_thread().name) + + def test_thread_pooled_construct(self): + @threaded.threadpooled() + async def test(): + return threading.current_thread().name + + pooled_name = concurrent.futures.wait([test()]) + self.assertNotEqual(pooled_name, threading.current_thread().name) + + def test_thread_pooled_loop(self): + loop = asyncio.get_event_loop() + + @threaded.threadpooled(loop_getter=loop) + async def test(): + return threading.current_thread().name + + pooled_name = loop.run_until_complete(asyncio.wait_for(test(), 1)) + self.assertNotEqual(pooled_name, threading.current_thread().name) + + def test_thread_pooled_loop_getter(self): + loop = asyncio.get_event_loop() + + @threaded.threadpooled(loop_getter=asyncio.get_event_loop) + async def test(): + return threading.current_thread().name + + pooled_name = loop.run_until_complete(asyncio.wait_for(test(), 1)) + self.assertNotEqual(pooled_name, threading.current_thread().name) + + def test_thread_pooled_loop_getter_context(self): + loop = asyncio.get_event_loop() + + def loop_getter(target): + return target + + @threaded.threadpooled( + loop_getter=loop_getter, + loop_getter_need_context=True + ) + async def test(*args, **kwargs): + return threading.current_thread().name + + pooled_name = loop.run_until_complete( + asyncio.wait_for(test(loop), 1) + ) + self.assertNotEqual(pooled_name, threading.current_thread().name) + + +class TestAsyncIOTask(unittest.TestCase): + def test_default(self): + @threaded.asynciotask + async def test(): + return 'test' + + loop = asyncio.get_event_loop() + res = loop.run_until_complete(asyncio.wait_for(test(), 1)) + self.assertEqual(res, 'test') + + def test_construct(self): + @threaded.asynciotask() + async def test(): + return 'test' + + loop = asyncio.get_event_loop() + res = loop.run_until_complete(asyncio.wait_for(test(), 1)) + self.assertEqual(res, 'test') diff --git a/test/test_gevent_threadpooled.py b/test/test_gevent_threadpooled.py index 71310b9..ef06000 100644 --- a/test/test_gevent_threadpooled.py +++ b/test/test_gevent_threadpooled.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from os import cpu_count +import os import threading import unittest @@ -38,14 +38,6 @@ def test(): pooled_name = test().wait() self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_default_async(self): - @threaded.gthreadpooled - async def test(): - return threading.current_thread().name - - pooled_name = test().wait() - self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_construct(self): @threaded.gthreadpooled() def test(): @@ -60,7 +52,7 @@ def test_thread_pooled_config(self): self.assertEqual( thread_pooled.executor.maxsize, - (cpu_count() or 1) * 5 + (os.cpu_count() or 1) * 5 ) thread_pooled.configure(max_workers=2) diff --git a/test/test_pooled_async.py b/test/test_pooled_coroutine.py similarity index 60% rename from test/test_pooled_async.py rename to test/test_pooled_coroutine.py index 0b936e1..43cfe38 100644 --- a/test/test_pooled_async.py +++ b/test/test_pooled_coroutine.py @@ -33,14 +33,6 @@ def test(): pooled_name = concurrent.futures.wait([test()]) self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_default_a(self): - @threaded.threadpooled - async def test(): - return threading.current_thread().name - - pooled_name = concurrent.futures.wait([test()]) - self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_construct(self): @threaded.threadpooled() @asyncio.coroutine @@ -50,14 +42,6 @@ def test(): pooled_name = concurrent.futures.wait([test()]) self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_construct_a(self): - @threaded.threadpooled() - async def test(): - return threading.current_thread().name - - pooled_name = concurrent.futures.wait([test()]) - self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_loop(self): loop = asyncio.get_event_loop() @@ -69,16 +53,6 @@ def test(): pooled_name = loop.run_until_complete(asyncio.wait_for(test(), 1)) self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_loop_a(self): - loop = asyncio.get_event_loop() - - @threaded.threadpooled(loop_getter=loop) - async def test(): - return threading.current_thread().name - - pooled_name = loop.run_until_complete(asyncio.wait_for(test(), 1)) - self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_loop_getter(self): loop = asyncio.get_event_loop() @@ -90,16 +64,6 @@ def test(): pooled_name = loop.run_until_complete(asyncio.wait_for(test(), 1)) self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_loop_getter_a(self): - loop = asyncio.get_event_loop() - - @threaded.threadpooled(loop_getter=asyncio.get_event_loop) - async def test(): - return threading.current_thread().name - - pooled_name = loop.run_until_complete(asyncio.wait_for(test(), 1)) - self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_loop_getter_context(self): loop = asyncio.get_event_loop() @@ -119,24 +83,6 @@ def test(*args, **kwargs): ) self.assertNotEqual(pooled_name, threading.current_thread().name) - def test_thread_pooled_loop_getter_context_a(self): - loop = asyncio.get_event_loop() - - def loop_getter(target): - return target - - @threaded.threadpooled( - loop_getter=loop_getter, - loop_getter_need_context=True - ) - async def test(*args, **kwargs): - return threading.current_thread().name - - pooled_name = loop.run_until_complete( - asyncio.wait_for(test(loop), 1) - ) - self.assertNotEqual(pooled_name, threading.current_thread().name) - class TestAsyncIOTask(unittest.TestCase): def test_default(self): @@ -149,26 +95,12 @@ def test(): res = loop.run_until_complete(asyncio.wait_for(test(), 1)) self.assertEqual(res, 'test') - def test_default_a(self): - @threaded.asynciotask - async def test(): - return 'test' - - loop = asyncio.get_event_loop() - res = loop.run_until_complete(asyncio.wait_for(test(), 1)) - self.assertEqual(res, 'test') - def test_construct(self): @threaded.asynciotask() @asyncio.coroutine def test(): return 'test' - def test_construct_a(self): - @threaded.asynciotask() - async def test(): - return 'test' - loop = asyncio.get_event_loop() res = loop.run_until_complete(asyncio.wait_for(test(), 1)) self.assertEqual(res, 'test') diff --git a/threaded/__init__.py b/threaded/__init__.py index 7c2e4b9..6dbdfe5 100644 --- a/threaded/__init__.py +++ b/threaded/__init__.py @@ -14,7 +14,7 @@ """threaded module.""" -import typing # noqa # pylint: disable=unused-import +import typing # pylint: disable=no-name-in-module from ._asynciotask import AsyncIOTask, asynciotask diff --git a/threaded/_threadpooled.py b/threaded/_threadpooled.py index 38049ea..e2aed59 100644 --- a/threaded/_threadpooled.py +++ b/threaded/_threadpooled.py @@ -19,6 +19,7 @@ import asyncio import concurrent.futures +import os import functools import typing @@ -146,7 +147,7 @@ def _get_loop( def _get_function_wrapper( self, func: typing.Callable - ) -> typing.Callable[..., typing.Union[typing.Awaitable, concurrent.futures.Future]]: + ) -> typing.Callable[..., typing.Union[concurrent.futures.Future, 'typing.Awaitable']]: """Here should be constructed and returned real decorator. :param func: Wrapped function @@ -163,8 +164,8 @@ def wrapper( *args: typing.Any, **kwargs: typing.Any ) -> typing.Union[ - typing.Awaitable, concurrent.futures.Future, - typing.Callable[..., typing.Union[typing.Awaitable, concurrent.futures.Future]] + concurrent.futures.Future, 'typing.Awaitable', + typing.Callable[..., typing.Union[concurrent.futures.Future, 'typing.Awaitable']] ]: loop = self._get_loop(*args, **kwargs) @@ -187,8 +188,8 @@ def __call__( # pylint: disable=useless-super-delegation *args: typing.Union[typing.Callable, typing.Any], **kwargs: typing.Any ) -> typing.Union[ - concurrent.futures.Future, typing.Awaitable, - typing.Callable[..., typing.Union[typing.Awaitable, concurrent.futures.Future]] + concurrent.futures.Future, 'typing.Awaitable', + typing.Callable[..., typing.Union[concurrent.futures.Future, 'typing.Awaitable']] ]: """Callable instance.""" return super(ThreadPooled, self).__call__(*args, **kwargs) # type: ignore @@ -263,7 +264,7 @@ def threadpooled( # noqa: F811 loop_getter_need_context: bool = False ) -> typing.Union[ ThreadPooled, - typing.Callable[..., typing.Union[concurrent.futures.Future, typing.Awaitable]] + typing.Callable[..., typing.Union[concurrent.futures.Future, 'typing.Awaitable']] ]: """Post function to ThreadPoolExecutor. @@ -301,6 +302,24 @@ class ThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor): __slots__ = () + def __init__( + self, + max_workers: typing.Optional[int] = None + ) -> None: + """Override init due to difference between Python <3.5 and 3.5+. + + :param max_workers: Maximum workers allowed. If none: cpu_count() or 1) * 5 + :type max_workers: typing.Optional[int] + """ + if max_workers is None: # Use 3.5+ behavior + max_workers = (os.cpu_count() or 1) * 5 + super( + ThreadPoolExecutor, + self + ).__init__( + max_workers=max_workers, + ) + @property def max_workers(self) -> int: """MaxWorkers. diff --git a/tools/build-wheels.sh b/tools/build-wheels.sh index 0bc72ec..032245f 100755 --- a/tools/build-wheels.sh +++ b/tools/build-wheels.sh @@ -1,5 +1,5 @@ #!/bin/bash -PYTHON_VERSIONS="cp35-cp35m cp36-cp36m cp37-cp37m" +PYTHON_VERSIONS="cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m" # Avoid creation of __pycache__/*.py[c|o] export PYTHONDONTWRITEBYTECODE=1 diff --git a/tox.ini b/tox.ini index fcc8382..76d8956 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ [tox] minversion = 2.0 -envlist = pep8, pylint, mypy, bandit, pep257, py{35,36,37,py3}, docs, py{35,36,37}-nocov +envlist = pep8, pylint, mypy, bandit, pep257, py{34,35,36,37,py3}, docs, py{34,35,36,37}-nocov skipsdist = True skip_missing_interpreters = True @@ -26,6 +26,13 @@ commands = py.test --junitxml=unit_result.xml --cov-config .coveragerc --cov-report html --cov=threaded {posargs:test} coverage report --fail-under 90 +[testenv:py34-nocov] +usedevelop = False +commands = + python setup.py bdist_wheel + pip install threaded --no-index -f dist + py.test -vv {posargs:test} + [testenv:py35-nocov] usedevelop = False commands = @@ -51,6 +58,7 @@ commands = commands = {posargs:} [tox:travis] +3.4 = py34, 3.5 = py35, 3.6 = py36, 3.7 = py37,