Skip to content

Commit

Permalink
Merge pull request #588 from jaraco/feature/jaraco.classes
Browse files Browse the repository at this point in the history
Use jaraco.classes for properties
  • Loading branch information
jaraco authored Sep 1, 2022
2 parents 5de87e5 + 3c38fa4 commit 977ed03
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 84 deletions.
7 changes: 7 additions & 0 deletions keyring/_compat.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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.

Loading

0 comments on commit 977ed03

Please sign in to comment.