Skip to content
Open
34 changes: 34 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Stopit (macOS)

on:
push:
branches: [ master ]
pull_request:
branches: '**'

jobs:
build:
env:
LDFLAGS: "-L/usr/local/opt/llvm@14/lib"
CPPFLAGS: "-I/usr/local/opt/llvm@14/include"
runs-on: macos-latest
strategy:
matrix:
os: [macOS]
python-version: ['3.10', '3.11']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install OS dependencies
run: |
python -m pip install --upgrade pip
- name: Install stopit
run: |
pip install "setuptools>=70.0.0" packaging pytest
pip install -e .
- name: Test Stopit
run: |
python tests.py
60 changes: 60 additions & 0 deletions .github/workflows/pyodide.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copied from SymPy https://github.com/sympy/sympy/pull/27183

name: Stopit (Pyodide)

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
pyodide-test:
runs-on: ubuntu-latest
env:
PYODIDE_VERSION: 0.27.0a2
# PYTHON_VERSION and EMSCRIPTEN_VERSION are determined by PYODIDE_VERSION.
# The appropriate versions can be found in the Pyodide repodata.json
# "info" field, or in Makefile.envs:
# https://github.com/pyodide/pyodide/blob/main/Makefile.envs#L2
PYTHON_VERSION: 3.12.1
EMSCRIPTEN_VERSION: 3.1.58
NODE_VERSION: 20
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Set up Emscripten toolchain
uses: mymindstorm/setup-emsdk@v14
with:
version: ${{ env.EMSCRIPTEN_VERSION }}
actions-cache-folder: emsdk-cache

- name: Install pyodide-build
run: pip install pyodide-build

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Set up Pyodide virtual environment and run tests
run: |
# Set up Pyodide virtual environment
pyodide xbuildenv install ${{ env.PYODIDE_VERSION }}
pyodide venv .venv-pyodide

# Activate the virtual environment
source .venv-pyodide/bin/activate

pip install "setuptools>=70.0.0" PyYAML click packaging pytest
python -m pip install --no-build-isolation -e .
- name: Test stopit
run: |
python -c "import sys; print(sys.path); import your_package_name"
python tests.py
28 changes: 28 additions & 0 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Stopit (ubuntu)

on:
push:
branches: [ master ]
pull_request:
branches: '**'

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.12', '3.11', '3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install stopit
run: |
python -m pip install --upgrade pip
pip install "setuptools>=70.0.0" packaging pytest
pip install -e .
- name: Test stopit
run: |
python tests.py
31 changes: 31 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Stopit (Windows)

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
runs-on: windows-latest
strategy:
matrix:
os: [windows]
# "make doctest" on MS Windows fails without showing much of a
# trace of where things went wrong on Python before 3.11.
python-version: ['3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install stopit
run: |
python -m pip install --upgrade pip
pip install "setuptools>=70.0.0" packaging pytest
pip install -e .
- name: Test stopit
run: |
python tests.py
37 changes: 32 additions & 5 deletions src/stopit/signalstop.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,56 @@
from .utils import TimeoutException, BaseTimeout, base_timeoutable


ALARMS = []



def handle_alarms(signum, frame):
global ALARMS
new_alarms = [(ctx, max(0, remaining-1),) for ctx, remaining in ALARMS]
expired = [ctx for ctx, remaining in new_alarms if remaining==0]
ALARMS = [(ctx, remaining,) for ctx, remaining in new_alarms if remaining>0]
if ALARMS:
signal.alarm(1)
for task in expired:
task.stop()
break


class SignalTimeout(BaseTimeout):
"""Context manager for limiting in the time the execution of a block
using signal.SIGALRM Unix signal.

See :class:`stopit.utils.BaseTimeout` for more information
"""

def __init__(self, seconds, swallow_exc=True):
seconds = int(seconds) # alarm delay for signal MUST be int
super(SignalTimeout, self).__init__(seconds, swallow_exc)

def handle_timeout(self, signum, frame):
def stop(self):
self.state = BaseTimeout.TIMED_OUT
self.__class__.exception_source = self
raise TimeoutException('Block exceeded maximum timeout '
'value (%d seconds).' % self.seconds)

# Required overrides
def setup_interrupt(self):
signal.signal(signal.SIGALRM, self.handle_timeout)
signal.alarm(self.seconds)
global ALARMS
for ctx, remaining in ALARMS:
if ctx is self:
return
if len(ALARMS)==0:
signal.signal(signal.SIGALRM, handle_alarms)
signal.alarm(1)
ALARMS.append((self, int(self.seconds),))

def suppress_interrupt(self):
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)
global ALARMS
ALARMS = [(ctx, remaining) for ctx, remaining in ALARMS if ctx is not self]
if len(ALARMS)==0:
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)


class signal_timeoutable(base_timeoutable): #noqa
Expand Down
23 changes: 15 additions & 8 deletions src/stopit/threadstop.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import sys
import threading

from .utils import TimeoutException, BaseTimeout, base_timeoutable
from .utils import LOG, TimeoutException, BaseTimeout, base_timeoutable

if sys.version_info < (3, 7):
tid_ctype = ctypes.c_long
Expand All @@ -30,8 +30,9 @@ def async_raise(target_tid, exception):
"""
# Ensuring and releasing GIL are useless since we're not in C
# gil_state = ctypes.pythonapi.PyGILState_Ensure()
ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid_ctype(target_tid),
ctypes.py_object(exception))
ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(
tid_ctype(target_tid), ctypes.py_object(exception)
)
# ctypes.pythonapi.PyGILState_Release(gil_state)
if ret == 0:
raise ValueError("Invalid thread ID {}".format(target_tid))
Expand All @@ -46,7 +47,13 @@ class ThreadingTimeout(BaseTimeout):

See :class:`stopit.utils.BaseTimeout` for more information
"""

# This class property keep track about who produced the
# exception.

def __init__(self, seconds, swallow_exc=True):
# Ensure that any new handler find a clear
# pointer
super(ThreadingTimeout, self).__init__(seconds, swallow_exc)
self.target_tid = threading.current_thread().ident
self.timer = None # PEP8
Expand All @@ -56,26 +63,26 @@ def stop(self):
caller thread
"""
self.state = BaseTimeout.TIMED_OUT
self.__class__.exception_source = self
async_raise(self.target_tid, TimeoutException)

# Required overrides
def setup_interrupt(self):
"""Setting up the resource that interrupts the block
"""
"""Setting up the resource that interrupts the block"""
self.timer = threading.Timer(self.seconds, self.stop)
self.timer.start()

def suppress_interrupt(self):
"""Removing the resource that interrupts the block
"""
"""Removing the resource that interrupts the block"""
self.timer.cancel()


class threading_timeoutable(base_timeoutable): #noqa
class threading_timeoutable(base_timeoutable): # noqa
"""A function or method decorator that raises a ``TimeoutException`` to
decorated functions that should not last a certain amount of time.
this one uses ``ThreadingTimeout`` context manager.

See :class:`.utils.base_timoutable`` class for further comments.
"""

to_ctx_mgr = ThreadingTimeout
15 changes: 12 additions & 3 deletions src/stopit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,27 @@ def __repr__(self):
return "<{0} in state: {1}>".format(self.__class__.__name__, self.state)

def __enter__(self):
self.__class__.exception_source = None
self.state = BaseTimeout.EXECUTING
self.setup_interrupt()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
exc_src = self.__class__.exception_source
if exc_type is TimeoutException:
if self.state != BaseTimeout.TIMED_OUT:
self.state = BaseTimeout.INTERRUPTED
self.suppress_interrupt()
LOG.warning("Code block execution exceeded {0} seconds timeout".format(self.seconds),
exc_info=(exc_type, exc_val, exc_tb))
return self.swallow_exc
# LOG.warning(
# "Code block execution exceeded {0} seconds timeout".format(
# self.seconds
# ),
# exc_info=(exc_type, exc_val, exc_tb),
#)
if exc_src is self:
if self.swallow_exc:
self.__class__.exception_source = None
return True
else:
if exc_type is None:
self.state = BaseTimeout.EXECUTED
Expand Down
Loading