Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use jaraco.classes for properties #588

Merged
merged 2 commits into from Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions 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
169 changes: 169 additions & 0 deletions keyring/_properties_compat.py
@@ -0,0 +1,169 @@
# 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
8 changes: 3 additions & 5 deletions keyring/backend.py
Expand Up @@ -12,7 +12,7 @@

from .py310compat import metadata
from . import credentials, errors, util
from .util import properties
from ._compat import properties

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,8 +61,7 @@ def priority(cls):
suitable, but a priority of one or greater is recommended.
"""

@properties.ClassProperty
@classmethod
@properties.classproperty
def viable(cls):
with errors.ExceptionRaisedContext() as exc:
cls.priority
Expand All @@ -75,8 +74,7 @@ def get_viable_backends(cls):
"""
return filter(operator.attrgetter('viable'), cls._classes)

@properties.ClassProperty
@classmethod
@properties.classproperty
def name(cls):
"""
The keyring name, suitable for display.
Expand Down
5 changes: 2 additions & 3 deletions keyring/backends/SecretService.py
Expand Up @@ -2,7 +2,7 @@
import logging

from .. import backend
from ..util import properties
from .._compat import properties
from ..backend import KeyringBackend
from ..credentials import SimpleCredential
from ..errors import (
Expand All @@ -29,8 +29,7 @@ class Keyring(backend.SchemeSelectable, KeyringBackend):

appid = 'Python keyring library'

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
with ExceptionRaisedContext() as exc:
secretstorage.__name__
Expand Down
5 changes: 2 additions & 3 deletions keyring/backends/Windows.py
@@ -1,6 +1,6 @@
import logging

from ..util import properties
from .._compat import properties
from ..backend import KeyringBackend
from ..credentials import SimpleCredential
from ..errors import PasswordDeleteError, ExceptionRaisedContext
Expand Down Expand Up @@ -80,8 +80,7 @@ class WinVaultKeyring(KeyringBackend):

persist = Persistence()

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
"""
If available, the preferred backend on Windows.
Expand Down
8 changes: 3 additions & 5 deletions keyring/backends/chainer.py
Expand Up @@ -4,7 +4,7 @@
"""

from .. import backend
from ..util import properties
from .._compat import properties
from . import fail


Expand All @@ -18,8 +18,7 @@ class ChainerBackend(backend.KeyringBackend):
# until other backends have been constructed
viable = True

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
"""
If there are backends to chain, high priority
Expand All @@ -28,8 +27,7 @@ def priority(cls):
"""
return 10 if len(cls.backends) > 1 else (fail.Keyring.priority - 1)

@properties.ClassProperty
@classmethod
@properties.classproperty
def backends(cls):
"""
Discover all keyrings for chaining.
Expand Down
8 changes: 3 additions & 5 deletions keyring/backends/kwallet.py
Expand Up @@ -6,7 +6,7 @@
from ..credentials import SimpleCredential
from ..errors import PasswordDeleteError
from ..errors import PasswordSetError, InitError, KeyringLocked
from ..util import properties
from .._compat import properties

try:
import dbus
Expand Down Expand Up @@ -37,8 +37,7 @@ class DBusKeyring(KeyringBackend):
bus_name = 'org.kde.kwalletd5'
object_path = '/modules/kwalletd5'

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
if 'dbus' not in globals():
raise RuntimeError('python-dbus not installed')
Expand Down Expand Up @@ -161,7 +160,6 @@ class DBusKeyringKWallet4(DBusKeyring):
bus_name = 'org.kde.kwalletd'
object_path = '/modules/kwalletd'

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
return super().priority - 1
5 changes: 2 additions & 3 deletions keyring/backends/libsecret.py
@@ -1,7 +1,7 @@
import logging

from .. import backend
from ..util import properties
from .._compat import properties
from ..backend import KeyringBackend
from ..credentials import SimpleCredential
from ..errors import (
Expand Down Expand Up @@ -48,8 +48,7 @@ def schema(self):
def collection(self):
return Secret.COLLECTION_DEFAULT

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
with ExceptionRaisedContext() as exc:
Secret.__name__
Expand Down
5 changes: 2 additions & 3 deletions keyring/backends/macOS/__init__.py
Expand Up @@ -7,7 +7,7 @@
from ...errors import PasswordDeleteError
from ...errors import KeyringLocked
from ...errors import KeyringError
from ...util import properties
from ..._compat import properties

try:
from . import api
Expand All @@ -21,8 +21,7 @@ class Keyring(KeyringBackend):
keychain = os.environ.get('KEYCHAIN_PATH')
"Path to keychain file, overriding default"

@properties.ClassProperty
@classmethod
@properties.classproperty
def priority(cls):
"""
Preferred for all macOS environments.
Expand Down
57 changes: 0 additions & 57 deletions keyring/util/properties.py

This file was deleted.