diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..316758e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# This is an EditorConfig file +# https://EditorConfig.org + +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = tab +indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d98e87b..7344fde 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,4 +1,4 @@ -name: Stopit (macOS) +name: Timed Threads (macOS) on: push: @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [macOS] - python-version: ['3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -25,10 +25,10 @@ jobs: - name: Install OS dependencies run: | python -m pip install --upgrade pip - - name: Install stopit + - name: Install timed_threads run: | pip install "setuptools>=70.0.0" packaging pytest - pip install -e . + pip install -e .[dev] - name: Test Stopit run: | - python tests.py + pytest test diff --git a/.github/workflows/pyodide.yml b/.github/workflows/pyodide.yml index 268854e..3f73f4c 100644 --- a/.github/workflows/pyodide.yml +++ b/.github/workflows/pyodide.yml @@ -1,6 +1,6 @@ # Copied from SymPy https://github.com/sympy/sympy/pull/27183 -name: Stopit (Pyodide) +name: Timed Theads (Pyodide) on: push: @@ -54,10 +54,10 @@ jobs: pip install "setuptools>=70.0.0" PyYAML click packaging pytest pip install --no-build-isolation -v -v -v -e . - - name: Test stopit + - name: Test Timed Threads run: | # Activate the virtual environment . .venv-pyodide/bin/activate echo $PATH - python -c "import sys; print(sys.path); import stopit" - # python tests.py + python -c "import sys; print(sys.path); import timed_threads" + # python test diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 4ae30e2..d5fa25d 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -1,4 +1,4 @@ -name: Stopit (ubuntu) +name: Timed Threads (ubuntu) on: push: @@ -11,18 +11,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.12', '3.11', '3.8', '3.9', '3.10'] + python-version: ['3.13', '3.11', '3.10', '3.12', '3.14'] 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 + - name: Install Timed Threads run: | python -m pip install --upgrade pip pip install "setuptools>=70.0.0" packaging pytest - pip install -e . - - name: Test stopit + pip install -e .[dev] + - name: Test Timed threads run: | - python tests.py + pytest test diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b6538cc..da1f022 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,4 +1,4 @@ -name: Stopit (Windows) +name: Timed Threads (Windows) on: push: @@ -21,11 +21,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install stopit + - name: Install Timed Threads run: | python -m pip install --upgrade pip pip install "setuptools>=70.0.0" packaging pytest - pip install -e . - - name: Test stopit + pip install -e .[dev] + - name: Test Timed Threads run: | - python tests.py + pytest test diff --git a/.gitignore b/.gitignore index 2881d3e..9b7cf1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,36 @@ +*.c +*.cpp +*.egg +*.gz +*.nix +*.py[cod] +*.so +*.stackdump +*~ +.coverage +.idea/ +.mypy_cache +.project +.pydevproject +.settings +.vscode +/.cache +/.pyodide-xbuildenv-* +/.pytest_cache +/.python-version *.egg-info *.pyc *.pyo .DS_Store +/.python-version +/ChangeLog-spell-corrected +/Timed_Threads.egg-info /.pyodide-xbuildenv* /.python-version +/ChangeLog +/ChangeLog.orig +/ChangeLog.rej __pycache__/ build/ dist/ +tmp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..648473f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +default_language_version: + python: python +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + stages: [pre-commit] + exclude: ChangeLog-spell-corrected.diff|mathics/builtin/system.py + - id: end-of-file-fixer + stages: [pre-commit] + exclude: ChangeLog-spell-corrected.diff +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + stages: [pre-commit] +- repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + language_version: python3 + exclude: 'mathics/version.py' + stages: [pre-commit] diff --git a/CHANGES.rst b/CHANGES.rst index 50d9418..0a55750 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ Changes log =========== +2.0.0 - 2025 +------------ + 1.1.2 - 2018-02-09 ------------------ @@ -16,7 +19,7 @@ Changes log 1.1.0 - 2014-05-02 ------------------ -* Added support for TIMER signal based timeout control (Posix OS only) +* Added support for TIMER signal based timeout control (POSIX OS only) * API changes due to new timeout controls * An exhaustive documentation. diff --git a/ChangeLog-spell-corrected.diff b/ChangeLog-spell-corrected.diff new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..60d4844 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +# A GNU Makefile to run various tasks - compatibility for us old-timers. + +# Note: This makefile include remake-style target comments. +# These comments before the targets start with #: +# remake --tasks to shows the targets and the comments + +GIT2CL ?= ./admin-tools/git2cl +PYTHON ?= python3 +PIP ?= $(PYTHON) -m pip +RM ?= rm +PIP_INSTALL_OPTS ?= + +.PHONY: all build \ + check clean \ + develop \ + pytest \ + rmChangeLog \ + test + +#: Default target - same as "develop" +all: develop + +#: Set up to run from the source tree +develop: + $(PIP) install -e .$(PIP_INSTALL_OPTS) + +#: Install timed_threads +install: + $(PYTHON) -m pip install -e . + +#: Run unit tests and Mathics doctest +check: pytest + +#: Same as check +test: check + +#: Remove derived files +clean: + @find . -name *.pyc -type f -delete; + +#: Run py.test tests. Use environment variable "o" for pytest options +pytest: + $(PYTHON) -m pytest test $o + +#: Remove ChangeLog +rmChangeLog: + $(RM) ChangeLog || true + +#: Create a ChangeLog from git via git log and git2cl +ChangeLog: rmChangeLog + git log --pretty --numstat --summary | $(GIT2CL) >$@ + patch -p0 ChangeLog < ChangeLog-spell-corrected.diff diff --git a/README.rst b/README.rst index 1d6eb66..d525c39 100644 --- a/README.rst +++ b/README.rst @@ -1,35 +1,29 @@ -====== -stopit -====== +============== +Timed Threads +============== -Raise asynchronous exceptions in other threads, control the timeout of -blocks or callables with two context managers and two decorators. +Adds the ability to set relative elapsed time deadlines on asynchronous threads, and allows one thread to stop another by means of raising an exception. -.. attention:: API Changes +Note that due to the GIL lock in Python 3.14, this does not give us any more concurrency. - Users of 1.0.0 should upgrade their source code: +It is hoped that in the future, in conjunction with an implementation of Python that does not have a global GIL lock there can be and implementation that improves concurrency of hardware threads. - - ``stopit.Timeout`` is renamed ``stopit.ThreadingTimeout`` - - ``stopit.timeoutable`` is renamed ``stopit.threading_timeoutable`` +The main motivation of this module is to support TimedConstraint in the open-source implementation of Mathematica, called Mathics3. - Explications follow below... - -.. contents:: Overview ======== This module provides: -- a function that raises an exception in another thread, including the main +- a function that allows an exception to be raised in another thread, including the main thread. -- two context managers that may stop its inner block activity on timeout. +- context managers that may stop its inner block activity on timeout. -- two decorators that may stop its decorated callables on timeout. +- decorators that may stop its decorated callables on timeout. -Developed and tested with CPython 2.6, 2.7, 3.3 and 3.4 on MacOSX. Should work -on any OS (xBSD, Linux, Windows) except when explicitly mentioned. +Developed and tested with CPython 3.10+ using Python's threading model. .. note:: @@ -40,28 +34,17 @@ on any OS (xBSD, Linux, Windows) except when explicitly mentioned. Installation ============ -Using ``stopit`` in your application ------------------------------------- - -Both work identically: - .. code:: bash - easy_install stopit - pip install stopit + pip install Timed-Threads + -Developing ``stopit`` ---------------------- +To install from source: .. code:: bash - # You should prefer forking if you have a Github account - git clone https://github.com/glenfant/stopit.git - cd stopit - python setup.py develop + pip install -e . - # Does it work for you ? - python setup.py test Public API ========== @@ -69,17 +52,17 @@ Public API Exception --------- -``stopit.TimeoutException`` +``timed_threads.TimeoutException`` ........................... -A ``stopit.TimeoutException`` may be raised in a timeout context manager +A ``timed_threads.TimeoutException`` may be raised in a timeout context manager controlled block. This exception may be propagated in your application at the end of execution of the context manager controlled block, see the ``swallow_ex`` parameter of the context managers. -Note that the ``stopit.TimeoutException`` is always swallowed after the +Note that the ``timed_threads.TimeoutException`` is always swallowed after the execution of functions decorated with ``xxx_timeoutable(...)``. Anyway, you may catch this exception **within** the decorated function. @@ -97,7 +80,7 @@ Threading based resources executing a ``time.sleep(20)``, the asynchronous exception is effective **after** its execution. -``stopit.async_raise`` +``timed_threads.async_raise`` ...................... A function that raises an arbitrary exception in another thread @@ -110,7 +93,7 @@ A function that raises an arbitrary exception in another thread - ``exception`` is the exception class or object to raise in the thread. -``stopit.ThreadingTimeout`` +``timed_threads.ThreadingTimeout`` ........................... A context manager that "kills" its inner block execution that exceeds the @@ -121,13 +104,13 @@ provided time. - ``seconds`` is the number of seconds allowed to the execution of the context managed block. -- ``swallow_exc`` : if ``False``, the possible ``stopit.TimeoutException`` will +- ``swallow_exc`` : if ``False``, the possible ``timed_threads.TimeoutException`` will be re-raised when quitting the context managed block. **Attention**: a ``True`` value does not swallow other potential exceptions. **Methods and attributes** -of a ``stopit.ThreadingTimeout`` context manager. +of a ``timed_threads.ThreadingTimeout`` context manager. .. list-table:: :header-rows: 1 @@ -162,7 +145,7 @@ of a ``stopit.ThreadingTimeout`` context manager. * - ``.INTERRUPTED`` - The code under timeout control may itself raise explicit - ``stopit.TimeoutException`` for any application logic reason that may + ``timed_threads.TimeoutException`` for any application logic reason that may occur. This intentional exit can be spotted from outside the timeout controlled block with this state value. @@ -176,9 +159,9 @@ A typical usage: .. code:: python - import stopit + import timed_threads # ... - with stopit.ThreadingTimeout(10) as to_ctx_mgr: + with timed_threads.ThreadingTimeout(10) as to_ctx_mgr: assert to_ctx_mgr.state == to_ctx_mgr.EXECUTING # Something potentially very long but which # ... @@ -207,13 +190,13 @@ indicating (if ``True``) that the block executed normally: # Yes, the code under timeout control completed # Objects it created or changed may be considered consistent -``stopit.threading_timeoutable`` +``timed_threads.threading_timeoutable`` ................................ A decorator that kills the function or method it decorates, if it does not return within a given time frame. -``stopit.threading_timeoutable([default [, timeout_param]])`` +``timed_threads.threading_timeoutable([default [, timeout_param]])`` - ``default`` is the value to be returned by the decorated function or method of when its execution timed out, to notify the caller code that the function @@ -224,7 +207,7 @@ return within a given time frame. .. code:: python - @stopit.threading_timeoutable(default='not finished') + @timed_threads.threading_timeoutable(default='not finished') def infinite_loop(): # As its name says... @@ -238,7 +221,7 @@ return within a given time frame. .. code:: python - @stopit.threading_timeoutable(timeout_param='my_timeout') + @timed_threads.threading_timeoutable(timeout_param='my_timeout') def some_slow_function(a, b, timeout='whatever'): # As its name says... @@ -264,9 +247,9 @@ Signaling based resources Using signaling based resources will **not** work under Windows or any OS that's not based on Unix. -``stopit.SignalTimeout`` and ``stopit.signal_timeoutable`` have exactly the +``timed_threads.SignalTimeout`` and ``timed_threads.signal_timeoutable`` have exactly the same API as their respective threading based resources, namely -`stopit.ThreadingTimeout`_ and `stopit.threading_timeoutable`_. +`timed_threads.ThreadingTimeout`_ and `timed_threads.threading_timeoutable`_. See the `comparison chart`_ that warns on the more or less subtle differences between the `Threading based resources`_ and the `Signaling based resources`_. @@ -274,14 +257,14 @@ between the `Threading based resources`_ and the `Signaling based resources`_. Logging ------- -The ``stopit`` named logger emits a warning each time a block of code +The ``timed_threads`` named logger emits a warning each time a block of code execution exceeds the associated timeout. To turn logging off, just: .. code:: python import logging - stopit_logger = logging.getLogger('stopit') - stopit_logger.setLevel(logging.ERROR) + timed_threads_logger = logging.getLogger('timed_threads') + timed_threads_logger.setLevel(logging.ERROR) .. _comparison chart: @@ -369,7 +352,7 @@ managed block or decorated functions are executing. Threading timeout control as mentioned in `Threading based resources`_ does not work as expected when used in the context of a gevent worker. -See the discussion in `Issue 13 `_ for more details. +See the discussion in `Issue 13 `_ for more details. Tests and demos =============== @@ -377,19 +360,19 @@ Tests and demos .. code:: pycon >>> import threading - >>> from stopit import async_raise, TimeoutException + >>> from timed_threads import async_raise, TimeoutException In a real application, you should either use threading based timeout resources: .. code:: pycon - >>> from stopit import ThreadingTimeout as Timeout, threading_timeoutable as timeoutable #doctest: +SKIP + >>> from timed_threads import ThreadingTimeout as Timeout, threading_timeoutable as timeoutable #doctest: +SKIP Or the POSIX signal based resources: .. code:: pycon - >>> from stopit import SignalTimeout as Timeout, signal_timeoutable as timeoutable #doctest: +SKIP + >>> from timed_threads import SignalTimeout as Timeout, signal_timeoutable as timeoutable #doctest: +SKIP Let's define some utilities: @@ -633,17 +616,18 @@ Links ===== Source code (clone, fork, ...) - https://github.com/glenfant/stopit + https://github.com/glenfant/timed_threads Issues tracker - https://github.com/glenfant/stopit/issues + https://github.com/glenfant/timed_threads/issues PyPI - https://pypi.python.org/pypi/stopit + https://pypi.python.org/pypi/timed_threads Credits ======= +- This is a modernization for newer Python of Gilles Lenfant `stopit `_ with some slight changes. - This is a NIH package which is mainly a theft of `Gabriel Ahtune's recipe `_ with tests, minor improvements and refactorings, documentation and setuptools diff --git a/admin-tools/git2cl b/admin-tools/git2cl new file mode 100755 index 0000000..5d9b90c --- /dev/null +++ b/admin-tools/git2cl @@ -0,0 +1,407 @@ +#!/usr/bin/perl + +# Copyright (C) 2007, 2008 Simon Josefsson +# Copyright (C) 2007 Luis Mondesi +# * calls git directly. To use it just: +# cd ~/Project/my_git_repo; git2cl > ChangeLog +# * implements strptime() +# * fixes bugs in $comment parsing +# - copy input before we remove leading spaces +# - skip "merge branch" statements as they don't +# have information about files (i.e. we never +# go into $state 2) +# - behaves like a pipe/filter if input is given from the CLI +# else it calls git log by itself +# +# The functions mywrap, last_line_len, wrap_log_entry are derived from +# the cvs2cl tool, see : +# Copyright (C) 2001,2002,2003,2004 Martyn J. Pearce +# Copyright (C) 1999 Karl Fogel +# +# git2cl is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# git2cl is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with git2cl; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA. + +=head1 NAME + +git2cl - tool to convert git logs to GNU ChangeLog + +=head1 SYNOPSIS + +git2cl > ChangeLog + +If you don't want git2cl to invoke git log internally, you can use it +as a pipe. +It needs a git log generated with --pretty --numstat and --summary. +You can use it as follows: + + git log --pretty --numstat --summary | git2cl > ChangeLog + +=head1 DESCRIPTION + +This is a quick'n'dirty tool to convert git logs to GNU ChangeLog +format. + +The tool invokes `git log` internally unless you pipe a log to it. +Thus, typically you would use it as follows: + +=head1 SEE ALSO + +Output format specification: + + +=head1 AUTHORS + +git2cl is developed by Simon Josefsson + and Luis Mondesi + +=cut + +use strict; +use POSIX qw(strftime); +use Text::Wrap qw(wrap); +use FileHandle; + +use constant EMPTY_LOG_MESSAGE => '*** empty log message ***'; + +# this is a helper hash for stptime. +# Assumes you are calling 'git log ...' with LC_ALL=C +my %month = ( + 'Jan'=>0, + 'Feb'=>1, + 'Mar'=>2, + 'Apr'=>3, + 'May'=>4, + 'Jun'=>5, + 'Jul'=>6, + 'Aug'=>7, + 'Sep'=>8, + 'Oct'=>9, + 'Nov'=>10, + 'Dec'=>11, +); + +my $fh = new FileHandle; + +sub key_ready +{ + my ($rin, $nfd); + vec($rin, fileno(STDIN), 1) = 1; + return $nfd = select($rin, undef, undef, 0); +} + +sub strptime { + my $str = shift; + return undef if not defined $str; + + # we are parsing this format + # Fri Oct 26 00:42:56 2007 -0400 + # to these fields + # sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1 + # Luis Mondesi + my @date; + if ($str =~ /([[:alpha:]]{3})\s+([[:alpha:]]{3})\s+([[:digit:]]{1,2})\s+([[:digit:]]{1,2}):([[:digit:]]{1,2}):([[:digit:]]{1,2})\s+([[:digit:]]{4})/){ + push(@date,$6,$5,$4,$3,$month{$2},($7 - 1900),-1,-1,-1); + } else { + die ("Cannot parse date '$str'\n'"); + } + return @date; +} + +sub mywrap { + my ($indent1, $indent2, @text) = @_; + # If incoming text looks preformatted, don't get clever + my $text = Text::Wrap::wrap($indent1, $indent2, @text); + if ( grep /^\s+/m, @text ) { + return $text; + } + my @lines = split /\n/, $text; + $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e; + $lines[0] =~ s/^$indent1\s+/$indent1/; + s/^$indent2\s+/$indent2/ + for @lines[1..$#lines]; + my $newtext = join "\n", @lines; + $newtext .= "\n" + if substr($text, -1) eq "\n"; + return $newtext; +} + +sub last_line_len { + my $files_list = shift; + my @lines = split (/\n/, $files_list); + my $last_line = pop (@lines); + return length ($last_line); +} + +# A custom wrap function, sensitive to some common constructs used in +# log entries. +sub wrap_log_entry { + my $text = shift; # The text to wrap. + my $left_pad_str = shift; # String to pad with on the left. + + # These do NOT take left_pad_str into account: + my $length_remaining = shift; # Amount left on current line. + my $max_line_length = shift; # Amount left for a blank line. + + my $wrapped_text = ''; # The accumulating wrapped entry. + my $user_indent = ''; # Inherited user_indent from prev line. + + my $first_time = 1; # First iteration of the loop? + my $suppress_line_start_match = 0; # Set to disable line start checks. + + my @lines = split (/\n/, $text); + while (@lines) # Don't use `foreach' here, it won't work. + { + my $this_line = shift (@lines); + chomp $this_line; + + if ($this_line =~ /^(\s+)/) { + $user_indent = $1; + } + else { + $user_indent = ''; + } + + # If it matches any of the line-start regexps, print a newline now... + if ($suppress_line_start_match) + { + $suppress_line_start_match = 0; + } + elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/) + || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/) + || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/) + || ($this_line =~ /^(\s+)(\S+)/) + || ($this_line =~ /^(\s*)- +/) + || ($this_line =~ /^()\s*$/) + || ($this_line =~ /^(\s*)\*\) +/) + || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/)) + { + $length_remaining = $max_line_length - (length ($user_indent)); + } + + # Now that any user_indent has been preserved, strip off leading + # whitespace, so up-folding has no ugly side-effects. + $this_line =~ s/^\s*//; + + # Accumulate the line, and adjust parameters for next line. + my $this_len = length ($this_line); + if ($this_len == 0) + { + # Blank lines should cancel any user_indent level. + $user_indent = ''; + $length_remaining = $max_line_length; + } + elsif ($this_len >= $length_remaining) # Line too long, try breaking it. + { + # Walk backwards from the end. At first acceptable spot, break + # a new line. + my $idx = $length_remaining - 1; + if ($idx < 0) { $idx = 0 }; + while ($idx > 0) + { + if (substr ($this_line, $idx, 1) =~ /\s/) + { + my $line_now = substr ($this_line, 0, $idx); + my $next_line = substr ($this_line, $idx); + $this_line = $line_now; + + # Clean whitespace off the end. + chomp $this_line; + + # The current line is ready to be printed. + $this_line .= "\n${left_pad_str}"; + + # Make sure the next line is allowed full room. + $length_remaining = $max_line_length - (length ($user_indent)); + + # Strip next_line, but then preserve any user_indent. + $next_line =~ s/^\s*//; + + # Sneak a peek at the user_indent of the upcoming line, so + # $next_line (which will now precede it) can inherit that + # indent level. Otherwise, use whatever user_indent level + # we currently have, which might be none. + my $next_next_line = shift (@lines); + if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) { + $next_line = $1 . $next_line if (defined ($1)); + # $length_remaining = $max_line_length - (length ($1)); + $next_next_line =~ s/^\s*//; + } + else { + $next_line = $user_indent . $next_line; + } + if (defined ($next_next_line)) { + unshift (@lines, $next_next_line); + } + unshift (@lines, $next_line); + + # Our new next line might, coincidentally, begin with one of + # the line-start regexps, so we temporarily turn off + # sensitivity to that until we're past the line. + $suppress_line_start_match = 1; + + last; + } + else + { + $idx--; + } + } + + if ($idx == 0) + { + # We bottomed out because the line is longer than the + # available space. But that could be because the space is + # small, or because the line is longer than even the maximum + # possible space. Handle both cases below. + + if ($length_remaining == ($max_line_length - (length ($user_indent)))) + { + # The line is simply too long -- there is no hope of ever + # breaking it nicely, so just insert it verbatim, with + # appropriate padding. + $this_line = "\n${left_pad_str}${this_line}"; + } + else + { + # Can't break it here, but may be able to on the next round... + unshift (@lines, $this_line); + $length_remaining = $max_line_length - (length ($user_indent)); + $this_line = "\n${left_pad_str}"; + } + } + } + else # $this_len < $length_remaining, so tack on what we can. + { + # Leave a note for the next iteration. + $length_remaining = $length_remaining - $this_len; + + if ($this_line =~ /\.$/) + { + $this_line .= " "; + $length_remaining -= 2; + } + else # not a sentence end + { + $this_line .= " "; + $length_remaining -= 1; + } + } + + # Unconditionally indicate that loop has run at least once. + $first_time = 0; + + $wrapped_text .= "${user_indent}${this_line}"; + } + + # One last bit of padding. + $wrapped_text .= "\n"; + + return $wrapped_text; +} + +# main + +my @date; +my $author; +my @files; +my $comment; + +my $state; # 0-header 1-comment 2-files +my $done = 0; + +$state = 0; + +# if reading from STDIN, we assume that we are +# getting git log as input +if (key_ready()) +{ + + #my $dummyfh; # don't care about writing + #($fh,$dummyfh) = FileHandle::pipe; + $fh->fdopen(*STDIN, 'r'); +} +else +{ + $fh->open("LC_ALL=C git log --pretty --numstat --summary|") + or die("Cannot execute git log...$!\n"); +} + +while (my $_l = <$fh>) { + #print STDERR "debug ($state, " . (@date ? (strftime "%Y-%m-%d", @date) : "") . "): `$_'\n"; + if ($state == 0) { + if ($_l =~ m,^Author: (.*),) { + $author = $1; + } + if ($_l =~ m,^Date: (.*),) { + @date = strptime($1); + } + $state = 1 if ($_l =~ m,^$, and $author and (@date+0>0)); + } elsif ($state == 1) { + # * modifying our input text is a bad choice + # let's make a copy of it first, then we remove spaces + # * if we meet a "merge branch" statement, we need to start + # over and find a real entry + # Luis Mondesi + my $_s = $_l; + $_s =~ s/^ //g; + if ($_s =~ m/^Merge branch/) + { + $state=0; + next; + } + $comment = $comment . $_s; + $state = 2 if ($_l =~ m,^$,); + } elsif ($state == 2) { + if ($_l =~ m,^([0-9]+)\t([0-9]+)\t(.*)$,) { + push @files, $3; + } + $done = 1 if ($_l =~ m,^$,); + } + + if ($done) { + print (strftime "%Y-%m-%d $author\n\n", @date); + + my $files = join (", ", @files); + $files = mywrap ("\t", "\t", "* $files"), ": "; + + if (index($comment, EMPTY_LOG_MESSAGE) > -1 ) { + $comment = "[no log message]\n"; + } + + my $files_last_line_len = 0; + $files_last_line_len = last_line_len($files) + 1; + my $msg = wrap_log_entry($comment, "\t", 69-$files_last_line_len, 69); + + $msg =~ s/[ \t]+\n/\n/g; + + print "$files: $msg\n"; + + @date = (); + $author = ""; + @files = (); + $comment = ""; + + $state = 0; + $done = 0; + } +} + +if (@date + 0) +{ + print (strftime "%Y-%m-%d $author\n\n", @date); + my $msg = wrap_log_entry($comment, "\t", 69, 69); + $msg =~ s/[ \t]+\n/\n/g; + print "\t* $msg\n"; +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..eaf20d6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "Timed-Threads" +description = "Absolute time deadlines and thread cancelling for Python asynchronous threads" +requires-python = ">=3.10" +readme = "README.rst" +license = {text = "MIT"} +maintainers = [ + {name = "Mathics3 Group"}, +] +classifiers = [ + "Topic :: Utilities", + "Programming Language :: Python", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Development Status :: 5 - Production/Stable" +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/Mathics3/python-timed-threads/" + +[project.optional-dependencies] +dev = [ + "pytest", +] + +[tool.setuptools] +packages = [ + "timed_threads", +] + +[tool.setuptools.dynamic] +version = {attr = "timed_threads.version.__version__"} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8dd4c40 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,37 @@ +# Recommended flake8 settings while editing zoom, we use Black for the final +# linting/say in how code is formatted +# +# pip install flake8 flake8-bugbear +# +# This will warn/error on things that black does not fix, on purpose. + +# This config file MUST be ASCII to prevent weird flake8 dropouts + +[flake8] +# max-line-length setting: NO we do not want everyone writing 120-character lines! +# We are setting the maximum line length big here because there are longer +# lines allowed by black in some cases that are forbidden by flake8. Since +# black has the final say about code formatting issues, this setting is here to +# make sure that flake8 doesn't fail the build on longer lines allowed by +# black. +max-line-length = 120 +max-complexity = 12 +select = E,F,W,C,B,B9 +ignore = + # E123 closing bracket does not match indentation of opening bracket's line + E123 + # E203 whitespace before ':' (Not PEP8 compliant, Python Black) + E203 + # E501 line too long (82 > 79 characters) (replaced by B950 from flake8-bugbear, + # https://github.com/PyCQA/flake8-bugbear) + E501 + # W503 line break before binary operator (Not PEP8 compliant, Python Black) + W503 + # W504 line break after binary operator (Not PEP8 compliant, Python Black) + W504 + # C901 function too complex - since many of zz9 functions are too complex with a lot + # of if branching + C901 + # module level import not at top of file. This is too restrictive. Can't even have a + # docstring higher. + E402 diff --git a/setup.py b/setup.py deleted file mode 100644 index 1f17bf4..0000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -""" -====== -stopit -====== - -Raise asynchronous exceptions in other thread, control the timeout of blocks -or callables with a context manager or a decorator. -""" - -import os -from setuptools import setup, find_packages - -version = '1.1.3.dev0' - -this_directory = os.path.abspath(os.path.dirname(__file__)) - - -def read(*names): - return open(os.path.join(this_directory, *names), 'r').read().strip() - - -long_description = read('README.rst') + '\n\n' + read('CHANGES.rst') - -setup(name='stopit', - version=version, - description="Timeout control decorator and context managers, raise any exception in another thread", - long_description=long_description, - classifiers=[ - "Topic :: Utilities", - "Programming Language :: Python", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers", - "Development Status :: 5 - Production/Stable" - ], - keywords='threads timeout', - author='Gilles Lenfant', - author_email='gilles.lenfant@gmail.com', - url='https://github.com/glenfant/stopit/', - license='GPLv3', - packages=find_packages('src'), - package_dir={'': 'src'}, - test_suite='tests.suite', - zip_safe=False - ) diff --git a/src/stopit/__init__.py b/src/stopit/__init__.py deleted file mode 100644 index 6ca0180..0000000 --- a/src/stopit/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -""" -====== -stopit -====== - -Public resources from ``stopit`` -""" - -import pkg_resources - -from .utils import LOG, TimeoutException -from .threadstop import ThreadingTimeout, async_raise, threading_timeoutable -from .signalstop import SignalTimeout, signal_timeoutable - -# PEP 396 style version marker -try: - __version__ = pkg_resources.get_distribution(__name__).version -except: - LOG.warning("Could not get the package version from pkg_resources") - __version__ = 'unknown' - -__all__ = ( - 'ThreadingTimeout', 'async_raise', 'threading_timeoutable', - 'SignalTimeout', 'signal_timeoutable' -) diff --git a/test/test_basic.py b/test/test_basic.py new file mode 100644 index 0000000..a1b5f6e --- /dev/null +++ b/test/test_basic.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +import os +import time + +from timed_threads import ( + SignalTimeout, + ThreadingTimeout, + TimeoutException, + signal_timeoutable, + threading_timeoutable, +) + +# We run twice the same doctest with two distinct sets of globs +# This one is for testing signals based timeout control +signaling_globs = {"Timeout": SignalTimeout, "timeoutable": signal_timeoutable} + +# And this one is for testing threading based timeout control +threading_globs = {"Timeout": ThreadingTimeout, "timeoutable": threading_timeoutable} + + +handlers = ( + ( + SignalTimeout, + ThreadingTimeout, + ) + if os.name == "posix" + else (ThreadingTimeout,) +) + + +def aware_wait(duration): + remaining = duration * 100 + t_start = time.time() + while remaining > 0: + time.sleep(0.01) + if time.time() - t_start > duration: + return 0 + remaining = remaining - 1 + return 0 + + +def check_nest(t1, t2, duration, HandlerClass): + try: + with HandlerClass(t1, swallow_exc=False) as to_ctx_mgr1: + assert to_ctx_mgr1.state == to_ctx_mgr1.EXECUTING + with HandlerClass(t2, swallow_exc=False) as to_ctx_mgr2: + assert to_ctx_mgr2.state == to_ctx_mgr2.EXECUTING + aware_wait(duration) + return "success" + except TimeoutException: + if HandlerClass.exception_source is to_ctx_mgr1: + return "outer" + elif HandlerClass.exception_source is to_ctx_mgr2: + return "inner" + else: + print(HandlerClass.exception_source) + return "unknown source" + + +def check_nest_swallow(t1, t2, duration, HandlerClass): + with HandlerClass(t1) as to_ctx_mgr1: + assert to_ctx_mgr1.state == to_ctx_mgr1.EXECUTING + with HandlerClass(t2) as to_ctx_mgr2: + assert to_ctx_mgr2.state == to_ctx_mgr2.EXECUTING + aware_wait(duration) + return "success" + return "inner" + return "outer" + + +def test_nested_long_inner(): + for handler in handlers: + assert check_nest(1.0, 10.0, 5.0, handler) == "outer" + assert check_nest_swallow(1.0, 10.0, 5.0, handler) == "outer" + + +def test_nested_success(): + for handler in handlers: + assert check_nest_swallow(5.0, 10.0, 1.0, handler) == "success" + assert check_nest(5.0, 10.0, 1.0, handler) == "success" + + +def test_nested_long_outer(): + for handler in handlers: + assert check_nest(10.0, 1.0, 5.0, handler) == "inner" + assert check_nest_swallow(10.0, 1.0, 5.0, handler) == "inner" + + +if os.name == "posix": # Other OS have no support for signal.SIGALRM + + def test_signal_handler(): + for settime, expect_time in ((-1.5, 1), (0, 1), (0.5, 1), (3, 3), (3.2, 4)): + assert SignalTimeout(settime).seconds == expect_time diff --git a/tests.py b/tests.py deleted file mode 100644 index 1989c44..0000000 --- a/tests.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -import time -import doctest -import os -import unittest - -from stopit import ( - TimeoutException, - ThreadingTimeout, - threading_timeoutable, - SignalTimeout, - signal_timeoutable, -) - -# We run twice the same doctest with two distinct sets of globs -# This one is for testing signals based timeout control -signaling_globs = {"Timeout": SignalTimeout, "timeoutable": signal_timeoutable} - -# And this one is for testing threading based timeout control -threading_globs = {"Timeout": ThreadingTimeout, "timeoutable": threading_timeoutable} - - -class TestNesting(unittest.TestCase): - handlers = ( - ( - SignalTimeout, - ThreadingTimeout, - ) - if os.name == "posix" - else (ThreadingTimeout,) - ) - - def aware_wait(self, duration): - remaining = duration * 100 - t_start = time.time() - while remaining > 0: - time.sleep(0.01) - if time.time() - t_start > duration: - return 0 - remaining = remaining - 1 - return 0 - - def check_nest(self, t1, t2, duration, HandlerClass): - try: - with HandlerClass(t1, swallow_exc=False) as to_ctx_mgr1: - assert to_ctx_mgr1.state == to_ctx_mgr1.EXECUTING - with HandlerClass(t2, swallow_exc=False) as to_ctx_mgr2: - assert to_ctx_mgr2.state == to_ctx_mgr2.EXECUTING - self.aware_wait(duration) - return "success" - except TimeoutException: - if HandlerClass.exception_source is to_ctx_mgr1: - return "outer" - elif HandlerClass.exception_source is to_ctx_mgr2: - return "inner" - else: - print(HandlerClass.exception_source) - return "unknown source" - - def check_nest_swallow(self, t1, t2, duration, HandlerClass): - with HandlerClass(t1) as to_ctx_mgr1: - assert to_ctx_mgr1.state == to_ctx_mgr1.EXECUTING - with HandlerClass(t2) as to_ctx_mgr2: - assert to_ctx_mgr2.state == to_ctx_mgr2.EXECUTING - self.aware_wait(duration) - return "success" - return "inner" - return "outer" - - def test_nested_long_inner(self): - for handler in self.handlers: - self.assertEqual(self.check_nest(1.0, 10.0, 5.0, handler), "outer") - self.assertEqual(self.check_nest_swallow(1.0, 10.0, 5.0, handler), "outer") - - def test_nested_success(self): - for handler in self.handlers: - self.assertEqual( - self.check_nest_swallow(5.0, 10.0, 1.0, handler), "success" - ) - self.assertEqual(self.check_nest(5.0, 10.0, 1.0, handler), "success") - - def test_nested_long_outer(self): - for handler in self.handlers: - self.assertEqual(self.check_nest(10.0, 1.0, 5.0, handler), "inner") - self.assertEqual(self.check_nest_swallow(10.0, 1.0, 5.0, handler), "inner") - - if os.name == "posix": # Other OS have no support for signal.SIGALRM - def test_signal_handler(self): - for settime, expect_time in ((-1.5, 1), (0, 1), (0.5, 1), (3, 3), - (3.2, 4)): - self.assertEqual(SignalTimeout(settime).seconds, expect_time) - - -def suite(): # Func for setuptools.setup(test_suite=xxx) - test_suite = unittest.TestSuite() - test_suite.addTest(doctest.DocFileSuite("README.rst", globs=threading_globs)) - if os.name == "posix": # Other OS have no support for signal.SIGALRM - test_suite.addTest(doctest.DocFileSuite("README.rst", globs=signaling_globs)) - return test_suite - - -if __name__ == "__main__": - unittest.TextTestRunner(verbosity=2).run(suite()) - unittest.main() diff --git a/timed_threads/__init__.py b/timed_threads/__init__.py new file mode 100644 index 0000000..04b30c2 --- /dev/null +++ b/timed_threads/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +timed_threads +""" + +from timed_threads.utils import LOG, TimeoutException +from timed_threads.threadstop import ( + ThreadingTimeout, + async_raise, + threading_timeoutable, +) +from timed_threads.signalstop import SignalTimeout, signal_timeoutable +from timed_threads.version import __version__ + + +__all__ = ( + "LOG", + "SignalTimeout", + "ThreadingTimeout", + "TimeoutException", + "__version__", + "async_raise", + "signal_timeoutable", + "threading_timeoutable", +) diff --git a/src/stopit/signalstop.py b/timed_threads/signalstop.py similarity index 62% rename from src/stopit/signalstop.py rename to timed_threads/signalstop.py index 5e421b4..5db1c1b 100644 --- a/src/stopit/signalstop.py +++ b/timed_threads/signalstop.py @@ -1,31 +1,40 @@ # -*- coding: utf-8 -*- """ -================= -stopit.signalstop -================= - Control the timeout of blocks or callables with a context manager or a decorator. Based on the use of signal.SIGALRM """ import signal +import time +from typing import List -from .utils import TimeoutException, BaseTimeout, base_timeoutable - +from timed_threads.utils import BaseTimeout, TimeoutException, base_timeoutable -ALARMS = [] +ALARMS: List[tuple] = [] 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] + new_alarms = [ + ( + ctx, + max(0, drop_dead_time), + ) + for ctx, drop_dead_time in ALARMS + ] + current_time = time.time() + expired = [ + ctx for ctx, drop_dead_time in new_alarms if current_time >= drop_dead_time + ] + ALARMS = [ ( - ctx, - remaining, - ) for ctx, remaining in new_alarms - if remaining > 0] + ctx, + drop_dead_time, + ) + for ctx, drop_dead_time in new_alarms + if current_time < drop_dead_time + ] if ALARMS: signal.alarm(1) for task in expired: @@ -37,22 +46,23 @@ 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 + See :class:`timed_threads.utils.BaseTimeout` for more information """ def __init__(self, seconds, swallow_exc=True): - # The alarm delay for a SIGALARM MUST be an integer # greater than 1. Round up non-integer values. - seconds = max(1, int(seconds + 0.99)) + self.seconds = max(1, int(seconds + 0.99)) + self.drop_dead_time = time.time() + self.seconds - super(SignalTimeout, self).__init__(seconds, swallow_exc) + super(SignalTimeout, self).__init__(self.seconds, swallow_exc) def stop(self): self.state = BaseTimeout.TIMED_OUT self.__class__.exception_source = self - raise TimeoutException('Block exceeded maximum timeout ' - 'value (%d seconds).' % self.seconds) + raise TimeoutException( + "Block exceeded maximum timeout " "value (%d seconds)." % self.seconds + ) # Required overrides def setup_interrupt(self): @@ -71,11 +81,13 @@ def setup_interrupt(self): # Register our self.seconds value in the global # ALARMS registry. - ALARMS.append((self, int(self.seconds),)) + ALARMS.append((self, self.drop_dead_time)) def suppress_interrupt(self): global ALARMS - ALARMS = [(ctx, remaining) for ctx, remaining in ALARMS if ctx is not self] + ALARMS = [ + (ctx, drop_dead_time) for ctx, drop_dead_time in ALARMS if ctx is not self + ] if len(ALARMS) == 0: signal.alarm(0) signal.signal(signal.SIGALRM, signal.SIG_DFL) @@ -88,4 +100,5 @@ class signal_timeoutable(base_timeoutable): # noqa See :class:`.utils.base_timoutable`` class for further comments. """ + to_ctx_mgr = SignalTimeout diff --git a/src/stopit/threadstop.py b/timed_threads/threadstop.py similarity index 87% rename from src/stopit/threadstop.py rename to timed_threads/threadstop.py index fa59878..51f9402 100644 --- a/src/stopit/threadstop.py +++ b/timed_threads/threadstop.py @@ -1,23 +1,15 @@ # -*- coding: utf-8 -*- """ -================= -stopit.threadstop -================= - Raise asynchronous exceptions in other thread, control the timeout of blocks or callables with a context manager or a decorator. """ import ctypes -import sys import threading -from .utils import LOG, TimeoutException, BaseTimeout, base_timeoutable +from timed_threads.utils import BaseTimeout, TimeoutException, base_timeoutable -if sys.version_info < (3, 7): - tid_ctype = ctypes.c_long -else: - tid_ctype = ctypes.c_ulong +tid_ctype = ctypes.c_ulong def async_raise(target_tid, exception): @@ -35,7 +27,7 @@ def async_raise(target_tid, exception): ) # ctypes.pythonapi.PyGILState_Release(gil_state) if ret == 0: - raise ValueError("Invalid thread ID {}".format(target_tid)) + raise ValueError(f"Invalid thread ID {target_tid}") elif ret > 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(tid_ctype(target_tid), None) raise SystemError("PyThreadState_SetAsyncExc failed") @@ -45,7 +37,7 @@ class ThreadingTimeout(BaseTimeout): """Context manager for limiting in the time the execution of a block using asynchronous threads launching exception. - See :class:`stopit.utils.BaseTimeout` for more information + See :class:`timed_threads.utils.BaseTimeout` for more information """ # This class property keep track about who produced the diff --git a/src/stopit/utils.py b/timed_threads/utils.py similarity index 82% rename from src/stopit/utils.py rename to timed_threads/utils.py index a45164d..1e10cf6 100644 --- a/src/stopit/utils.py +++ b/timed_threads/utils.py @@ -1,35 +1,14 @@ # -*- coding: utf-8 -*- """ -============ -stopit.utils -============ - -Misc utilities and common resources +Miscellaneous utilities and common resources. """ import functools import logging -import sys +from logging import NullHandler # Custom logger -LOG = logging.getLogger(name='stopit') - -if sys.version_info < (2, 7): - class NullHandler(logging.Handler): - """Copied from Python 2.7 to avoid getting `No handlers could be found - for logger "xxx"` http://bugs.python.org/issue16539 - """ - def handle(self, record): - pass - - def emit(self, record): - pass - - def createLock(self): - self.lock = None # noqa -else: - from logging import NullHandler - +LOG = logging.getLogger(name="timed_threads") LOG.addHandler(NullHandler()) @@ -37,6 +16,7 @@ class TimeoutException(Exception): """Raised when the block under context management takes longer to complete than the allowed maximum timeout value. """ + pass @@ -50,6 +30,7 @@ class BaseTimeout(object): structure. ``True`` (default) if you just want to check the execution of the block with the ``state`` attribute of the context manager. """ + # Possible values for the ``state`` attribute, self explanative EXECUTED, EXECUTING, TIMED_OUT, INTERRUPTED, CANCELED = range(5) @@ -59,14 +40,17 @@ def __init__(self, seconds, swallow_exc=True): self.state = BaseTimeout.EXECUTED def __bool__(self): - return self.state in (BaseTimeout.EXECUTED, BaseTimeout.EXECUTING, BaseTimeout.CANCELED) + return self.state in ( + BaseTimeout.EXECUTED, + BaseTimeout.EXECUTING, + BaseTimeout.CANCELED, + ) __nonzero__ = __bool__ # Python 2.x def __repr__(self): - """Debug helper - """ - return "<{0} in state: {1}>".format(self.__class__.__name__, self.state) + """Debug helper""" + return f"<{self.__class__.__name__} in state: {self.state}>" def __enter__(self): self.__class__.exception_source = None @@ -85,7 +69,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): # self.seconds # ), # exc_info=(exc_type, exc_val, exc_tb), - #) + # ) if exc_src is self: if self.swallow_exc: self.__class__.exception_source = None @@ -98,19 +82,17 @@ def __exit__(self, exc_type, exc_val, exc_tb): def cancel(self): """In case in the block you realize you don't need anymore - limitation""" + limitation""" self.state = BaseTimeout.CANCELED self.suppress_interrupt() # Methods must be provided by subclasses def suppress_interrupt(self): - """Removes/neutralizes the feature that interrupts the executed block - """ + """Removes/neutralizes the feature that interrupts the executed block""" raise NotImplementedError def setup_interrupt(self): - """Installs/initializes the feature that interrupts the executed block - """ + """Installs/initializes the feature that interrupts the executed block""" raise NotImplementedError @@ -138,9 +120,10 @@ class base_timeoutable(object): # noqa the ``to_ctx_mgr`` with a timeout context manager class which in turn must subclasses of above ``BaseTimeout`` class. """ + to_ctx_mgr = None - def __init__(self, default=None, timeout_param='timeout'): + def __init__(self, default=None, timeout_param="timeout"): self.default, self.timeout_param = default, timeout_param def __call__(self, func): @@ -155,4 +138,5 @@ def wrapper(*args, **kwargs): return result else: return func(*args, **kwargs) + return wrapper diff --git a/timed_threads/version.py b/timed_threads/version.py new file mode 100644 index 0000000..1fdb531 --- /dev/null +++ b/timed_threads/version.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +# This file is suitable for sourcing inside POSIX shell as +# well as importing into Python. That's why there is no +# space around "=" below. +# fmt: off +__version__="2.0.0dev0" # noqa