diff --git a/.coverage b/.coverage index acb4b54..5a7fa84 100644 Binary files a/.coverage and b/.coverage differ diff --git a/.github/workflows/test-worker.yml b/.github/workflows/test-worker.yml index bfb507a..71d033c 100644 --- a/.github/workflows/test-worker.yml +++ b/.github/workflows/test-worker.yml @@ -1,6 +1,6 @@ name: Run Python tests -on: [ push, pull_request ] +on: [ push ] jobs: build: @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11", "3.x"] + python-version: ["3.9", "3.10", "3.11", "3.x"] steps: diff --git a/.gitignore b/.gitignore index 10ef4e2..06b49b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Python stuff -venv/ +venv*/ *.pytest_cache __pycache__/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6ef7ebe..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Jun Xiang - -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/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..afae473 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2023, Jun Xiang + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 23e020a..4d7d71c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] -[![MIT License][license-shield]][license-url] +[![BSD-3-Clause License][license-shield]][license-url] [![LinkedIn][linkedin-shield]][linkedin-url] @@ -54,14 +54,6 @@ I hope thread will become your threading solution! ♡⸜(˶˃ ᵕ ˂˶)⸝♡ -### Built With - -* [![Python 3.11.6+](https://img.shields.io/badge/python-3.11.6+-blue.svg)](https://www.python.org/downloads/release/python-3116/) - -

(back to top)

- - - ## Getting Started @@ -70,7 +62,7 @@ To get a local copy up and running follow these simple example steps. ### Prerequisites -* Python 3.10+ +* Python 3.9+ ### Installation @@ -102,12 +94,11 @@ Our docs are [here!](/docs/getting-started.md) ## Roadmap -- [x] 0.0.1 Release - [x] Bug fixes -- [x] 0.1.0 Release -- [x] 0.1.1 Hotfix -- [ ] New Features -- [ ] 0.1.1 Release +- [x] Set Thread class to inherit from threading.Thread +- [ ] Add kill method +- [ ] Docs Update +- [ ] v0.1.2 Release See the [open issues](https://github.com/caffeine-addictt/thread/issues) for a full list of proposed features (and known issues). @@ -136,7 +127,7 @@ Don't forget to give the project a star! Thanks again! ## License -Distributed under the MIT License. See `LICENSE.txt` for more information. +Distributed under the BSD-3-Clause License. See `LICENSE.txt` for more information.

(back to top)

diff --git a/poetry.lock b/poetry.lock index ab61971..8173b83 100644 --- a/poetry.lock +++ b/poetry.lock @@ -203,7 +203,18 @@ files = [ {file = "ruff-0.1.5.tar.gz", hash = "sha256:5cbec0ef2ae1748fb194f420fb03fb2c25c3258c86129af7172ff8f198f125ab"}, ] +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "781a675fea5f2f70ce3fda416982ebf031e9951e266d0ecdafd8960fe52a4d90" +content-hash = "07e6ff0a0176752eab68a233e414e2009bd7b1cd732ca68167dd80f1b4b206aa" diff --git a/pyproject.toml b/pyproject.toml index 9ed2794..57cf462 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "thread" version = "0.1.1" description = "Threading module extension" authors = ["Alex "] -license = "MIT" +license = "BSD-3-Clause" readme = "README.md" packages = [{include = "thread", from = "src"}] include = [{ path = "tests", format = "sdist" }] @@ -14,15 +14,16 @@ keywords = ["threading", "extension", "multiprocessing"] classifiers = [ "Development Status :: 1 - Planning", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License" + "License :: OSI Approved :: BSD License" ] [tool.poetry.urls] "Bug Tracker" = "https://github.com/caffeine-addictt/thread/issues" [tool.poetry.dependencies] -python = "^3.11" +python = "^3.9" numpy = "^1.26.2" +typing-extensions = "^4.8.0" [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt index 1357faf..ded088e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ numpy==1.26.2 +typing_extensions==4.8.0 diff --git a/src/thread/exceptions.py b/src/thread/exceptions.py index 77a5813..bdc6048 100644 --- a/src/thread/exceptions.py +++ b/src/thread/exceptions.py @@ -1,5 +1,5 @@ import traceback -from typing import Any, Optional +from typing import Any, Optional, Sequence, Tuple class ThreadErrorBase(Exception): @@ -20,17 +20,37 @@ class ThreadNotRunningError(ThreadErrorBase): class ThreadNotInitializedError(ThreadErrorBase): """Exception class for attempting to invoke a method which requires the thread to be initialized, but isn't""" - message: str = 'Thread is not initialized, unable to invoke method. Have you ran `Thread.start()` at least once?' + message: str = 'Thread is not initialized, unable to invoke method.' class HookRuntimeError(ThreadErrorBase): """Exception class for hook runtime errors""" message: str = 'Encountered runtime errors in hooks' count: int = 0 - def add_exception_case(self, func_name: str, error: Exception): - self.count += 1 - trace = '\n'.join(traceback.format_stack()) - - self.add_note(f'\n{self.count}. {func_name}\n>>>>>>>>>>') - self.add_note(f'{trace}\n{error}') - self.add_note('<<<<<<<<<<') + def __init__(self, message: Optional[str] = '', extra: Sequence[Tuple[Exception, str]] = []) -> None: + """ + Extra for parsing all hooks that errored + + Parameters + ---------- + :param message: The message to be parsed, can be left blank + :param extra: Tuple of (Exception_Raised, function_name) + """ + new_message: str = message or self.message + + for i, v in enumerate(extra): + trace = '\n'.join(traceback.format_stack()) + new_message += f'\n\n{i}. {v[1]}\n>>>>>>>>>>' + new_message += f'{trace}\n{v[0]}' + new_message += '<<<<<<<<<<' + super().__init__(new_message) + + + # Python 3.9 doesn't support Exception.add_note() + # def add_exception_case(self, func_name: str, error: Exception): + # self.count += 1 + # trace = '\n'.join(traceback.format_stack()) + + # self.add_note(f'\n{self.count}. {func_name}\n>>>>>>>>>>') + # self.add_note(f'{trace}\n{error}') + # self.add_note('<<<<<<<<<<') diff --git a/src/thread/thread.py b/src/thread/thread.py index 67d93c6..c8345e8 100644 --- a/src/thread/thread.py +++ b/src/thread/thread.py @@ -1,29 +1,35 @@ -import numpy +import sys +import time +import signal import threading + +import numpy from . import exceptions from functools import wraps -from dataclasses import dataclass from typing import ( Any, List, - Callable, Concatenate, - Optional, Literal, - Mapping, Sequence + Callable, Union, Optional, Literal, + Mapping, Sequence, Tuple ) -@dataclass -class JoinTerminatedStatus: - """How the `Thread.join()` method terminated""" - status: Literal['Timeout Exceeded', 'Thread terminated'] - +ThreadStatus = Literal[ + 'Idle', + 'Running', + 'Invoking hooks', + 'Completed', -ThreadStatus = Literal['Idle', 'Running', 'Invoking hooks', 'Completed', 'Errored'] + 'Errored', + 'Kill Scheduled', + 'Killed' +] Data_In = Any Data_Out = Any Overflow_In = Any -class Thread: + +class Thread(threading.Thread): """ Wraps python's `threading.Thread` class --------------------------------------- @@ -31,25 +37,22 @@ class Thread: Type-Safe and provides more functionality on top """ - _thread : Optional[threading.Thread] status : ThreadStatus - hooks : List[Callable[[Data_Out], Any | None]] - returned_value : Data_Out - - target : Callable[Concatenate[Data_In, ...], Data_Out] - args : Sequence[Data_In] - kwargs : Mapping[str, Data_In] + hooks : List[Callable[[Data_Out], Union[Any, None]]] + returned_value: Data_Out errors : List[Exception] ignore_errors : Sequence[type[Exception]] suppress_errors: bool - overflow_args : Sequence[Overflow_In] - overflow_kwargs: Mapping[str, Overflow_In] + # threading.Thread stuff + _initialized : bool + _run : Callable + def __init__( self, - target: Callable[Concatenate[Data_In, ...], Data_Out], + target: Callable[..., Data_Out], args: Sequence[Data_In] = (), kwargs: Mapping[str, Data_In] = {}, ignore_errors: Sequence[type[Exception]] = (), @@ -57,6 +60,7 @@ def __init__( name: Optional[str] = None, daemon: bool = False, + group = None, *overflow_args: Overflow_In, **overflow_kwargs: Overflow_In ) -> None: @@ -72,34 +76,33 @@ def __init__( :param suppress_errors: This should be a boolean indicating whether exceptions will be raised, else will only write to internal `errors` property :param name: This is an argument parsed to `threading.Thread` :param daemon: This is an argument parsed to `threading.Thread` + :param group: This does nothing right now, but should be left as None :param *: These are arguments parsed to `threading.Thread` :param **: These are arguments parsed to `thread.Thread` """ - self._thread = None + _target = self._wrap_target(target) + self.returned_values = None self.status = 'Idle' self.hooks = [] - self.returned_value = None - - self.target = self._wrap_target(target) - self.args = args - self.kwargs = kwargs self.errors = [] self.ignore_errors = ignore_errors self.suppress_errors = suppress_errors - self.overflow_args = overflow_args - self.overflow_kwargs = { - 'name': name, - 'daemon': daemon, + super().__init__( + target = _target, + args = args, + kwargs = kwargs, + name = name, + daemon = daemon, + group = group, + *overflow_args, **overflow_kwargs - } + ) - def _wrap_target( - self, - target: Callable[Concatenate[Data_In, ...], Data_Out] - ) -> Callable[Concatenate[Data_In, ...], Data_Out]: + def _wrap_target(self, target: Callable[..., Data_Out]) -> Callable[..., Data_Out]: + """Wraps the target function""" @wraps(target) def wrapper(*args: Any, **kwargs: Any) -> Any: self.status = 'Running' @@ -116,32 +119,55 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: self._invoke_hooks() self.status = 'Completed' return wrapper - + def _invoke_hooks(self) -> None: - trace = exceptions.HookRuntimeError() + """Invokes hooks in the thread""" + errors: List[Tuple[Exception, str]] = [] for hook in self.hooks: try: hook(self.returned_value) except Exception as e: if not any(isinstance(e, ignore) for ignore in self.ignore_errors): - trace.add_exception_case( - hook.__name__, - e - ) + errors.append(( + e, + hook.__name__ + )) - if trace.count > 0: - self.errors.append(trace) + if len(errors) > 0: + self.errors.append(exceptions.HookRuntimeError( + None, errors + )) def _handle_exceptions(self) -> None: - """Raises exceptions if not suppressed""" + """Raises exceptions if not suppressed in the main thread""" if self.suppress_errors: return for e in self.errors: raise e + + def global_trace(self, frame, event: str, arg) -> Optional[Callable]: + if event == 'call': + return self.local_trace + + def local_trace(self, frame, event: str, arg): + if self.status == 'Kill Scheduled' and event == 'line': + print('KILLED ident:%s' % self.ident) + self.status = 'Killed' + raise SystemExit() + return self.local_trace + + def _run_with_trace(self) -> None: + """This will replace `threading.Thread`'s `run()` method""" + if not self._run: + raise exceptions.ThreadNotInitializedError('Running `_run_with_trace` may cause unintended behaviour, run `start` instead') + + sys.settrace(self.global_trace) + self._run() + @property def result(self) -> Data_Out: @@ -150,14 +176,13 @@ def result(self) -> Data_Out: Raises ------ - ThreadNotInitializedError: If the thread is not initialized + ThreadNotInitializedError: If the thread is not intialized ThreadNotRunningError: If the thread is not running ThreadStillRunningError: If the thread is still running """ - if not self._thread: + if not self._initialized: raise exceptions.ThreadNotInitializedError() - - if self.status == 'Idle': + if self.status in ['Idle', 'Killed']: raise exceptions.ThreadNotRunningError() self._handle_exceptions() @@ -175,12 +200,12 @@ def is_alive(self) -> bool: ------ ThreadNotInitializedError: If the thread is not intialized """ - if not self._thread: + if not self._initialized: raise exceptions.ThreadNotInitializedError() - return self._thread.is_alive() + return super().is_alive() - def add_hook(self, hook: Callable[[Data_Out], Any | None]) -> None: + def add_hook(self, hook: Callable[[Data_Out], Union[Any, None]]) -> None: """ Adds a hook to the thread ------------------------- @@ -194,7 +219,7 @@ def add_hook(self, hook: Callable[[Data_Out], Any | None]) -> None: self.hooks.append(hook) - def join(self, timeout: Optional[float] = None) -> 'JoinTerminatedStatus': + def join(self, timeout: Optional[float] = None) -> bool: """ Halts the current thread execution until a thread completes or exceeds the timeout @@ -204,22 +229,22 @@ def join(self, timeout: Optional[float] = None) -> 'JoinTerminatedStatus': Returns ------- - :returns JoinTerminatedStatus: Why the method stoped halting the thread + :returns bool: True if the thread is no-longer alive Raises ------ ThreadNotInitializedError: If the thread is not initialized ThreadNotRunningError: If the thread is not running """ - if not self._thread: + if not self._initialized: raise exceptions.ThreadNotInitializedError() - if self.status == 'Idle': + if self.status == ['Idle', 'Killed']: raise exceptions.ThreadNotRunningError() - self._thread.join(timeout) + super().join(timeout) self._handle_exceptions() - return JoinTerminatedStatus(self._thread.is_alive() and 'Timeout Exceeded' or 'Thread terminated') + return not self.is_alive() def get_return_value(self) -> Data_Out: @@ -234,25 +259,55 @@ def get_return_value(self) -> Data_Out: return self.result + def kill(self, yielding: bool = False, timeout: float = 5) -> bool: + """ + Schedules a thread to be killed + + Parameters + ---------- + :param yielding: If true, halts the current thread execution until the thread is killed + :param timeout: The maximum number of seconds to wait before exiting + + Returns + ------- + :returns bool: False if the it exceeded the timeout + + Raises + ------ + ThreadNotInitializedError: If the thread is not initialized + ThreadNotRunningError: If the thread is not running + """ + if not self.is_alive(): + raise exceptions.ThreadNotRunningError() + + self.status = 'Kill Scheduled' + if not yielding: + return True + + start = time.perf_counter() + while self.status != 'Killed': + time.sleep(0.01) + if (time.perf_counter() - start) >= timeout: + return False + + return True + + def start(self) -> None: """ Starts the thread Raises ------ + ThreadNotInitializedError: If the thread is not intialized ThreadStillRunningError: If there already is a running thread """ - if self._thread is not None and self._thread.is_alive(): + if self.is_alive(): raise exceptions.ThreadStillRunningError() - self._thread = threading.Thread( - target = self.target, - args = self.args, - kwargs = self.kwargs, - *self.overflow_args, - **self.overflow_kwargs - ) - self._thread.start() + self._run = self.run + self.run = self._run_with_trace + super().start() @@ -270,7 +325,7 @@ class ParallelProcessing: _return_vales : Mapping[int, List[Data_Out]] status : ThreadStatus - function : Callable[Concatenate[Sequence[Data_In], ...], List[Data_Out]] + function : Callable[..., List[Data_Out]] dataset : Sequence[Data_In] max_threads : int @@ -279,7 +334,7 @@ class ParallelProcessing: def __init__( self, - function: Callable[Concatenate[Data_In, ...], Data_Out], + function: Callable[..., Data_Out], dataset: Sequence[Data_In], max_threads: int = 8, @@ -294,7 +349,7 @@ def __init__( Parameters ---------- - :param function: This should be the function to validate each data entry in the `dataset` + :param function: This should be the function to validate each data entry in the `dataset`, the first argument parsed will be a value of the dataset :param dataset: This should be an iterable sequence of data entries :param max_threads: This should be an integer value of the max threads allowed :param *: These are arguments parsed to `threading.Thread` and `Thread` @@ -320,8 +375,8 @@ def __init__( def _wrap_function( self, - function: Callable[Concatenate[Data_In, ...], Data_Out] - ) -> Callable[Concatenate[Sequence[Data_In], ...], List[Data_Out]]: + function: Callable[..., Data_Out] + ) -> Callable[..., List[Data_Out]]: @wraps(function) def wrapper(data_chunk: Sequence[Data_In], *args: Any, **kwargs: Any) -> List[Data_Out]: computed: List[Data_Out] = [] @@ -354,10 +409,6 @@ def results(self) -> Data_Out: results: List[Data_Out] = [] for thread in self._threads: results += thread.result - if thread.status == 'Idle': - raise exceptions.ThreadNotRunningError() - elif thread.status == 'Running': - raise exceptions.ThreadStillRunningError() return results @@ -389,13 +440,13 @@ def get_return_values(self) -> List[Data_Out]: return results - def join(self) -> 'JoinTerminatedStatus': + def join(self) -> bool: """ Halts the current thread execution until a thread completes or exceeds the timeout Returns ------- - :returns JoinTerminatedStatus: Why the method stoped halting the thread + :returns bool: True if the thread is no-longer alive Raises ------ @@ -410,7 +461,20 @@ def join(self) -> 'JoinTerminatedStatus': for thread in self._threads: thread.join() - return JoinTerminatedStatus('Thread terminated') + return True + + + def kill(self) -> None: + """ + Kills the threads + + Raises + ------ + ThreadNotInitializedError: If the thread is not initialized + ThreadNotRunningError: If the thread is not running + """ + for thread in self._threads: + thread.kill() def start(self) -> None: @@ -440,3 +504,22 @@ def start(self) -> None: ) self._threads.append(chunk_thread) chunk_thread.start() + + + + + +# Handle abrupt exit +def service_shutdown(signum, frame): + print('\nCaught signal %d' % signum) + print('Gracefully killing active threads') + + for thread in threading.enumerate(): + if isinstance(thread, Thread): + thread.kill() + sys.exit(0) + + +# Register the signal handlers +signal.signal(signal.SIGTERM, service_shutdown) +signal.signal(signal.SIGINT, service_shutdown) diff --git a/tests/test_parallelprocessing.py b/tests/test_parallelprocessing.py index 165f3ac..8686b59 100644 --- a/tests/test_parallelprocessing.py +++ b/tests/test_parallelprocessing.py @@ -24,7 +24,8 @@ def test_threadsScaleDown(): function = _dummy_dataProcessor, dataset = dataset, max_threads = 4, - kwargs = { 'delay': 2 } + kwargs = { 'delay': 2 }, + daemon = True ) new.start() assert len(new._threads) == 2 @@ -35,7 +36,8 @@ def test_threadsProcessing(): new = ParallelProcessing( function = _dummy_dataProcessor, dataset = dataset, - args = [0.001] + args = [0.001], + daemon = True ) new.start() assert new.get_return_values() == dataset @@ -44,24 +46,14 @@ def test_threadsProcessing(): # >>>>>>>>>> Raising Exceptions <<<<<<<<<< # -def test_raises_notInitializedError(): - """This test should raise ThreadNotInitializedError""" - dataset = numpy.arange(0, 8).tolist() - new = ParallelProcessing( - function = _dummy_dataProcessor, - dataset = dataset - ) - - with pytest.raises(exceptions.ThreadNotInitializedError): - new.results - def test_raises_StillRunningError(): """This test should raise ThreadStillRunningError""" dataset = numpy.arange(0, 8).tolist() new = ParallelProcessing( function = _dummy_dataProcessor, dataset = dataset, - args = [1] + args = [1], + daemon = True ) new.start() with pytest.raises(exceptions.ThreadStillRunningError): @@ -73,7 +65,8 @@ def test_raises_RunTimeError(): new = ParallelProcessing( function = _dummy_raiseException, dataset = dataset, - args = [0.01] + args = [0.01], + daemon = True ) with pytest.raises(RuntimeError): new.start() diff --git a/tests/test_thread.py b/tests/test_thread.py index c7c0121..99d8cdd 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -12,6 +12,11 @@ def _dummy_raiseException(x: Exception, delay: float = 0): time.sleep(delay) raise x +def _dummy_iterative(itemCount: int, pTime: float = 0.1, delay: float = 0): + time.sleep(delay) + for i in range(itemCount): + time.sleep(pTime) + @@ -21,11 +26,11 @@ def test_threadCreation(): new = Thread( target = _dummy_target_raiseToPower, args = [4], - kwargs = { 'power': 2 } + kwargs = { 'power': 2 }, + daemon = True ) new.start() - s = new.join() - assert s.status == 'Thread terminated' + assert new.join() assert new.result == 16 def test_threadingThreadParsing(): @@ -37,7 +42,7 @@ def test_threadingThreadParsing(): daemon = True ) new.start() - assert new._thread and new._thread.name == 'testingThread' + assert new.name == 'testingThread' def test_suppressAll(): """This test is for testing that errors are suppressed properly""" @@ -58,6 +63,7 @@ def test_ignoreSpecificError(): target = _dummy_raiseException, args = [ValueError()], ignore_errors = [ValueError], + daemon = True ) new.start() new.join() @@ -69,30 +75,32 @@ def test_ignoreAll(): target = _dummy_raiseException, args = [ValueError()], ignore_errors = [Exception], + daemon = True ) new.start() new.join() assert len(new.errors) == 0 - - - -# >>>>>>>>>> Raising Exceptions <<<<<<<<<< # -def test_raises_notInitializedError(): - """This test should raise ThreadNotInitializedError""" +def test_threadKilling(): + """This test is for testing that threads are killed properly""" new = Thread( - target = _dummy_target_raiseToPower, - args = [4, 2] + target = _dummy_iterative, + args = [5, 0.1, 0] ) + new.start() + new.kill(True) + assert not new.is_alive() + - with pytest.raises(exceptions.ThreadNotInitializedError): - new.result + +# >>>>>>>>>> Raising Exceptions <<<<<<<<<< # def test_raises_stillRunningError(): """This test should raise ThreadStillRunningError""" new = Thread( target = _dummy_target_raiseToPower, - args = [4, 2, 5] + args = [4, 2, 5], + daemon = True ) new.start() @@ -105,7 +113,8 @@ def test_raises_ignoreSpecificError(): target = _dummy_raiseException, args = [FileExistsError()], ignore_errors = [ValueError], - suppress_errors = False + suppress_errors = False, + daemon = True ) with pytest.raises(FileExistsError): new.start() @@ -115,7 +124,8 @@ def test_raises_HookError(): """This test should raise """ new = Thread( target = _dummy_target_raiseToPower, - args = [4, 2] + args = [4, 2], + daemon = True ) def newhook(x: int):