diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8b6578d16..94b4dafe1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,13 +4,14 @@ on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" - - "[0-9]+.[0-9]+.[0-9].post[0-9]+" - - "[0-9]+.[0-9]+.[0-9][a-b][0-9]+" - - "[0-9]+.[0-9]+.[0-9]rc[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+.post[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+" + - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" jobs: - publish: + build: runs-on: ubuntu-latest + environment: release steps: - uses: actions/checkout@v3 - name: Set up Python @@ -20,8 +21,21 @@ jobs: - name: Install dependencies run: pip install build - name: Create packages - run: python -m build -s -w . + run: python -m build + - name: Archive packages + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist + + publish: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - name: Retrieve packages + uses: actions/download-artifact@v3 - name: Upload packages uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.pypi_password }} diff --git a/apscheduler/schedulers/qt.py b/apscheduler/schedulers/qt.py index 890a44a8f..6762c5c39 100644 --- a/apscheduler/schedulers/qt.py +++ b/apscheduler/schedulers/qt.py @@ -1,24 +1,22 @@ from __future__ import absolute_import +from importlib import import_module +from itertools import product + from apscheduler.schedulers.base import BaseScheduler -try: - from PyQt5.QtCore import QObject, QTimer -except (ImportError, RuntimeError): # pragma: nocover +for version, pkgname in product(range(6, 1, -1), ("PySide", "PyQt")): try: - from PyQt4.QtCore import QObject, QTimer + qtcore = import_module(pkgname + str(version) + ".QtCore") except ImportError: - try: - from PySide6.QtCore import QObject, QTimer # noqa - except ImportError: - try: - from PySide2.QtCore import QObject, QTimer # noqa - except ImportError: - try: - from PySide.QtCore import QObject, QTimer # noqa - except ImportError: - raise ImportError('QtScheduler requires either PyQt5, PyQt4, PySide6, PySide2 ' - 'or PySide installed') + pass + else: + QTimer = qtcore.QTimer + break +else: + raise ImportError( + "QtScheduler requires either PySide/PyQt (v6 to v2) installed" + ) class QtScheduler(BaseScheduler): diff --git a/apscheduler/util.py b/apscheduler/util.py index 64b27d7b6..86c00960c 100644 --- a/apscheduler/util.py +++ b/apscheduler/util.py @@ -6,7 +6,7 @@ from datetime import date, datetime, time, timedelta, tzinfo from calendar import timegm from functools import partial -from inspect import isclass, ismethod +from inspect import isclass, isfunction, ismethod import re import sys @@ -214,28 +214,15 @@ def get_callable_name(func): :rtype: str """ - # the easy case (on Python 3.3+) - if hasattr(func, '__qualname__'): + if ismethod(func): + self = func.__self__ + cls = self if isclass(self) else type(self) + return f"{cls.__qualname__}.{func.__name__}" + elif isclass(func) or isfunction(func): return func.__qualname__ - - # class methods, bound and unbound methods - f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None) - if f_self and hasattr(func, '__name__'): - f_class = f_self if isclass(f_self) else f_self.__class__ - else: - f_class = getattr(func, 'im_class', None) - - if f_class and hasattr(func, '__name__'): - return '%s.%s' % (f_class.__name__, func.__name__) - - # class or class instance - if hasattr(func, '__call__'): - # class - if hasattr(func, '__name__'): - return func.__name__ - + elif hasattr(func, '__call__') and callable(func.__call__): # instance of a class with a __call__ method - return func.__class__.__name__ + return type(func).__qualname__ raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func) diff --git a/docs/modules/triggers/interval.rst b/docs/modules/triggers/interval.rst index e67e23903..c0a2ed299 100644 --- a/docs/modules/triggers/interval.rst +++ b/docs/modules/triggers/interval.rst @@ -54,7 +54,7 @@ You can use ``start_date`` and ``end_date`` to limit the total time in which the The :meth:`~apscheduler.schedulers.base.BaseScheduler.scheduled_job` decorator works nicely too:: - + @sched.scheduled_job('interval', id='my_job_id', hours=2) def job_function(): print("Hello World") @@ -64,5 +64,5 @@ The ``jitter`` option enables you to add a random component to the execution tim multiple servers and don't want them to run a job at the exact same moment or if you want to prevent multiple jobs with similar options from always running concurrently:: - # Run the `job_function` every hour with an extra-delay picked randomly in a [-120,+120] seconds window. + # Run the `job_function` every hour with an extra delay picked randomly between 0 and 120 seconds. sched.add_job(job_function, 'interval', hours=1, jitter=120) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index e407d7203..00594e4db 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -4,6 +4,15 @@ Version history To find out how to migrate your application from a previous version of APScheduler, see the :doc:`migration section `. +UNRELEASED +---------- + +* Ensured consistent support for both PySide and PyQt (v6 to v2) on QtScheduler +* Replaced uses of the deprecated ``pkg_resources`` module with ``importlib.metadata`` +* Fixed scheduling class methods like ``B.methodname`` where the ``B`` class inherits + from class ``A`` and ``methodname`` is a class method of class ``A`` + + 3.10.1 ------ diff --git a/examples/schedulers/qt.py b/examples/schedulers/qt.py index e59b14a45..e43dca27b 100644 --- a/examples/schedulers/qt.py +++ b/examples/schedulers/qt.py @@ -6,22 +6,24 @@ from datetime import datetime import signal import sys +from importlib import import_module +from itertools import product from apscheduler.schedulers.qt import QtScheduler -try: - from PyQt5.QtWidgets import QApplication, QLabel -except ImportError: +for version, pkgname in product(range(6, 1, -1), ("PySide", "PyQt")): try: - from PyQt4.QtGui import QApplication, QLabel + qtwidgets = import_module(pkgname + str(version) + ".QtWidgets") except ImportError: - try: - from PySide6.QtWidgets import QApplication, QLabel - except ImportError: - try: - from PySide2.QtWidgets import QApplication, QLabel - except ImportError: - from PySide.QtGui import QApplication, QLabel + pass + else: + QApplication = qtwidgets.QApplication + QLabel = qtwidgets.QLabel + break +else: + raise ImportError( + "Could not import the QtWidgets module from either PySide or PyQt" + ) def tick(): @@ -44,4 +46,4 @@ def tick(): scheduler.start() # Execution will block here until the user closes the windows or Ctrl+C is pressed. - app.exec_() + app.exec() diff --git a/tests/conftest.py b/tests/conftest.py index 19fba99fa..8bcf56e02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,23 +1,14 @@ -# coding: utf-8 from datetime import datetime import sys import pytest import pytz +from unittest.mock import Mock from apscheduler.job import Job from apscheduler.schedulers.base import BaseScheduler from apscheduler.schedulers.blocking import BlockingScheduler -try: - from unittest.mock import Mock -except ImportError: - from mock import Mock - - -def pytest_ignore_collect(path, config): - return path.basename.endswith('_py35.py') and sys.version_info < (3, 5) - def minpython(*version): version_str = '.'.join([str(num) for num in version]) diff --git a/tests/test_util.py b/tests/test_util.py index 64ad23300..68f93951b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,25 +4,22 @@ from datetime import date, datetime, timedelta, tzinfo from functools import partial, wraps from types import ModuleType +from unittest.mock import Mock import pytest import pytz import six -from apscheduler.job import Job -from apscheduler.util import (asbool, asint, astimezone, check_callable_args, - convert_to_datetime, datetime_ceil, - datetime_repr, datetime_to_utc_timestamp, - get_callable_name, maybe_ref, obj_to_ref, - ref_to_obj, repr_escape, timedelta_seconds, - utc_timestamp_to_datetime) +from apscheduler.util import ( + asbool, asint, astimezone, check_callable_args, + convert_to_datetime, datetime_ceil, + datetime_repr, datetime_to_utc_timestamp, + get_callable_name, iscoroutinefunction_partial, maybe_ref, obj_to_ref, + ref_to_obj, repr_escape, timedelta_seconds, + utc_timestamp_to_datetime, +) from tests.conftest import maxpython, minpython -try: - from unittest.mock import Mock -except ImportError: - from mock import Mock - class DummyClass(object): def meth(self): @@ -45,7 +42,7 @@ def innerclassmeth(cls): pass -class InheritedDummyClass(Job): +class InheritedDummyClass(DummyClass): pass @@ -182,15 +179,16 @@ def test_datetime_repr(input, expected): class TestGetCallableName(object): @pytest.mark.parametrize('input,expected', [ (asint, 'asint'), - (DummyClass.staticmeth, 'DummyClass.staticmeth' if - hasattr(DummyClass, '__qualname__') else 'staticmeth'), + (DummyClass.staticmeth, 'DummyClass.staticmeth'), (DummyClass.classmeth, 'DummyClass.classmeth'), - (DummyClass.meth, 'meth' if sys.version_info[:2] == (3, 2) else 'DummyClass.meth'), + (DummyClass.meth, 'DummyClass.meth'), (DummyClass().meth, 'DummyClass.meth'), (DummyClass, 'DummyClass'), - (DummyClass(), 'DummyClass') + (DummyClass(), 'DummyClass'), + (InheritedDummyClass.classmeth, "InheritedDummyClass.classmeth"), + (DummyClass.InnerDummyClass.innerclassmeth, "DummyClass.InnerDummyClass.innerclassmeth") ], ids=['function', 'static method', 'class method', 'unbounded method', 'bounded method', - 'class', 'instance']) + 'class', 'instance', "class method in inherited", "inner class method"]) def test_inputs(self, input, expected): assert get_callable_name(input) == expected @@ -362,3 +360,22 @@ def wrapper(arg): func() check_callable_args(wrapper, (1,), {}) + + +class TestIsCoroutineFunctionPartial: + @staticmethod + def not_a_coro(x): + pass + + @staticmethod + async def a_coro(x): + pass + + def test_non_coro(self): + assert not iscoroutinefunction_partial(self.not_a_coro) + + def test_coro(self): + assert iscoroutinefunction_partial(self.a_coro) + + def test_coro_partial(self): + assert iscoroutinefunction_partial(partial(self.a_coro, 1)) diff --git a/tests/test_util_py35.py b/tests/test_util_py35.py deleted file mode 100644 index e3d35cdbe..000000000 --- a/tests/test_util_py35.py +++ /dev/null @@ -1,22 +0,0 @@ -from functools import partial - -from apscheduler.util import iscoroutinefunction_partial - - -class TestIsCoroutineFunctionPartial: - @staticmethod - def not_a_coro(x): - pass - - @staticmethod - async def a_coro(x): - pass - - def test_non_coro(self): - assert not iscoroutinefunction_partial(self.not_a_coro) - - def test_coro(self): - assert iscoroutinefunction_partial(self.a_coro) - - def test_coro_partial(self): - assert iscoroutinefunction_partial(partial(self.a_coro, 1))