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):