Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-futurist for openSUSE:Factory
checked in at 2025-11-10 19:17:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-futurist (Old)
and /work/SRC/openSUSE:Factory/.python-futurist.new.1980 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-futurist"
Mon Nov 10 19:17:00 2025 rev:17 rq:1316757 version:3.2.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-futurist/python-futurist.changes
2024-10-08 17:25:47.688499462 +0200
+++
/work/SRC/openSUSE:Factory/.python-futurist.new.1980/python-futurist.changes
2025-11-10 19:17:14.304090390 +0100
@@ -1,0 +2,30 @@
+Sun Nov 9 14:34:13 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 3.2.1:
+ * Fix \`shutdown(wait=True)\` to wait for queued tasks
+ * Use consistent validation error message for worker args
+ * Remove sleeps from DynamicThreadPoolExecutor tests
+ * Add DynamicThreadPoolExecutor that resizes itself
+ * Fix ThreadWorker.stop to stop only this worker
+ * Provide insights into the state of ThreadPoolExecutor
+ * Trivial: avoid noqa in \_\_init\_\_.py
+ * add pyproject.toml to support pip 23.1
+ * Drop explicit dependency on python-subunit
+ * tox: Remove basepython
+- switch to singlespec
+
+-------------------------------------------------------------------
+Fri Jul 18 21:46:33 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 3.1.0:
+ * deprecate eventlet based classes (green executors)
+ * Remove Python 3.8 support
+ * Run pyupgrade to clean up Python 2 syntaxes
+ * Use pre-commit hook to run doc8
+ * pre-commit: Bump versions
+ * Declare Python 3.12 support
+ * Replace use of testtools.testcase.TestSkipped
+ * Remove old excludes
+ * reno: Update master for unmaintained/victoria
+
+-------------------------------------------------------------------
Old:
----
_service
futurist-3.0.0.tar.gz
New:
----
futurist-3.2.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-futurist.spec ++++++
--- /var/tmp/diff_new_pack.IYZznH/_old 2025-11-10 19:17:16.052163622 +0100
+++ /var/tmp/diff_new_pack.IYZznH/_new 2025-11-10 19:17:16.064164124 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-futurist
#
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2025 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -17,22 +17,30 @@
Name: python-futurist
-Version: 3.0.0
+Version: 3.2.1
Release: 0
Summary: Useful additions to futures, from the future.
License: Apache-2.0
Group: Development/Languages/Python
URL: https://docs.openstack.org/futurist
-Source0:
https://files.pythonhosted.org/packages/source/f/futurist/futurist-3.0.0.tar.gz
+Source0:
https://files.pythonhosted.org/packages/source/f/futurist/futurist-%{version}.tar.gz
+BuildRequires: %{python_module PrettyTable}
+BuildRequires: %{python_module debtcollector >= 3.0.0}
+BuildRequires: %{python_module eventlet}
+BuildRequires: %{python_module oslotest}
+BuildRequires: %{python_module pip}
+BuildRequires: %{python_module stestr}
+BuildRequires: %{python_module testscenarios}
+BuildRequires: %{python_module wheel}
BuildRequires: openstack-macros
-BuildRequires: python3-PrettyTable
BuildRequires: python3-Sphinx
-BuildRequires: python3-eventlet
BuildRequires: python3-openstackdocstheme
-BuildRequires: python3-oslotest
-BuildRequires: python3-stestr
-BuildRequires: python3-testscenarios
+Requires: python-debtcollector >= 3.0.0
BuildArch: noarch
+%if "python%{python_nodots_ver}" == "%{primary_python}"
+Obsoletes: python3-futurist < %{version}
+%endif
+%python_subpackages
%description
Useful additions to futures, from the future.
@@ -46,26 +54,25 @@
This package contains the Python 3.x module.
%prep
-%autosetup -p1 -n futurist-3.0.0
-%py_req_cleanup
+%autosetup -p1 -n futurist-%{version}
%build
-%{py3_build}
+%pyproject_wheel
# generate html docs
-PBR_VERSION=3.0.0 %sphinx_build -b html doc/source doc/build/html
+PBR_VERSION=%{version} sphinx-build -b html doc/source doc/build/html
# remove the sphinx-build leftovers
rm -r doc/build/html/.{doctrees,buildinfo}
%install
-%{py3_install}
+%pyproject_install
%check
%{openstack_stestr_run}
-%files -n python3-futurist
+%files %{python_files}
%doc doc/build/html README.rst
%license LICENSE
-%{python3_sitelib}/futurist
-%{python3_sitelib}/futurist-*-py?.*.egg-info
+%{python_sitelib}/futurist
+%{python_sitelib}/futurist-%{version}.dist-info
++++++ futurist-3.0.0.tar.gz -> futurist-3.2.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/.pre-commit-config.yaml
new/futurist-3.2.1/.pre-commit-config.yaml
--- old/futurist-3.0.0/.pre-commit-config.yaml 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/.pre-commit-config.yaml 2025-08-29 17:05:26.000000000
+0200
@@ -1,15 +1,9 @@
-# We from the Oslo project decided to pin repos based on the
-# commit hash instead of the version tag to prevend arbitrary
-# code from running in developer's machines. To update to a
-# newer version, run `pre-commit autoupdate` and then replace
-# the newer versions with their commit hash.
-
default_language_version:
python: python3
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: 9136088a246768144165fcc3ecc3d31bb686920a # v3.3.0
+ rev: v5.0.0
hooks:
- id: trailing-whitespace
# Replaces or checks mixed line ending
@@ -27,13 +21,18 @@
- id: debug-statements
- id: check-yaml
files: .*\.(yaml|yml)$
- - repo: local
+ - repo: https://opendev.org/openstack/hacking
+ rev: 7.0.0
+ hooks:
+ - id: hacking
+ additional_dependencies: []
+ - repo: https://github.com/PyCQA/doc8
+ rev: v1.1.2
+ hooks:
+ - id: doc8
+ files: doc/source/.*\.rst$
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.18.0
hooks:
- - id: flake8
- name: flake8
- additional_dependencies:
- - hacking>=6.1.0,<6.2.0
- language: python
- entry: flake8
- files: '^.*\.py$'
- exclude: '^(doc|releasenotes|tools)/.*$'
+ - id: pyupgrade
+ args: [--py3-only]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/AUTHORS new/futurist-3.2.1/AUTHORS
--- old/futurist-3.0.0/AUTHORS 2024-02-23 13:20:16.000000000 +0100
+++ new/futurist-3.2.1/AUTHORS 2025-08-29 17:06:00.000000000 +0200
@@ -1,4 +1,5 @@
98k <[email protected]>
+Afonne-CID <[email protected]>
Akihiro Motoki <[email protected]>
Andreas Jaeger <[email protected]>
Anh Tran <[email protected]>
@@ -8,6 +9,7 @@
Daniel Bengtsson <[email protected]>
Davanum Srinivas <[email protected]>
Dmitriy Ukhlov <[email protected]>
+Dmitry Tantsur <[email protected]>
Dmitry Tantsur <[email protected]>
Doug Hellmann <[email protected]>
Flavio Percoco <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/ChangeLog new/futurist-3.2.1/ChangeLog
--- old/futurist-3.0.0/ChangeLog 2024-02-23 13:20:16.000000000 +0100
+++ new/futurist-3.2.1/ChangeLog 2025-08-29 17:06:00.000000000 +0200
@@ -1,6 +1,42 @@
CHANGES
=======
+3.2.1
+-----
+
+* Fix \`shutdown(wait=True)\` to wait for queued tasks
+
+3.2.0
+-----
+
+* Use consistent validation error message for worker args
+* Remove sleeps from DynamicThreadPoolExecutor tests
+* Add DynamicThreadPoolExecutor that resizes itself
+* Fix ThreadWorker.stop to stop only this worker
+* Provide insights into the state of ThreadPoolExecutor
+* Trivial: avoid noqa in \_\_init\_\_.py
+* add pyproject.toml to support pip 23.1
+* Drop explicit dependency on python-subunit
+* tox: Remove basepython
+
+3.1.1
+-----
+
+
+3.1.0
+-----
+
+* deprecate eventlet based classes (green executors)
+* Remove fallback logic for Python < 3.3
+* Remove Python 3.8 support
+* Run pyupgrade to clean up Python 2 syntaxes
+* Use pre-commit hook to run doc8
+* pre-commit: Bump versions
+* Declare Python 3.12 support
+* Replace use of testtools.testcase.TestSkipped
+* Remove old excludes
+* reno: Update master for unmaintained/victoria
+
3.0.0
-----
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/PKG-INFO new/futurist-3.2.1/PKG-INFO
--- old/futurist-3.0.0/PKG-INFO 2024-02-23 13:20:17.195890400 +0100
+++ new/futurist-3.2.1/PKG-INFO 2025-08-29 17:06:00.858912000 +0200
@@ -1,47 +1,10 @@
-Metadata-Version: 1.2
+Metadata-Version: 2.1
Name: futurist
-Version: 3.0.0
+Version: 3.2.1
Summary: Useful additions to futures, from the future.
Home-page: https://docs.openstack.org/futurist/latest/
Author: OpenStack
Author-email: [email protected]
-License: UNKNOWN
-Description: ========================
- Team and repository tags
- ========================
-
- .. image:: https://governance.openstack.org/tc/badges/futurist.svg
- :target:
https://governance.openstack.org/tc/reference/tags/index.html
-
- .. Change things from this point on
-
- ========
- Futurist
- ========
-
- .. image:: https://img.shields.io/pypi/v/futurist.svg
- :target: https://pypi.org/project/futurist/
- :alt: Latest Version
-
- .. image:: https://img.shields.io/pypi/dm/futurist.svg
- :target: https://pypi.org/project/futurist/
- :alt: Downloads
-
- Code from the future, delivered to you in the **now**. The goal of
this library
- would be to provide a well documented futures
classes/utilities/additions that
- allows for providing a level of transparency in how asynchronous work
gets
- executed. This library currently adds statistics gathering, an eventlet
- executor, a synchronous executor etc.
-
- * Free software: Apache license
- * Documentation: https://docs.openstack.org/futurist/latest/
- * Source: https://opendev.org/openstack/futurist
- * Bugs: https://bugs.launchpad.net/futurist
- * Blueprints: https://blueprints.launchpad.net/futurist
- * Release notes: https://docs.openstack.org/releasenotes/futurist
-
-
-Platform: UNKNOWN
Classifier: Environment :: OpenStack
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
@@ -49,10 +12,47 @@
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation :: CPython
-Requires-Python: >=3.8
+Requires-Python: >=3.9
+License-File: LICENSE
+Requires-Dist: debtcollector>=3.0.0
+
+========================
+Team and repository tags
+========================
+
+.. image:: https://governance.openstack.org/tc/badges/futurist.svg
+ :target: https://governance.openstack.org/tc/reference/tags/index.html
+
+.. Change things from this point on
+
+========
+Futurist
+========
+
+.. image:: https://img.shields.io/pypi/v/futurist.svg
+ :target: https://pypi.org/project/futurist/
+ :alt: Latest Version
+
+.. image:: https://img.shields.io/pypi/dm/futurist.svg
+ :target: https://pypi.org/project/futurist/
+ :alt: Downloads
+
+Code from the future, delivered to you in the **now**. The goal of this library
+would be to provide a well documented futures classes/utilities/additions that
+allows for providing a level of transparency in how asynchronous work gets
+executed. This library currently adds statistics gathering, an eventlet
+executor, a synchronous executor etc.
+
+* Free software: Apache license
+* Documentation: https://docs.openstack.org/futurist/latest/
+* Source: https://opendev.org/openstack/futurist
+* Bugs: https://bugs.launchpad.net/futurist
+* Blueprints: https://blueprints.launchpad.net/futurist
+* Release notes: https://docs.openstack.org/releasenotes/futurist
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/doc/requirements.txt
new/futurist-3.2.1/doc/requirements.txt
--- old/futurist-3.0.0/doc/requirements.txt 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/doc/requirements.txt 2025-08-29 17:05:26.000000000
+0200
@@ -1,3 +1,3 @@
-sphinx>=2.0.0,!=2.1.0 # BSD
+sphinx>=2.0.0 # BSD
openstackdocstheme>=2.2.1 # Apache-2.0
reno>=3.1.0 # Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/doc/source/reference/index.rst
new/futurist-3.2.1/doc/source/reference/index.rst
--- old/futurist-3.0.0/doc/source/reference/index.rst 2024-02-23
13:19:46.000000000 +0100
+++ new/futurist-3.2.1/doc/source/reference/index.rst 2025-08-29
17:05:26.000000000 +0200
@@ -22,6 +22,10 @@
:members:
:special-members: __init__
+.. autoclass:: futurist.DynamicThreadPoolExecutor
+ :members:
+ :special-members: __init__
+
-------
Futures
-------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/__init__.py
new/futurist-3.2.1/futurist/__init__.py
--- old/futurist-3.0.0/futurist/__init__.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/__init__.py 2025-08-29 17:05:26.000000000
+0200
@@ -14,17 +14,33 @@
# Promote accessible items to this module namespace (for easy access).
-from futurist._futures import Future # noqa
-from futurist._futures import GreenFuture # noqa
+from futurist._futures import Future
+from futurist._futures import GreenFuture
-from futurist._futures import CancelledError # noqa
-from futurist._futures import TimeoutError # noqa
+from futurist._futures import CancelledError
+from futurist._futures import TimeoutError
-from futurist._futures import GreenThreadPoolExecutor # noqa
-from futurist._futures import ProcessPoolExecutor # noqa
-from futurist._futures import SynchronousExecutor # noqa
-from futurist._futures import ThreadPoolExecutor # noqa
+from futurist._futures import DynamicThreadPoolExecutor
+from futurist._futures import GreenThreadPoolExecutor
+from futurist._futures import ProcessPoolExecutor
+from futurist._futures import SynchronousExecutor
+from futurist._futures import ThreadPoolExecutor
-from futurist._futures import RejectedSubmission # noqa
+from futurist._futures import RejectedSubmission
-from futurist._futures import ExecutorStatistics # noqa
+from futurist._futures import ExecutorStatistics
+
+
+__all__ = [
+ 'Future', 'GreenFuture',
+
+ 'CancelledError', 'TimeoutError',
+
+ 'GreenThreadPoolExecutor', 'ProcessPoolExecutor',
+ 'SynchronousExecutor', 'ThreadPoolExecutor',
+ 'DynamicThreadPoolExecutor',
+
+ 'RejectedSubmission',
+
+ 'ExecutorStatistics',
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/_futures.py
new/futurist-3.2.1/futurist/_futures.py
--- old/futurist-3.0.0/futurist/_futures.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/_futures.py 2025-08-29 17:05:26.000000000
+0200
@@ -13,21 +13,27 @@
# under the License.
import functools
+import logging
import queue
import threading
+import time
from concurrent import futures as _futures
from concurrent.futures import process as _process
+from debtcollector import removals
+
from futurist import _green
from futurist import _thread
from futurist import _utils
-
TimeoutError = _futures.TimeoutError
CancelledError = _futures.CancelledError
+LOG = logging.getLogger(__name__)
+
+
class RejectedSubmission(Exception):
"""Exception raised when a submitted call is rejected (for some reason)."""
@@ -36,7 +42,7 @@
Future = _futures.Future
-class _Gatherer(object):
+class _Gatherer:
def __init__(self, submit_func, lock_factory, start_before_submit=False):
self._submit_func = submit_func
self._stats_lock = lock_factory()
@@ -101,6 +107,10 @@
It gathers statistics about the submissions executed for post-analysis...
+ Note that this executor never shrinks its thread pool, which will cause
+ the pool to eventually reach its maximum capacity defined by max_workers.
+ Check :py:class:`DynamicThreadPoolExecutor` for an alternative.
+
See: https://docs.python.org/dev/library/concurrent.futures.html
"""
@@ -127,7 +137,7 @@
if max_workers is None:
max_workers = _utils.get_optimal_thread_count()
if max_workers <= 0:
- raise ValueError("Max workers must be greater than zero")
+ raise ValueError("max_workers must be greater than zero")
self._max_workers = max_workers
self._work_queue = queue.Queue()
self._shutdown_lock = threading.RLock()
@@ -146,23 +156,54 @@
"""Accessor to determine if the executor is alive/active."""
return not self._shutdown
+ @property
+ def queue_size(self):
+ """The current size of the queue.
+
+ This value represents the number of tasks that are waiting for a free
+ worker thread.
+ """
+ return self._work_queue.qsize()
+
+ @property
+ def num_workers(self):
+ """The current number of worker threads."""
+ return len(self._workers)
+
+ def get_num_idle_workers(self):
+ """Get the number of currently idle threads.
+
+ A thread is idle if it's waiting for new tasks from the queue.
+
+ This method is required to obtain the shutdown lock it provides
+ an accurate count.
+ """
+ with self._shutdown_lock:
+ return sum(1 for w in self._workers if w.idle)
+
+ def _add_thread(self):
+ w = _thread.ThreadWorker.create_and_register(
+ self, self._work_queue)
+ # Always save it before we start (so that even if we fail
+ # starting it we can correctly join on it).
+ self._workers.append(w)
+ w.start()
+
def _maybe_spin_up(self):
"""Spin up a worker if needed."""
- # Do more advanced idle checks and/or reaping of very idle
- # threads in the future....
if (not self._workers or
len(self._workers) < self._max_workers):
- w = _thread.ThreadWorker.create_and_register(
- self, self._work_queue)
- # Always save it before we start (so that even if we fail
- # starting it we can correctly join on it).
- self._workers.append(w)
- w.start()
+ self._add_thread()
def shutdown(self, wait=True):
with self._shutdown_lock:
if not self._shutdown:
self._shutdown = True
+ if wait:
+ # Wait for all queued work to complete using queue.join()
+ # This will block until all work items have been processed
+ # and task_done() has been called for each
+ self._work_queue.join()
for w in self._workers:
w.stop()
if wait:
@@ -177,6 +218,9 @@
def submit(self, fn, *args, **kwargs):
"""Submit some work to be executed (and gather statistics)."""
+ # NOTE(dtantsur): DynamicThreadPoolExecutor relies on this lock for
+ # its complex logic around thread management. If you ever decide to
+ # remove it, please add a lock there instead.
with self._shutdown_lock:
if self._shutdown:
raise RuntimeError('Can not schedule new futures'
@@ -185,6 +229,174 @@
return self._gatherer.submit(fn, *args, **kwargs)
+class DynamicThreadPoolExecutor(ThreadPoolExecutor):
+ """Executor that creates or removes threads on demand.
+
+ As new work is scheduled on the executor, it will try to keep the
+ proportion of busy threads within the provided range (between 40% and 80%
+ by default). A busy thread is a thread that is not waiting on the
+ task queue.
+
+ Each time a task is submitted, the executor makes a decision whether to
+ grow or shrink the pool. It takes the proportion of the number of busy
+ threads to the total number of threads and compares it to shrink_threshold
+ and grow_threshold.
+
+ Initially, the pool is empty, so submitting a task always result in one
+ new thread. Since min_workers must be greater than zero, at least one
+ thread will always be available after this point.
+
+ Once the proportion of busy threads reaches grow_threshold (e.g. 4 out of 5
+ with the default grow_threshold of 0.8), a new thread is created when a
+ task is submitted. If on submitting a task a proportion of busy threads is
+ below shrink_threshold (e.g. only 2 out of 5), one idle thread is stopped.
+
+ The values of grow_threshold and shrink_threshold are different to prevent
+ the number of threads from oscilating on reaching grow_threshold.
+
+ If threads are not created often in your application, the number of idle
+ threads may stay high for a long time. To avoid it, you can call
+ :py:meth:`.maintain` periodically to keep the number of threads within
+ the thresholds.
+
+ """
+
+ def __init__(self, max_workers=None, check_and_reject=None,
+ min_workers=1, grow_threshold=0.8,
+ shrink_threshold=0.4):
+ """Initializes a thread pool executor.
+
+ :param max_workers: maximum number of workers that can be
+ simultaneously active at the same time, further
+ submitted work will be queued up when this limit
+ is reached.
+ :type max_workers: int
+ :param check_and_reject: a callback function that will be provided
+ two position arguments, the first argument
+ will be this executor instance, and the second
+ will be the number of currently queued work
+ items in this executors backlog; the callback
+ should raise a :py:class:`.RejectedSubmission`
+ exception if it wants to have this submission
+ rejected.
+ :type check_and_reject: callback
+ :param min_workers: the minimum number of workers that can be reached
+ when shrinking the pool. Note that the pool always
+ starts at zero workers and will be smaller than
+ min_workers until enough workers are created.
+ At least one thread is required.
+ :type max_workers: int
+ :param grow_threshold: minimum proportion of busy threads to total
+ threads for the pool to grow.
+ :type grow_threshold: float
+ :param shrink_threshold: maximum proportion of busy threads to total
+ threads for the pool to shrink.
+ :type shrink_threshold: float
+ """
+ super().__init__(max_workers=max_workers,
+ check_and_reject=check_and_reject)
+ if min_workers <= 0:
+ raise ValueError('min_workers must be greater than zero')
+ if max_workers and min_workers >= max_workers:
+ raise ValueError('min_workers must be less than max_workers')
+ self._min_workers = min_workers
+
+ if grow_threshold <= 0 or grow_threshold > 1.0:
+ raise ValueError('grow_threshold must be within (0, 1]')
+ if shrink_threshold < 0 or shrink_threshold >= 1.0:
+ raise ValueError('shrink_threshold must be within [0, 1)')
+ if shrink_threshold >= grow_threshold:
+ raise ValueError(
+ 'shrink_threshold must be less than grow_threshold')
+ self._grow_threshold = grow_threshold
+ self._shrink_threshold = shrink_threshold
+
+ self._dead_workers = []
+
+ def _drop_thread(self):
+ new_workers = []
+ idle_worker = None
+ for i, w in enumerate(self._workers):
+ if w.idle:
+ new_workers = self._workers[i + 1:]
+ idle_worker = w
+ break
+ new_workers.append(w)
+
+ if idle_worker is None:
+ # Should not actually happen but races are possible; do nothing
+ LOG.warning(
+ 'No idle worker thread to delete when shrinking pool %r', self)
+ return False
+
+ w.stop()
+ self._workers = new_workers
+ self._dead_workers.append(w)
+ return True
+
+ def _maybe_spin_up(self):
+ nthreads = self.num_workers
+ if nthreads < self._min_workers:
+ self._add_thread()
+ return True
+
+ # NOTE(dtantsur): here we count the number of threads that are
+ # doing something (i.e. are not waiting on the queue) plus the
+ # number of tasks in the queue. In theory, if there are idle
+ # workers, the queue should be empty. But race conditions are
+ # possible when workers do not pick up tasks quickly enough,
+ # especially in the presence of CPU-bound tasks.
+ idle = self.get_num_idle_workers()
+ busy = (nthreads - idle + self.queue_size) / nthreads
+ if busy >= self._grow_threshold and nthreads < self._max_workers:
+ LOG.debug('Creating a new worker thread for pool %r '
+ '(%d thread(s) idle, queue size %d, total %d thread(s))',
+ self, idle, self.queue_size, nthreads)
+ self._add_thread()
+ return True
+ elif busy <= self._shrink_threshold and nthreads > self._min_workers:
+ LOG.debug('Deleting a worker thread from pool %r '
+ '(%d thread(s) idle, queue size %d, total %d thread(s))',
+ self, idle, self.queue_size, nthreads)
+ return self._drop_thread()
+
+ return False
+
+ def maintain(self):
+ """Keep the number of threads within the expected range.
+
+ If too many idle threads are running, they are deleted.
+ Additionally, deleted workers are joined to free up resources.
+ """
+ # NOTE(dtantsur): this call can potentially run for some time, so
+ # avoid taking shutdown_lock once and holding it for the entire
+ # duration (blocking any new tasks from being added).
+ keep_going = True
+ while keep_going:
+ if self._shutdown:
+ return
+
+ with self._shutdown_lock:
+ keep_going = self._maybe_spin_up()
+
+ time.sleep(0)
+
+ # NOTE(dtantsur): copy the value of _dead_workers to prevent races with
+ # other invocations for maintain or shutdown.
+ with self._shutdown_lock:
+ dead_workers = self._dead_workers
+ self._dead_workers = []
+
+ for w in dead_workers:
+ w.join()
+
+ def shutdown(self, wait=True):
+ super().shutdown(wait=wait)
+ if wait:
+ for w in self._dead_workers:
+ w.join()
+
+
class ProcessPoolExecutor(_process.ProcessPoolExecutor):
"""Executor that uses a process pool to execute calls asynchronously.
@@ -198,14 +410,14 @@
def __init__(self, max_workers=None):
if max_workers is None:
max_workers = _utils.get_optimal_process_count()
- super(ProcessPoolExecutor, self).__init__(max_workers=max_workers)
+ super().__init__(max_workers=max_workers)
if self._max_workers <= 0:
raise ValueError("Max workers must be greater than zero")
self._gatherer = _Gatherer(
# Since our submit will use this gatherer we have to reference
# the parent submit, bound to this instance (which is what we
# really want to use anyway).
- super(ProcessPoolExecutor, self).submit,
+ super().submit,
self.threading.lock_object)
@property
@@ -236,6 +448,10 @@
threading = _thread.Threading()
+ @removals.removed_kwarg('green',
+ message="Eventlet support is deprecated. "
+ "Please migrate your code and stop enforcing "
+ "its usage.")
def __init__(self, green=False, run_work_func=lambda work: work.run()):
"""Synchronous executor constructor.
@@ -299,11 +515,15 @@
return fut
[email protected]_class("GreenFuture",
+ message="Eventlet support is deprecated. "
+ "Please migrate your code and stop using Green "
+ "future.")
class GreenFuture(Future):
__doc__ = Future.__doc__
def __init__(self):
- super(GreenFuture, self).__init__()
+ super().__init__()
if not _utils.EVENTLET_AVAILABLE:
raise RuntimeError('Eventlet is needed to use a green future')
# NOTE(harlowja): replace the built-in condition with a greenthread
@@ -315,6 +535,10 @@
self._condition = _green.threading.condition_object()
[email protected]_class("GreenThreadPoolExecutor",
+ message="Eventlet support is deprecated. "
+ "Please migrate your code and stop using Green "
+ "executor.")
class GreenThreadPoolExecutor(_futures.Executor):
"""Executor that uses a green thread pool to execute calls asynchronously.
@@ -415,7 +639,7 @@
self._pool.waitall()
-class ExecutorStatistics(object):
+class ExecutorStatistics:
"""Holds *immutable* information about a executors executions."""
__slots__ = ['_failures', '_executed', '_runtime', '_cancelled']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/_green.py
new/futurist-3.2.1/futurist/_green.py
--- old/futurist-3.0.0/futurist/_green.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/_green.py 2025-08-29 17:05:26.000000000
+0200
@@ -32,7 +32,7 @@
Queue = greenqueue.Queue
is_monkey_patched = greenpatcher.is_monkey_patched
- class GreenThreading(object):
+ class GreenThreading:
@staticmethod
def event_object(*args, **kwargs):
@@ -58,7 +58,7 @@
is_monkey_patched = lambda mod: False
-class GreenWorker(object):
+class GreenWorker:
def __init__(self, work, work_queue):
self.work = work
self.work_queue = work_queue
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/_thread.py
new/futurist-3.2.1/futurist/_thread.py
--- old/futurist-3.0.0/futurist/_thread.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/_thread.py 2025-08-29 17:05:26.000000000
+0200
@@ -16,7 +16,7 @@
import weakref
-class Threading(object):
+class Threading:
@staticmethod
def event_object(*args, **kwargs):
@@ -37,22 +37,24 @@
_to_be_cleaned = weakref.WeakKeyDictionary()
_dying = False
-_TOMBSTONE = object()
+
+
+class _Stopping(Exception):
+ pass
class ThreadWorker(threading.Thread):
MAX_IDLE_FOR = 1
def __init__(self, executor, work_queue):
- super(ThreadWorker, self).__init__()
+ super().__init__()
self.work_queue = work_queue
self.should_stop = False
self.idle = False
self.daemon = True
# Ensure that when the owning executor gets cleaned up that these
# threads also get shutdown (if they were not already shutdown).
- self.executor_ref = weakref.ref(
- executor, lambda _obj: work_queue.put(_TOMBSTONE))
+ self.executor_ref = weakref.ref(executor, lambda _obj: self.stop())
@classmethod
def create_and_register(cls, executor, work_queue):
@@ -84,28 +86,25 @@
work = self.work_queue.get(True, self.MAX_IDLE_FOR)
except queue.Empty:
if self._is_dying():
- work = _TOMBSTONE
+ raise _Stopping()
self.idle = False
return work
- def stop(self, soon_as_possible=False):
- if soon_as_possible:
- # This will potentially leave unfinished work on queues.
- self.should_stop = True
- self.work_queue.put(_TOMBSTONE)
+ def stop(self):
+ self.should_stop = True
def run(self):
while not self._is_dying():
- work = self._wait_for_work()
try:
- if work is _TOMBSTONE:
- # Ensure any other threads on the same queue also get
- # the tombstone object...
- self.work_queue.put(_TOMBSTONE)
- return
- else:
- work.run()
+ work = self._wait_for_work()
+ except _Stopping:
+ return
+
+ try:
+ work.run()
finally:
+ # Mark task as done for queue.join() support
+ self.work_queue.task_done()
# Avoid any potential (self) references to the work item
# in tracebacks or similar...
del work
@@ -118,7 +117,7 @@
threads_to_wait_for = []
while _to_be_cleaned:
worker, _work_val = _to_be_cleaned.popitem()
- worker.stop(soon_as_possible=True)
+ worker.stop()
threads_to_wait_for.append(worker)
while threads_to_wait_for:
worker = threads_to_wait_for.pop()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/_utils.py
new/futurist-3.2.1/futurist/_utils.py
--- old/futurist-3.0.0/futurist/_utils.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/_utils.py 2025-08-29 17:05:26.000000000
+0200
@@ -29,7 +29,7 @@
EVENTLET_AVAILABLE = False
-class WorkItem(object):
+class WorkItem:
"""A thing to be executed by a executor."""
def __init__(self, future, fn, args, kwargs):
@@ -62,7 +62,7 @@
del exc_type, exc_value, exc_tb
-class Failure(object):
+class Failure:
"""Object that captures a exception (and its associated information)."""
def __init__(self, retain_tb):
@@ -139,7 +139,7 @@
return default
-class Barrier(object):
+class Barrier:
"""A class that ensures active <= 0 occur before unblocking."""
def __init__(self, cond_cls=threading.Condition):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/periodics.py
new/futurist-3.2.1/futurist/periodics.py
--- old/futurist-3.0.0/futurist/periodics.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/periodics.py 2025-08-29 17:05:26.000000000
+0200
@@ -64,7 +64,7 @@
return self.callback(*self.args, **self.kwargs)
-class Watcher(object):
+class Watcher:
"""A **read-only** object representing a periodic callback's activities."""
def __init__(self, metrics, work):
@@ -238,7 +238,7 @@
return how_often + now
-class _Schedule(object):
+class _Schedule:
"""Internal heap-based structure that maintains the schedule/ordering.
This stores a heap composed of the following ``(next_run, index)`` where
@@ -278,7 +278,7 @@
" seconds):\n%s", kind, cb_name, spacing, traceback)
-class _Runner(object):
+class _Runner:
def __init__(self, now_func, retain_traceback=True):
self.now_func = now_func
self.retain_traceback = retain_traceback
@@ -316,7 +316,7 @@
_SCHEDULE_RETRY_EXCEPTIONS = (RuntimeError, futurist.RejectedSubmission)
-class ExecutorFactory(object):
+class ExecutorFactory:
"""Base class for any executor factory."""
shutdown = True
@@ -338,7 +338,7 @@
return self._executor
-class PeriodicWorker(object):
+class PeriodicWorker:
"""Calls a collection of callables periodically (sleeping as needed...).
NOTE(harlowja): typically the :py:meth:`.start` method is executed in a
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/tests/test_executors.py
new/futurist-3.2.1/futurist/tests/test_executors.py
--- old/futurist-3.0.0/futurist/tests/test_executors.py 2024-02-23
13:19:46.000000000 +0100
+++ new/futurist-3.2.1/futurist/tests/test_executors.py 2025-08-29
17:05:26.000000000 +0200
@@ -10,12 +10,14 @@
# License for the specific language governing permissions and limitations
# under the License.
+from concurrent import futures
import threading
import time
+import unittest
+from unittest import mock
from eventlet.green import threading as green_threading
import testscenarios
-from testtools import testcase
import futurist
from futurist import rejection
@@ -38,6 +40,11 @@
time.sleep(wait_secs)
+def delayed_with_result(task_id):
+ time.sleep(0.1)
+ return task_id
+
+
class TestExecutors(testscenarios.TestWithScenarios, base.TestCase):
scenarios = [
('sync', {'executor_cls': futurist.SynchronousExecutor,
@@ -49,16 +56,18 @@
'restartable': False, 'executor_kwargs': {}}),
('thread', {'executor_cls': futurist.ThreadPoolExecutor,
'restartable': False, 'executor_kwargs': {}}),
+ ('thread_dyn', {'executor_cls': futurist.DynamicThreadPoolExecutor,
+ 'restartable': False, 'executor_kwargs': {}}),
('process', {'executor_cls': futurist.ProcessPoolExecutor,
'restartable': False, 'executor_kwargs': {}}),
]
def setUp(self):
- super(TestExecutors, self).setUp()
+ super().setUp()
self.executor = self.executor_cls(**self.executor_kwargs)
def tearDown(self):
- super(TestExecutors, self).tearDown()
+ super().tearDown()
self.executor.shutdown()
self.executor = None
@@ -93,9 +102,26 @@
executor.shutdown()
self.assertRaises(RuntimeError, executor.submit, returns_one)
+ def test_shutdown_waits_for_all_tasks(self):
+ num_tasks = 3
+ futures = []
+ for i in range(num_tasks):
+ future = self.executor.submit(delayed_with_result, i)
+ futures.append(future)
+
+ self.executor.shutdown(wait=True)
+
+ results = []
+ for future in futures:
+ self.assertTrue(future.done())
+ results.append(future.result())
+
+ self.assertEqual(len(results), num_tasks)
+ self.assertEqual(set(results), set(range(num_tasks)))
+
def test_restartable(self):
if not self.restartable:
- raise testcase.TestSkipped("not restartable")
+ raise unittest.SkipTest("not restartable")
else:
executor = self.executor_cls(**self.executor_kwargs)
fut = executor.submit(returns_one)
@@ -155,7 +181,7 @@
]
def setUp(self):
- super(TestRejection, self).setUp()
+ super().setUp()
self.executor = self.executor_cls(**self.executor_kwargs)
self.addCleanup(self.executor.shutdown, wait=True)
@@ -178,3 +204,151 @@
self.assertRaises(futurist.RejectedSubmission,
self.executor.submit, returns_one)
+
+
[email protected](futurist.DynamicThreadPoolExecutor, '_add_thread',
+ # Use the original function behind the scene
+ side_effect=futurist.DynamicThreadPoolExecutor._add_thread,
+ autospec=True)
+class TestDynamicThreadPool(base.TestCase):
+
+ def _new(self, *args, **kwargs):
+ executor = futurist.DynamicThreadPoolExecutor(*args, **kwargs)
+ self.addCleanup(executor.shutdown, wait=True)
+ self.assertEqual(0, executor.queue_size)
+ self.assertEqual(0, executor.num_workers)
+ self.assertEqual(0, executor.get_num_idle_workers())
+ self.assertEqual(0, len(executor._dead_workers))
+ return executor
+
+ def test_stays_at_min_worker(self, mock_add_thread):
+ """Executing tasks sequentially: no growth beyond 1 thread."""
+ executor = self._new(max_workers=3)
+ for _i in range(10):
+ executor.submit(lambda: None).result()
+ self.assertEqual(0, executor.queue_size)
+ self.assertEqual(1, executor.num_workers)
+ self.assertEqual(1, executor.get_num_idle_workers())
+ self.assertEqual(0, len(executor._dead_workers))
+ self.assertEqual(1, mock_add_thread.call_count)
+
+ def test_grow_and_shrink(self, mock_add_thread):
+ """Executing tasks in parallel: grows and shrinks."""
+ executor = self._new(max_workers=10)
+ started = threading.Barrier(11)
+ done = threading.Event()
+ tasks = []
+
+ self.addCleanup(started.abort)
+ self.addCleanup(done.set)
+
+ def task():
+ started.wait()
+ done.wait()
+
+ for _i in range(10):
+ tasks.append(executor.submit(task))
+
+ started.wait() # this ensures that all threads have been started
+ self.assertEqual(0, executor.queue_size)
+ self.assertEqual(10, executor.num_workers)
+ self.assertEqual(0, executor.get_num_idle_workers())
+ self.assertEqual(0, len(executor._dead_workers))
+ self.assertEqual(10, mock_add_thread.call_count)
+
+ done.set() # this allows all threads to stop
+ futures.wait(tasks)
+ executor.maintain()
+ self.assertEqual(0, executor.queue_size)
+ self.assertEqual(1, executor.num_workers)
+ self.assertEqual(1, executor.get_num_idle_workers())
+ self.assertEqual(0, len(executor._dead_workers))
+
+ def test_shutdown_waits_for_queued_tasks(self, mock_add_thread):
+ results = []
+ results_lock = threading.Lock()
+
+ def slow_task(task_id):
+ time.sleep(0.1)
+ with results_lock:
+ results.append(task_id)
+
+ num_tasks = 5
+ executor = self._new(max_workers=2, min_workers=1)
+ for i in range(num_tasks):
+ executor.submit(slow_task, i)
+
+ executor.shutdown(wait=True)
+
+ self.assertEqual(len(results), num_tasks)
+ self.assertEqual(set(results), set(range(num_tasks)))
+
+
[email protected]('futurist._thread.ThreadWorker.create_and_register', autospec=True)
+class TestDynamicThreadPoolMaintain(base.TestCase):
+ def test_ensure_one_worker(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor()
+ executor.maintain()
+ self.assertEqual(1, len(executor._workers))
+ created_worker = mock_create_thread.return_value
+ created_worker.start.assert_called_once_with()
+ created_worker.stop.assert_not_called()
+
+ def test_ensure_min_workers(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor(min_workers=42)
+ executor.maintain()
+ self.assertEqual(42, len(executor._workers))
+ created_worker = mock_create_thread.return_value
+ created_worker.start.assert_called_with()
+ self.assertEqual(42, created_worker.start.call_count)
+ created_worker.stop.assert_not_called()
+
+ def test_too_many_idle_workers(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor(min_workers=42)
+ executor._workers = [mock.Mock(idle=True)] * 100
+ executor.maintain()
+ self.assertEqual(42, len(executor._workers))
+ mock_create_thread.return_value.start.assert_not_called()
+ self.assertEqual(58, executor._workers[0].stop.call_count)
+
+ def test_all_busy_workers(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor(max_workers=100)
+ executor._workers = [mock.Mock(idle=False)] * 100
+ executor.maintain()
+ self.assertEqual(100, len(executor._workers))
+ mock_create_thread.return_value.start.assert_not_called()
+ executor._workers[0].stop.assert_not_called()
+
+ def test_busy_workers_create_more(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor(max_workers=200)
+ executor._workers = [mock.Mock(idle=False)] * 100
+ executor.maintain()
+ # NOTE(dtantsur): once the executor reaches 125 threads, the ratio of
+ # busy to total threads is exactly 100/125=0.8 (the default
+ # grow_threshold). One more thread is created, resulting in 126.
+ self.assertEqual(126, len(executor._workers))
+ self.assertEqual(26, executor.get_num_idle_workers())
+ created_worker = mock_create_thread.return_value
+ created_worker.start.assert_called_with()
+ self.assertEqual(26, created_worker.start.call_count)
+ created_worker.stop.assert_not_called()
+
+ def test_busy_workers_within_range(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor()
+ executor._workers = [mock.Mock(idle=i < 30) for i in range(100)]
+ executor.maintain()
+ self.assertEqual(100, len(executor._workers))
+ mock_create_thread.return_value.start.assert_not_called()
+
+ def test_busy_workers_and_large_queue(self, mock_create_thread):
+ executor = futurist.DynamicThreadPoolExecutor(max_workers=200)
+ executor._workers = [mock.Mock(idle=i < 30) for i in range(100)]
+ for i in range(20):
+ executor._work_queue.put(None)
+ executor.maintain()
+ # NOTE(dtantsur): initial busy ratio is (70+20)/100=0.9. As workers
+ # are added, it reaches (70+20)/113, which is just below 0.8.
+ self.assertEqual(113, len(executor._workers))
+ created_worker = mock_create_thread.return_value
+ created_worker.start.assert_called_with()
+ self.assertEqual(13, created_worker.start.call_count)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/tests/test_periodics.py
new/futurist-3.2.1/futurist/tests/test_periodics.py
--- old/futurist-3.0.0/futurist/tests/test_periodics.py 2024-02-23
13:19:46.000000000 +0100
+++ new/futurist-3.2.1/futurist/tests/test_periodics.py 2025-08-29
17:05:26.000000000 +0200
@@ -428,7 +428,7 @@
def test_create_with_arguments(self):
m = mock.Mock()
- class Object(object):
+ class Object:
@periodics.periodic(0.5)
def func1(self, *args, **kwargs):
m(*args, **kwargs)
@@ -533,7 +533,7 @@
def __init__(self):
self._rejections_count = 0
- super(RejectingExecutor, self).__init__(check_and_reject=self._reject)
+ super().__init__(check_and_reject=self._reject)
class TestPformat(base.TestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/tests/test_waiters.py
new/futurist-3.2.1/futurist/tests/test_waiters.py
--- old/futurist-3.0.0/futurist/tests/test_waiters.py 2024-02-23
13:19:46.000000000 +0100
+++ new/futurist-3.2.1/futurist/tests/test_waiters.py 2025-08-29
17:05:26.000000000 +0200
@@ -48,11 +48,11 @@
]
def setUp(self):
- super(TestWaiters, self).setUp()
+ super().setUp()
self.executor = self.executor_cls(**self.executor_kwargs)
def tearDown(self):
- super(TestWaiters, self).tearDown()
+ super().tearDown()
self.executor.shutdown()
self.executor = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist/waiters.py
new/futurist-3.2.1/futurist/waiters.py
--- old/futurist-3.0.0/futurist/waiters.py 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/futurist/waiters.py 2025-08-29 17:05:26.000000000
+0200
@@ -12,11 +12,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-try:
- from contextlib import ExitStack
-except ImportError:
- from contextlib2 import ExitStack
-
import collections
import contextlib
import functools
@@ -49,7 +44,7 @@
# always acquire the conditions in the same order, no matter what; a way
# to avoid dead-lock).
fs = sorted(fs, key=id)
- with ExitStack() as stack:
+ with contextlib.ExitStack() as stack:
for fut in fs:
stack.enter_context(fut._condition)
yield
@@ -113,7 +108,7 @@
'wait_for_any', timeout=timeout)
-class _AllGreenWaiter(object):
+class _AllGreenWaiter:
"""Provides the event that ``_wait_for_all_green`` blocks on."""
def __init__(self, pending):
@@ -137,7 +132,7 @@
self._decrement_pending()
-class _AnyGreenWaiter(object):
+class _AnyGreenWaiter:
"""Provides the event that ``_wait_for_any_green`` blocks on."""
def __init__(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist.egg-info/PKG-INFO
new/futurist-3.2.1/futurist.egg-info/PKG-INFO
--- old/futurist-3.0.0/futurist.egg-info/PKG-INFO 2024-02-23
13:20:17.000000000 +0100
+++ new/futurist-3.2.1/futurist.egg-info/PKG-INFO 2025-08-29
17:06:00.000000000 +0200
@@ -1,47 +1,10 @@
-Metadata-Version: 1.2
+Metadata-Version: 2.1
Name: futurist
-Version: 3.0.0
+Version: 3.2.1
Summary: Useful additions to futures, from the future.
Home-page: https://docs.openstack.org/futurist/latest/
Author: OpenStack
Author-email: [email protected]
-License: UNKNOWN
-Description: ========================
- Team and repository tags
- ========================
-
- .. image:: https://governance.openstack.org/tc/badges/futurist.svg
- :target:
https://governance.openstack.org/tc/reference/tags/index.html
-
- .. Change things from this point on
-
- ========
- Futurist
- ========
-
- .. image:: https://img.shields.io/pypi/v/futurist.svg
- :target: https://pypi.org/project/futurist/
- :alt: Latest Version
-
- .. image:: https://img.shields.io/pypi/dm/futurist.svg
- :target: https://pypi.org/project/futurist/
- :alt: Downloads
-
- Code from the future, delivered to you in the **now**. The goal of
this library
- would be to provide a well documented futures
classes/utilities/additions that
- allows for providing a level of transparency in how asynchronous work
gets
- executed. This library currently adds statistics gathering, an eventlet
- executor, a synchronous executor etc.
-
- * Free software: Apache license
- * Documentation: https://docs.openstack.org/futurist/latest/
- * Source: https://opendev.org/openstack/futurist
- * Bugs: https://bugs.launchpad.net/futurist
- * Blueprints: https://blueprints.launchpad.net/futurist
- * Release notes: https://docs.openstack.org/releasenotes/futurist
-
-
-Platform: UNKNOWN
Classifier: Environment :: OpenStack
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
@@ -49,10 +12,47 @@
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation :: CPython
-Requires-Python: >=3.8
+Requires-Python: >=3.9
+License-File: LICENSE
+Requires-Dist: debtcollector>=3.0.0
+
+========================
+Team and repository tags
+========================
+
+.. image:: https://governance.openstack.org/tc/badges/futurist.svg
+ :target: https://governance.openstack.org/tc/reference/tags/index.html
+
+.. Change things from this point on
+
+========
+Futurist
+========
+
+.. image:: https://img.shields.io/pypi/v/futurist.svg
+ :target: https://pypi.org/project/futurist/
+ :alt: Latest Version
+
+.. image:: https://img.shields.io/pypi/dm/futurist.svg
+ :target: https://pypi.org/project/futurist/
+ :alt: Downloads
+
+Code from the future, delivered to you in the **now**. The goal of this library
+would be to provide a well documented futures classes/utilities/additions that
+allows for providing a level of transparency in how asynchronous work gets
+executed. This library currently adds statistics gathering, an eventlet
+executor, a synchronous executor etc.
+
+* Free software: Apache license
+* Documentation: https://docs.openstack.org/futurist/latest/
+* Source: https://opendev.org/openstack/futurist
+* Bugs: https://bugs.launchpad.net/futurist
+* Blueprints: https://blueprints.launchpad.net/futurist
+* Release notes: https://docs.openstack.org/releasenotes/futurist
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist.egg-info/SOURCES.txt
new/futurist-3.2.1/futurist.egg-info/SOURCES.txt
--- old/futurist-3.0.0/futurist.egg-info/SOURCES.txt 2024-02-23
13:20:17.000000000 +0100
+++ new/futurist-3.2.1/futurist.egg-info/SOURCES.txt 2025-08-29
17:06:00.000000000 +0200
@@ -10,6 +10,7 @@
LICENSE
MANIFEST.in
README.rst
+pyproject.toml
requirements.txt
setup.cfg
setup.py
@@ -38,6 +39,7 @@
futurist.egg-info/dependency_links.txt
futurist.egg-info/not-zip-safe
futurist.egg-info/pbr.json
+futurist.egg-info/requires.txt
futurist.egg-info/top_level.txt
futurist/tests/__init__.py
futurist/tests/base.py
@@ -45,9 +47,13 @@
futurist/tests/test_periodics.py
futurist/tests/test_waiters.py
releasenotes/notes/add-reno-996dd44974d53238.yaml
+releasenotes/notes/deprecate-eventlet-d96deb8b97930fef.yaml
releasenotes/notes/drop-python27-support-5757997ea990b7ca.yaml
+releasenotes/notes/dynamic-pool-ae8811eecd5f9009.yaml
releasenotes/notes/improve-get-optimal-count-of-max_workers-for-pool-89368859d3b819e0.yaml
releasenotes/notes/prettytable-optional-2c9198a9250427b6.yaml
+releasenotes/notes/remove-py38-de797cad7e34f7be.yaml
+releasenotes/notes/shutdown-wait-for-queued-tasks-81a6b584f69ac62f.yaml
releasenotes/source/conf.py
releasenotes/source/index.rst
releasenotes/source/ocata.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist.egg-info/pbr.json
new/futurist-3.2.1/futurist.egg-info/pbr.json
--- old/futurist-3.0.0/futurist.egg-info/pbr.json 2024-02-23
13:20:17.000000000 +0100
+++ new/futurist-3.2.1/futurist.egg-info/pbr.json 2025-08-29
17:06:00.000000000 +0200
@@ -1 +1 @@
-{"git_version": "4e14db5", "is_release": true}
\ No newline at end of file
+{"git_version": "388ec84", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/futurist.egg-info/requires.txt
new/futurist-3.2.1/futurist.egg-info/requires.txt
--- old/futurist-3.0.0/futurist.egg-info/requires.txt 1970-01-01
01:00:00.000000000 +0100
+++ new/futurist-3.2.1/futurist.egg-info/requires.txt 2025-08-29
17:06:00.000000000 +0200
@@ -0,0 +1 @@
+debtcollector>=3.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/pyproject.toml
new/futurist-3.2.1/pyproject.toml
--- old/futurist-3.0.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100
+++ new/futurist-3.2.1/pyproject.toml 2025-08-29 17:05:26.000000000 +0200
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["pbr>=6.1.1"]
+build-backend = "pbr.build"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/futurist-3.0.0/releasenotes/notes/deprecate-eventlet-d96deb8b97930fef.yaml
new/futurist-3.2.1/releasenotes/notes/deprecate-eventlet-d96deb8b97930fef.yaml
---
old/futurist-3.0.0/releasenotes/notes/deprecate-eventlet-d96deb8b97930fef.yaml
1970-01-01 01:00:00.000000000 +0100
+++
new/futurist-3.2.1/releasenotes/notes/deprecate-eventlet-d96deb8b97930fef.yaml
2025-08-29 17:05:26.000000000 +0200
@@ -0,0 +1,9 @@
+---
+deprecations:
+ - |
+ Eventlet usages are deprecated and the removal of Eventlet from
+ OpenStack `is planned
+
<https://governance.openstack.org/tc//goals/proposed/remove-eventlet.html>`_,
+ for this reason the executors based on Eventlet are deprecated.
+ Start migrating your stack to the threading executor.
+ Please also start considering removing your internal Eventlet usages.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/futurist-3.0.0/releasenotes/notes/dynamic-pool-ae8811eecd5f9009.yaml
new/futurist-3.2.1/releasenotes/notes/dynamic-pool-ae8811eecd5f9009.yaml
--- old/futurist-3.0.0/releasenotes/notes/dynamic-pool-ae8811eecd5f9009.yaml
1970-01-01 01:00:00.000000000 +0100
+++ new/futurist-3.2.1/releasenotes/notes/dynamic-pool-ae8811eecd5f9009.yaml
2025-08-29 17:05:26.000000000 +0200
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Adds a new ``DynamicThreadPoolExecutor`` that can resize (grow or shrink)
+ its pool based on demand. As new work is scheduled on the executor, it will
+ try to keep the proportion of busy threads within the provided range
+ (between 40% and 80% by default).
+
+ The motivation is to provide a scalable alternative to
+ ``GreenThreadPoolExecutor``.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/futurist-3.0.0/releasenotes/notes/remove-py38-de797cad7e34f7be.yaml
new/futurist-3.2.1/releasenotes/notes/remove-py38-de797cad7e34f7be.yaml
--- old/futurist-3.0.0/releasenotes/notes/remove-py38-de797cad7e34f7be.yaml
1970-01-01 01:00:00.000000000 +0100
+++ new/futurist-3.2.1/releasenotes/notes/remove-py38-de797cad7e34f7be.yaml
2025-08-29 17:05:26.000000000 +0200
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Support for Python 3.8 has been removed. Now the minimum python version
+ supported is 3.9 .
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/futurist-3.0.0/releasenotes/notes/shutdown-wait-for-queued-tasks-81a6b584f69ac62f.yaml
new/futurist-3.2.1/releasenotes/notes/shutdown-wait-for-queued-tasks-81a6b584f69ac62f.yaml
---
old/futurist-3.0.0/releasenotes/notes/shutdown-wait-for-queued-tasks-81a6b584f69ac62f.yaml
1970-01-01 01:00:00.000000000 +0100
+++
new/futurist-3.2.1/releasenotes/notes/shutdown-wait-for-queued-tasks-81a6b584f69ac62f.yaml
2025-08-29 17:05:26.000000000 +0200
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ ``shutdown(wait=True)`` abandoned queued work.
+ This fix ensures it blocks until all submitted tasks (running and queued)
+ finishes, consistent with Python's concurrent.futures.ThreadPoolExecutor
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/releasenotes/source/victoria.rst
new/futurist-3.2.1/releasenotes/source/victoria.rst
--- old/futurist-3.0.0/releasenotes/source/victoria.rst 2024-02-23
13:19:46.000000000 +0100
+++ new/futurist-3.2.1/releasenotes/source/victoria.rst 2025-08-29
17:05:26.000000000 +0200
@@ -3,4 +3,4 @@
=============================
.. release-notes::
- :branch: stable/victoria
+ :branch: unmaintained/victoria
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/requirements.txt
new/futurist-3.2.1/requirements.txt
--- old/futurist-3.0.0/requirements.txt 2024-02-23 13:19:46.000000000 +0100
+++ new/futurist-3.2.1/requirements.txt 2025-08-29 17:05:26.000000000 +0200
@@ -0,0 +1 @@
+debtcollector>=3.0.0 # Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/setup.cfg new/futurist-3.2.1/setup.cfg
--- old/futurist-3.0.0/setup.cfg 2024-02-23 13:20:17.199892000 +0100
+++ new/futurist-3.2.1/setup.cfg 2025-08-29 17:06:00.858912000 +0200
@@ -6,7 +6,7 @@
author = OpenStack
author_email = [email protected]
home_page = https://docs.openstack.org/futurist/latest/
-python_requires = >=3.8
+python_requires = >=3.9
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -15,10 +15,10 @@
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3
- Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: Implementation :: CPython
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/test-requirements.txt
new/futurist-3.2.1/test-requirements.txt
--- old/futurist-3.0.0/test-requirements.txt 2024-02-23 13:19:46.000000000
+0100
+++ new/futurist-3.2.1/test-requirements.txt 2025-08-29 17:05:26.000000000
+0200
@@ -1,13 +1,9 @@
# Used for making sure the eventlet executors work.
-eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT
+eventlet>=0.18.2 # MIT
-doc8>=0.6.0 # Apache-2.0
-coverage!=4.4,>=4.0 # Apache-2.0
-python-subunit>=1.0.0 # Apache-2.0/BSD
+coverage>=4.0 # Apache-2.0
oslotest>=3.2.0 # Apache-2.0
stestr>=2.0.0 # Apache-2.0
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT
PrettyTable>=0.7.1 # BSD
-
-pre-commit>=2.6.0 # MIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/futurist-3.0.0/tox.ini new/futurist-3.2.1/tox.ini
--- old/futurist-3.0.0/tox.ini 2024-02-23 13:19:46.000000000 +0100
+++ new/futurist-3.2.1/tox.ini 2025-08-29 17:05:26.000000000 +0200
@@ -1,10 +1,8 @@
[tox]
minversion = 3.18.0
envlist = py3,pep8
-ignore_basepython_conflict = true
[testenv]
-basepython = python3
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
@@ -15,9 +13,10 @@
stestr run --slowest {posargs}
[testenv:pep8]
+deps =
+ pre-commit>=2.6.0 # MIT
commands =
- pre-commit run -a
- doc8 doc/source
+ pre-commit run -a
[testenv:venv]
commands = {posargs}