From e110fb26844ef0065150ebee393d7f9697120714 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 31 Aug 2022 20:29:07 -0400 Subject: [PATCH] Add copy of properties module from jaraco.classes 3.2. --- keyring/_compat.py | 7 ++ keyring/_properties_compat.py | 168 +++++++++++++++++++++++++++++ keyring/backend.py | 2 +- keyring/backends/SecretService.py | 2 +- keyring/backends/Windows.py | 2 +- keyring/backends/chainer.py | 2 +- keyring/backends/kwallet.py | 2 +- keyring/backends/libsecret.py | 2 +- keyring/backends/macOS/__init__.py | 2 +- 9 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 keyring/_compat.py create mode 100644 keyring/_properties_compat.py diff --git a/keyring/_compat.py b/keyring/_compat.py new file mode 100644 index 00000000..ae837c27 --- /dev/null +++ b/keyring/_compat.py @@ -0,0 +1,7 @@ +__all__ = ['properties'] + + +try: + from jaraco.compat import properties # pragma: no-cover +except ImportError: + from . import _properties_compat as properties # pragma: no-cover diff --git a/keyring/_properties_compat.py b/keyring/_properties_compat.py new file mode 100644 index 00000000..948f4783 --- /dev/null +++ b/keyring/_properties_compat.py @@ -0,0 +1,168 @@ +# from jaraco.classes 3.2.2 + +class NonDataProperty: + """Much like the property builtin, but only implements __get__, + making it a non-data property, and can be subsequently reset. + + See http://users.rcn.com/python/download/Descriptor.htm for more + information. + + >>> class X(object): + ... @NonDataProperty + ... def foo(self): + ... return 3 + >>> x = X() + >>> x.foo + 3 + >>> x.foo = 4 + >>> x.foo + 4 + """ + + def __init__(self, fget): + assert fget is not None, "fget cannot be none" + assert callable(fget), "fget must be callable" + self.fget = fget + + def __get__(self, obj, objtype=None): + if obj is None: + return self + return self.fget(obj) + + +class classproperty: + """ + Like @property but applies at the class level. + + + >>> class X(metaclass=classproperty.Meta): + ... val = None + ... @classproperty + ... def foo(cls): + ... return cls.val + ... @foo.setter + ... def foo(cls, val): + ... cls.val = val + >>> X.foo + >>> X.foo = 3 + >>> X.foo + 3 + >>> x = X() + >>> x.foo + 3 + >>> X.foo = 4 + >>> x.foo + 4 + + Setting the property on an instance affects the class. + + >>> x.foo = 5 + >>> x.foo + 5 + >>> X.foo + 5 + >>> vars(x) + {} + >>> X().foo + 5 + + Attempting to set an attribute where no setter was defined + results in an AttributeError: + + >>> class GetOnly(metaclass=classproperty.Meta): + ... @classproperty + ... def foo(cls): + ... return 'bar' + >>> GetOnly.foo = 3 + Traceback (most recent call last): + ... + AttributeError: can't set attribute + + It is also possible to wrap a classmethod or staticmethod in + a classproperty. + + >>> class Static(metaclass=classproperty.Meta): + ... @classproperty + ... @classmethod + ... def foo(cls): + ... return 'foo' + ... @classproperty + ... @staticmethod + ... def bar(): + ... return 'bar' + >>> Static.foo + 'foo' + >>> Static.bar + 'bar' + + *Legacy* + + For compatibility, if the metaclass isn't specified, the + legacy behavior will be invoked. + + >>> class X: + ... val = None + ... @classproperty + ... def foo(cls): + ... return cls.val + ... @foo.setter + ... def foo(cls, val): + ... cls.val = val + >>> X.foo + >>> X.foo = 3 + >>> X.foo + 3 + >>> x = X() + >>> x.foo + 3 + >>> X.foo = 4 + >>> x.foo + 4 + + Note, because the metaclass was not specified, setting + a value on an instance does not have the intended effect. + + >>> x.foo = 5 + >>> x.foo + 5 + >>> X.foo # should be 5 + 4 + >>> vars(x) # should be empty + {'foo': 5} + >>> X().foo # should be 5 + 4 + """ + + class Meta(type): + def __setattr__(self, key, value): + obj = self.__dict__.get(key, None) + if type(obj) is classproperty: + return obj.__set__(self, value) + return super().__setattr__(key, value) + + def __init__(self, fget, fset=None): + self.fget = self._ensure_method(fget) + self.fset = fset + fset and self.setter(fset) + + def __get__(self, instance, owner=None): + return self.fget.__get__(None, owner)() + + def __set__(self, owner, value): + if not self.fset: + raise AttributeError("can't set attribute") + if type(owner) is not classproperty.Meta: + owner = type(owner) + return self.fset.__get__(None, owner)(value) + + def setter(self, fset): + self.fset = self._ensure_method(fset) + return self + + @classmethod + def _ensure_method(cls, fn): + """ + Ensure fn is a classmethod or staticmethod. + """ + needs_method = not isinstance(fn, (classmethod, staticmethod)) + return classmethod(fn) if needs_method else fn diff --git a/keyring/backend.py b/keyring/backend.py index 7d14f054..d19bc09a 100644 --- a/keyring/backend.py +++ b/keyring/backend.py @@ -12,7 +12,7 @@ from .py310compat import metadata from . import credentials, errors, util -from jaraco.classes import properties +from ._compat import properties log = logging.getLogger(__name__) diff --git a/keyring/backends/SecretService.py b/keyring/backends/SecretService.py index ea471747..7edb238d 100644 --- a/keyring/backends/SecretService.py +++ b/keyring/backends/SecretService.py @@ -2,7 +2,7 @@ import logging from .. import backend -from jaraco.classes import properties +from .._compat import properties from ..backend import KeyringBackend from ..credentials import SimpleCredential from ..errors import ( diff --git a/keyring/backends/Windows.py b/keyring/backends/Windows.py index 85abe42a..a686d895 100644 --- a/keyring/backends/Windows.py +++ b/keyring/backends/Windows.py @@ -1,6 +1,6 @@ import logging -from jaraco.classes import properties +from .._compat import properties from ..backend import KeyringBackend from ..credentials import SimpleCredential from ..errors import PasswordDeleteError, ExceptionRaisedContext diff --git a/keyring/backends/chainer.py b/keyring/backends/chainer.py index 64b5faff..5a9281c7 100644 --- a/keyring/backends/chainer.py +++ b/keyring/backends/chainer.py @@ -4,7 +4,7 @@ """ from .. import backend -from jaraco.classes import properties +from .._compat import properties from . import fail diff --git a/keyring/backends/kwallet.py b/keyring/backends/kwallet.py index 82736312..3137daed 100644 --- a/keyring/backends/kwallet.py +++ b/keyring/backends/kwallet.py @@ -6,7 +6,7 @@ from ..credentials import SimpleCredential from ..errors import PasswordDeleteError from ..errors import PasswordSetError, InitError, KeyringLocked -from jaraco.classes import properties +from .._compat import properties try: import dbus diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index d4c5801a..c97e0d33 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -1,7 +1,7 @@ import logging from .. import backend -from jaraco.classes import properties +from .._compat import properties from ..backend import KeyringBackend from ..credentials import SimpleCredential from ..errors import ( diff --git a/keyring/backends/macOS/__init__.py b/keyring/backends/macOS/__init__.py index 0aeb0c70..4ded55b8 100644 --- a/keyring/backends/macOS/__init__.py +++ b/keyring/backends/macOS/__init__.py @@ -7,7 +7,7 @@ from ...errors import PasswordDeleteError from ...errors import KeyringLocked from ...errors import KeyringError -from jaraco.classes import properties +from ..._compat import properties try: from . import api