Skip to content

Added decorators #31

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 17 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ thread = "thread.__main__:app"
[tool.poetry.dependencies]
python = "^3.9"
typer = {extras = ["all"], version = "^0.9.0"}
typing-extensions = "^4.9.0"


[tool.poetry.group.dev.dependencies]
Expand Down
33 changes: 32 additions & 1 deletion src/thread/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
"""
## Thread Library
Documentation at https://thread.ngjx.org


---

Released under the GPG-3 License

Copyright (c) thread.ngjx.org, All rights reserved
"""

"""
This file contains the exports to
```py
import thread
```
"""


# Export Core
from .thread import (
Thread,
ParallelProcessing,
ParallelProcessing
)


from . import (
_types as types,
exceptions
)


# Export decorators
from .decorators import (
threaded
)


# Configuration
from .utils import Settings
31 changes: 31 additions & 0 deletions src/thread/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
## Types

Documentation: https://thread.ngjx.org
"""

from typing import Any, Literal, Callable, Union


# Descriptive Types
Data_In = Any
Data_Out = Any
Overflow_In = Any


# Variable Types
ThreadStatus = Literal[
'Idle',
'Running',
'Invoking hooks',
'Completed',

'Errored',
'Kill Scheduled',
'Killed'
]


# Function types
HookFunction = Callable[[Data_Out], Union[Any, None]]
TargetFunction = Callable[..., Data_Out]
135 changes: 135 additions & 0 deletions src/thread/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
## Decorators

Documentation: https://thread.ngjx.org
"""

from functools import wraps
from .thread import Thread

from ._types import Overflow_In, Data_Out, Data_In
from typing import Callable, Mapping, Sequence, Optional, Union, overload
from typing_extensions import ParamSpec, TypeVar


T = TypeVar('T')
P = ParamSpec('P')
TargetFunction = Callable[P, Data_Out]
NoParamReturn = Callable[P, Thread]
WithParamReturn = Callable[[TargetFunction], NoParamReturn]
FullParamReturn = Callable[P, Thread]
WrappedWithParamReturn = Callable[[TargetFunction], WithParamReturn]


@overload
def threaded(__function: TargetFunction) -> NoParamReturn: ...

@overload
def threaded(
*,
args: Sequence[Data_In] = (),
kwargs: Mapping[str, Data_In] = {},
ignore_errors: Sequence[type[Exception]] = (),
suppress_errors: bool = False,
**overflow_kwargs: Overflow_In
) -> WithParamReturn: ...

@overload
def threaded(
__function: Callable[P, Data_Out],
*,
args: Sequence[Data_In] = (),
kwargs: Mapping[str, Data_In] = {},
ignore_errors: Sequence[type[Exception]] = (),
suppress_errors: bool = False,
**overflow_kwargs: Overflow_In
) -> FullParamReturn: ...


def threaded(
__function: Optional[TargetFunction] = None,
*,
args: Sequence[Data_In] = (),
kwargs: Mapping[str, Data_In] = {},
ignore_errors: Sequence[type[Exception]] = (),
suppress_errors: bool = False,
**overflow_kwargs: Overflow_In
) -> Union[NoParamReturn, WithParamReturn, FullParamReturn]:
"""
Decorate a function to run it in a thread

Parameters
----------
:param __function: The function to run in a thread
:param args: Keyword-Only arguments to pass into `thread.Thread`
:param kwargs: Keyword-Only keyword arguments to pass into `thread.Thread`
:param ignore_errors: Keyword-Only arguments to pass into `thread.Thread`
:param suppress_errors: Keyword-Only arguments to pass into `thread.Thread`
:param **: Keyword-Only arguments to pass into `thread.Thread`

Returns
-------
:return decorator:

Use Case
--------
Now whenever `myfunction` is invoked, it will be executed in a thread and the `Thread` object will be returned

>>> @thread.threaded
>>> def myfunction(*args, **kwargs): ...

>>> myJob = myfunction(1, 2)
>>> type(myjob)
> Thread

You can also pass keyword arguments to change the thread behaviour, it otherwise follows the defaults of `thread.Thread`
>>> @thread.threaded(daemon = True)
>>> def myfunction(): ...

Args will be ordered infront of function-parsed args parsed into `thread.Thread.args`
>>> @thread.threaded(args = (1))
>>> def myfunction(*args):
>>> print(args)
>>>
>>> myfunction(4, 6).get_return_value()
1, 4, 6
"""

if not callable(__function):
def wrapper(func: TargetFunction) -> FullParamReturn:
return threaded(
func,
args = args,
kwargs = kwargs,
ignore_errors = ignore_errors,
suppress_errors = suppress_errors,
**overflow_kwargs
)
return wrapper

overflow_kwargs.update({
'ignore_errors': ignore_errors,
'suppress_errors': suppress_errors
})

kwargs = dict(kwargs)

@wraps(__function)
def wrapped(*parsed_args: P.args, **parsed_kwargs: P.kwargs) -> Thread:
kwargs.update(parsed_kwargs)

processed_args = ( *args, *parsed_args )
processed_kwargs = { i:v for i,v in parsed_kwargs.items() if i not in ['args', 'kwargs'] }

job = Thread(
target = __function,
args = processed_args,
kwargs = processed_kwargs,
**overflow_kwargs
)
job.start()
return job

return wrapped


28 changes: 13 additions & 15 deletions src/thread/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
"""
## Thread Exceptions

Documentation: https://thread.ngjx.org
"""

import traceback
from typing import Any, Optional, Sequence, Tuple


class ThreadErrorBase(Exception):
class ErrorBase(Exception):
"""Base exception class for all errors within this library"""
message: str = 'Something went wrong!'
def __init__(self, message: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
message = message or self.message
super().__init__(message, *args, **kwargs)


class ThreadStillRunningError(ThreadErrorBase):

# THREAD ERRORS #
class ThreadStillRunningError(ErrorBase):
"""Exception class for attempting to invoke a method which requries the thread not be running, but isn't"""
message: str = 'Thread is still running, unable to invoke method. You can wait for the thread to terminate with `Thread.join()` or check with `Thread.is_alive()`'

class ThreadNotRunningError(ThreadErrorBase):
class ThreadNotRunningError(ErrorBase):
"""Exception class for attempting to invoke a method which requires the thread to be running, but isn't"""
message: str = 'Thread is not running, unable to invoke method. Have you ran `Thread.start()`?'

class ThreadNotInitializedError(ThreadErrorBase):
class ThreadNotInitializedError(ErrorBase):
"""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.'

class HookRuntimeError(ThreadErrorBase):
class HookRuntimeError(ErrorBase):
"""Exception class for hook runtime errors"""
message: str = 'Encountered runtime errors in hooks'
count: int = 0
Expand All @@ -44,13 +52,3 @@ def __init__(self, message: Optional[str] = '', extra: Sequence[Tuple[Exception,
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('<<<<<<<<<<')
43 changes: 20 additions & 23 deletions src/thread/thread.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
"""
## Core of thread

```py
class Thread: ...
class ParallelProcessing: ...
```

Documentation: https://thread.ngjx.org
"""

import sys
import time
import signal
import threading
from functools import wraps

from . import exceptions
from .utils.config import Settings
from .utils.algorithm import chunk_split

from functools import wraps
from ._types import ThreadStatus, Data_In, Data_Out, Overflow_In, TargetFunction, HookFunction
from typing import (
Any, List,
Callable, Union, Optional, Literal,
Callable, Optional,
Mapping, Sequence, Tuple
)


ThreadStatus = Literal[
'Idle',
'Running',
'Invoking hooks',
'Completed',

'Errored',
'Kill Scheduled',
'Killed'
]
Data_In = Any
Data_Out = Any
Overflow_In = Any


Threads: set['Thread'] = set()
class Thread(threading.Thread):
"""
Expand All @@ -40,7 +37,7 @@ class Thread(threading.Thread):
"""

status : ThreadStatus
hooks : List[Callable[[Data_Out], Union[Any, None]]]
hooks : List[HookFunction]
returned_value: Data_Out

errors : List[Exception]
Expand All @@ -54,7 +51,7 @@ class Thread(threading.Thread):

def __init__(
self,
target: Callable[..., Data_Out],
target: TargetFunction,
args: Sequence[Data_In] = (),
kwargs: Mapping[str, Data_In] = {},
ignore_errors: Sequence[type[Exception]] = (),
Expand Down Expand Up @@ -103,7 +100,7 @@ def __init__(
)


def _wrap_target(self, target: Callable[..., Data_Out]) -> Callable[..., Data_Out]:
def _wrap_target(self, target: TargetFunction) -> TargetFunction:
"""Wraps the target function"""
@wraps(target)
def wrapper(*args: Any, **kwargs: Any) -> Any:
Expand Down Expand Up @@ -211,7 +208,7 @@ def is_alive(self) -> bool:
return super().is_alive()


def add_hook(self, hook: Callable[[Data_Out], Union[Any, None]]) -> None:
def add_hook(self, hook: HookFunction) -> None:
"""
Adds a hook to the thread
-------------------------
Expand Down Expand Up @@ -347,7 +344,7 @@ class ParallelProcessing:

def __init__(
self,
function: Callable[..., Data_Out],
function: TargetFunction,
dataset: Sequence[Data_In],
max_threads: int = 8,

Expand Down Expand Up @@ -388,7 +385,7 @@ def __init__(

def _wrap_function(
self,
function: Callable[..., Data_Out]
function: TargetFunction
) -> Callable[..., List[Data_Out]]:
@wraps(function)
def wrapper(index: int, data_chunk: Sequence[Data_In], *args: Any, **kwargs: Any) -> List[Data_Out]:
Expand Down
Loading