From a7736298ec1f5824df15a92403852ae6d19c3a1a Mon Sep 17 00:00:00 2001 From: andraxin Date: Sat, 21 Feb 2026 17:33:11 +0100 Subject: [PATCH 1/4] Address jaraco/keyring#709 --- keyring/backend.py | 6 +- keyring/backends/SecretService.py | 4 +- keyring/backends/Windows.py | 4 +- keyring/compat/__init__.py | 9 +-- keyring/compat/context.py | 130 ++++++++++++++++++++++++++++++ keyring/devpi_client.py | 3 +- keyring/errors.py | 2 +- pyproject.toml | 12 ++- 8 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 keyring/compat/context.py diff --git a/keyring/backend.py b/keyring/backend.py index 4f91e16e..e4c60e90 100644 --- a/keyring/backend.py +++ b/keyring/backend.py @@ -12,12 +12,10 @@ import os import typing import warnings - -from jaraco.context import ExceptionTrap -from jaraco.functools import once +from functools import lru_cache as once from . import credentials, errors, util -from .compat import properties +from .compat import ExceptionTrap, properties from .compat.py312 import metadata log = logging.getLogger(__name__) diff --git a/keyring/backends/SecretService.py b/keyring/backends/SecretService.py index 41aa7884..eb02a8dc 100644 --- a/keyring/backends/SecretService.py +++ b/keyring/backends/SecretService.py @@ -1,11 +1,9 @@ import logging from contextlib import closing -from jaraco.context import ExceptionTrap - from .. import backend from ..backend import KeyringBackend -from ..compat import properties +from ..compat import ExceptionTrap, properties from ..credentials import SimpleCredential from ..errors import ( InitError, diff --git a/keyring/backends/Windows.py b/keyring/backends/Windows.py index 110075b2..96e08368 100644 --- a/keyring/backends/Windows.py +++ b/keyring/backends/Windows.py @@ -2,10 +2,8 @@ import logging -from jaraco.context import ExceptionTrap - from ..backend import KeyringBackend -from ..compat import properties +from ..compat import ExceptionTrap, properties from ..credentials import SimpleCredential from ..errors import PasswordDeleteError diff --git a/keyring/compat/__init__.py b/keyring/compat/__init__.py index 22f1e1c4..ee6faae2 100644 --- a/keyring/compat/__init__.py +++ b/keyring/compat/__init__.py @@ -1,7 +1,4 @@ -__all__ = ['properties'] +from . import properties +from .context import ExceptionTrap, suppress - -try: - from jaraco.classes import properties -except ImportError: # pragma: no cover - from . import properties # type: ignore[no-redef] +__all__ = ['ExceptionTrap', 'suppress', 'properties'] diff --git a/keyring/compat/context.py b/keyring/compat/context.py new file mode 100644 index 00000000..afa9c4f4 --- /dev/null +++ b/keyring/compat/context.py @@ -0,0 +1,130 @@ +# from jaraco.context 6.1.0 +from __future__ import annotations + +import contextlib +import functools +import operator + + +class ExceptionTrap: + """ + A context manager that will catch certain exceptions and provide an + indication they occurred. + + >>> with ExceptionTrap() as trap: + ... raise Exception() + >>> bool(trap) + True + + >>> with ExceptionTrap() as trap: + ... pass + >>> bool(trap) + False + + >>> with ExceptionTrap(ValueError) as trap: + ... raise ValueError("1 + 1 is not 3") + >>> bool(trap) + True + >>> trap.value + ValueError('1 + 1 is not 3') + >>> trap.tb + + + >>> with ExceptionTrap(ValueError) as trap: + ... raise Exception() + Traceback (most recent call last): + ... + Exception + + >>> bool(trap) + False + """ + + exc_info = None, None, None + + def __init__(self, exceptions=(Exception,)): + self.exceptions = exceptions + + def __enter__(self): + return self + + @property + def type(self): + return self.exc_info[0] + + @property + def value(self): + return self.exc_info[1] + + @property + def tb(self): + return self.exc_info[2] + + def __exit__(self, *exc_info): + type = exc_info[0] + matches = type and issubclass(type, self.exceptions) + if matches: + self.exc_info = exc_info + return matches + + def __bool__(self): + return bool(self.type) + + def raises(self, func, *, _test=bool): + """ + Wrap func and replace the result with the truth + value of the trap (True if an exception occurred). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> raises = ExceptionTrap(ValueError).raises + + Now decorate a function that always fails. + + >>> @raises + ... def fail(): + ... raise ValueError('failed') + >>> fail() + True + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with ExceptionTrap(self.exceptions) as trap: + func(*args, **kwargs) + return _test(trap) + + return wrapper + + def passes(self, func): + """ + Wrap func and replace the result with the truth + value of the trap (True if no exception). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> passes = ExceptionTrap(ValueError).passes + + Now decorate a function that always fails. + + >>> @passes + ... def fail(): + ... raise ValueError('failed') + + >>> fail() + False + """ + return self.raises(func, _test=operator.not_) + + +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ diff --git a/keyring/devpi_client.py b/keyring/devpi_client.py index dd4b09d3..98404f15 100644 --- a/keyring/devpi_client.py +++ b/keyring/devpi_client.py @@ -1,10 +1,11 @@ import functools import pluggy -from jaraco.context import suppress import keyring.errors +from .compat import suppress + hookimpl = pluggy.HookimplMarker("devpiclient") diff --git a/keyring/errors.py b/keyring/errors.py index ed97cf94..6d6f5eb3 100644 --- a/keyring/errors.py +++ b/keyring/errors.py @@ -34,7 +34,7 @@ class ExceptionRaisedContext: def __init__(self, ExpectedException=Exception): warnings.warn( - "ExceptionRaisedContext is deprecated; use `jaraco.context.ExceptionTrap`", + "ExceptionRaisedContext is deprecated; use `keyring.compat.ExceptionTrap`", DeprecationWarning, stacklevel=2, ) diff --git a/pyproject.toml b/pyproject.toml index 18bb6eb0..495d264c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,6 @@ requires = [ "setuptools>=77", "setuptools_scm[toml]>=3.4.1", - # jaraco/skeleton#174 "coherent.licensed", ] build-backend = "setuptools.build_meta" @@ -26,13 +25,7 @@ classifiers = [ requires-python = ">=3.9" license = "MIT" dependencies = [ - 'pywin32-ctypes>=0.2.0; sys_platform=="win32"', - 'SecretStorage>=3.2; sys_platform=="linux"', - 'jeepney>=0.4.2; sys_platform=="linux"', 'importlib_metadata >= 4.11.4; python_version < "3.12"', - "jaraco.classes", - "jaraco.functools", - "jaraco.context", ] dynamic = ["version"] @@ -40,6 +33,11 @@ dynamic = ["version"] Source = "https://github.com/jaraco/keyring" [project.optional-dependencies] +backend = [ + 'pywin32-ctypes>=0.2.0; sys_platform=="win32"', + 'SecretStorage>=3.2; sys_platform=="linux"', +] + test = [ # upstream "pytest >= 6, != 8.1.*", From c2cfbe90968825c53be83f691548f7d6a67cd401 Mon Sep 17 00:00:00 2001 From: andraxin Date: Sat, 21 Feb 2026 19:08:50 +0100 Subject: [PATCH 2/4] Reuse `librt` "fix" from `jaraco/skeleton@8f3d95e --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 495d264c..84d6c5ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,9 @@ type = [ # upstream "pytest-mypy >= 1.0.1", + ## workaround for python/mypy#20454 + "mypy < 1.19; python_implementation == 'PyPy'", + # local "pygobject-stubs", "shtab", # Optional install for completion From 81a6f39cdedf626f1103ff401fc48439fbd5ad91 Mon Sep 17 00:00:00 2001 From: andraxin Date: Sat, 21 Feb 2026 20:28:15 +0100 Subject: [PATCH 3/4] Ensure new `backend` extra is enabled for testing --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 14243051..3652f0dc 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ commands = pytest {posargs} usedevelop = True extras = + backend test check cover From 9211df4471bed393d6f1f7113759f8b1566e0f2d Mon Sep 17 00:00:00 2001 From: andraxin Date: Sun, 22 Feb 2026 09:22:56 +0100 Subject: [PATCH 4/4] Fix indentation in `tox.ini` --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3652f0dc..7d13db8d 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ commands = pytest {posargs} usedevelop = True extras = - backend + backend test check cover