Skip to content

Commit 977ed03

Browse files
authored
Merge pull request #588 from jaraco/feature/jaraco.classes
Use jaraco.classes for properties
2 parents 5de87e5 + 3c38fa4 commit 977ed03

11 files changed

+194
-84
lines changed

keyring/_compat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__all__ = ['properties']
2+
3+
4+
try:
5+
from jaraco.compat import properties # pragma: no-cover
6+
except ImportError:
7+
from . import _properties_compat as properties # pragma: no-cover

keyring/_properties_compat.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# from jaraco.classes 3.2.2
2+
3+
4+
class NonDataProperty:
5+
"""Much like the property builtin, but only implements __get__,
6+
making it a non-data property, and can be subsequently reset.
7+
8+
See http://users.rcn.com/python/download/Descriptor.htm for more
9+
information.
10+
11+
>>> class X(object):
12+
... @NonDataProperty
13+
... def foo(self):
14+
... return 3
15+
>>> x = X()
16+
>>> x.foo
17+
3
18+
>>> x.foo = 4
19+
>>> x.foo
20+
4
21+
"""
22+
23+
def __init__(self, fget):
24+
assert fget is not None, "fget cannot be none"
25+
assert callable(fget), "fget must be callable"
26+
self.fget = fget
27+
28+
def __get__(self, obj, objtype=None):
29+
if obj is None:
30+
return self
31+
return self.fget(obj)
32+
33+
34+
class classproperty:
35+
"""
36+
Like @property but applies at the class level.
37+
38+
39+
>>> class X(metaclass=classproperty.Meta):
40+
... val = None
41+
... @classproperty
42+
... def foo(cls):
43+
... return cls.val
44+
... @foo.setter
45+
... def foo(cls, val):
46+
... cls.val = val
47+
>>> X.foo
48+
>>> X.foo = 3
49+
>>> X.foo
50+
3
51+
>>> x = X()
52+
>>> x.foo
53+
3
54+
>>> X.foo = 4
55+
>>> x.foo
56+
4
57+
58+
Setting the property on an instance affects the class.
59+
60+
>>> x.foo = 5
61+
>>> x.foo
62+
5
63+
>>> X.foo
64+
5
65+
>>> vars(x)
66+
{}
67+
>>> X().foo
68+
5
69+
70+
Attempting to set an attribute where no setter was defined
71+
results in an AttributeError:
72+
73+
>>> class GetOnly(metaclass=classproperty.Meta):
74+
... @classproperty
75+
... def foo(cls):
76+
... return 'bar'
77+
>>> GetOnly.foo = 3
78+
Traceback (most recent call last):
79+
...
80+
AttributeError: can't set attribute
81+
82+
It is also possible to wrap a classmethod or staticmethod in
83+
a classproperty.
84+
85+
>>> class Static(metaclass=classproperty.Meta):
86+
... @classproperty
87+
... @classmethod
88+
... def foo(cls):
89+
... return 'foo'
90+
... @classproperty
91+
... @staticmethod
92+
... def bar():
93+
... return 'bar'
94+
>>> Static.foo
95+
'foo'
96+
>>> Static.bar
97+
'bar'
98+
99+
*Legacy*
100+
101+
For compatibility, if the metaclass isn't specified, the
102+
legacy behavior will be invoked.
103+
104+
>>> class X:
105+
... val = None
106+
... @classproperty
107+
... def foo(cls):
108+
... return cls.val
109+
... @foo.setter
110+
... def foo(cls, val):
111+
... cls.val = val
112+
>>> X.foo
113+
>>> X.foo = 3
114+
>>> X.foo
115+
3
116+
>>> x = X()
117+
>>> x.foo
118+
3
119+
>>> X.foo = 4
120+
>>> x.foo
121+
4
122+
123+
Note, because the metaclass was not specified, setting
124+
a value on an instance does not have the intended effect.
125+
126+
>>> x.foo = 5
127+
>>> x.foo
128+
5
129+
>>> X.foo # should be 5
130+
4
131+
>>> vars(x) # should be empty
132+
{'foo': 5}
133+
>>> X().foo # should be 5
134+
4
135+
"""
136+
137+
class Meta(type):
138+
def __setattr__(self, key, value):
139+
obj = self.__dict__.get(key, None)
140+
if type(obj) is classproperty:
141+
return obj.__set__(self, value)
142+
return super().__setattr__(key, value)
143+
144+
def __init__(self, fget, fset=None):
145+
self.fget = self._ensure_method(fget)
146+
self.fset = fset
147+
fset and self.setter(fset)
148+
149+
def __get__(self, instance, owner=None):
150+
return self.fget.__get__(None, owner)()
151+
152+
def __set__(self, owner, value):
153+
if not self.fset:
154+
raise AttributeError("can't set attribute")
155+
if type(owner) is not classproperty.Meta:
156+
owner = type(owner)
157+
return self.fset.__get__(None, owner)(value)
158+
159+
def setter(self, fset):
160+
self.fset = self._ensure_method(fset)
161+
return self
162+
163+
@classmethod
164+
def _ensure_method(cls, fn):
165+
"""
166+
Ensure fn is a classmethod or staticmethod.
167+
"""
168+
needs_method = not isinstance(fn, (classmethod, staticmethod))
169+
return classmethod(fn) if needs_method else fn

keyring/backend.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .py310compat import metadata
1414
from . import credentials, errors, util
15-
from .util import properties
15+
from ._compat import properties
1616

1717
log = logging.getLogger(__name__)
1818

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

64-
@properties.ClassProperty
65-
@classmethod
64+
@properties.classproperty
6665
def viable(cls):
6766
with errors.ExceptionRaisedContext() as exc:
6867
cls.priority
@@ -75,8 +74,7 @@ def get_viable_backends(cls):
7574
"""
7675
return filter(operator.attrgetter('viable'), cls._classes)
7776

78-
@properties.ClassProperty
79-
@classmethod
77+
@properties.classproperty
8078
def name(cls):
8179
"""
8280
The keyring name, suitable for display.

keyring/backends/SecretService.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33

44
from .. import backend
5-
from ..util import properties
5+
from .._compat import properties
66
from ..backend import KeyringBackend
77
from ..credentials import SimpleCredential
88
from ..errors import (
@@ -29,8 +29,7 @@ class Keyring(backend.SchemeSelectable, KeyringBackend):
2929

3030
appid = 'Python keyring library'
3131

32-
@properties.ClassProperty
33-
@classmethod
32+
@properties.classproperty
3433
def priority(cls):
3534
with ExceptionRaisedContext() as exc:
3635
secretstorage.__name__

keyring/backends/Windows.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from ..util import properties
3+
from .._compat import properties
44
from ..backend import KeyringBackend
55
from ..credentials import SimpleCredential
66
from ..errors import PasswordDeleteError, ExceptionRaisedContext
@@ -80,8 +80,7 @@ class WinVaultKeyring(KeyringBackend):
8080

8181
persist = Persistence()
8282

83-
@properties.ClassProperty
84-
@classmethod
83+
@properties.classproperty
8584
def priority(cls):
8685
"""
8786
If available, the preferred backend on Windows.

keyring/backends/chainer.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
from .. import backend
7-
from ..util import properties
7+
from .._compat import properties
88
from . import fail
99

1010

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

21-
@properties.ClassProperty
22-
@classmethod
21+
@properties.classproperty
2322
def priority(cls):
2423
"""
2524
If there are backends to chain, high priority
@@ -28,8 +27,7 @@ def priority(cls):
2827
"""
2928
return 10 if len(cls.backends) > 1 else (fail.Keyring.priority - 1)
3029

31-
@properties.ClassProperty
32-
@classmethod
30+
@properties.classproperty
3331
def backends(cls):
3432
"""
3533
Discover all keyrings for chaining.

keyring/backends/kwallet.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..credentials import SimpleCredential
77
from ..errors import PasswordDeleteError
88
from ..errors import PasswordSetError, InitError, KeyringLocked
9-
from ..util import properties
9+
from .._compat import properties
1010

1111
try:
1212
import dbus
@@ -37,8 +37,7 @@ class DBusKeyring(KeyringBackend):
3737
bus_name = 'org.kde.kwalletd5'
3838
object_path = '/modules/kwalletd5'
3939

40-
@properties.ClassProperty
41-
@classmethod
40+
@properties.classproperty
4241
def priority(cls):
4342
if 'dbus' not in globals():
4443
raise RuntimeError('python-dbus not installed')
@@ -161,7 +160,6 @@ class DBusKeyringKWallet4(DBusKeyring):
161160
bus_name = 'org.kde.kwalletd'
162161
object_path = '/modules/kwalletd'
163162

164-
@properties.ClassProperty
165-
@classmethod
163+
@properties.classproperty
166164
def priority(cls):
167165
return super().priority - 1

keyring/backends/libsecret.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
from .. import backend
4-
from ..util import properties
4+
from .._compat import properties
55
from ..backend import KeyringBackend
66
from ..credentials import SimpleCredential
77
from ..errors import (
@@ -48,8 +48,7 @@ def schema(self):
4848
def collection(self):
4949
return Secret.COLLECTION_DEFAULT
5050

51-
@properties.ClassProperty
52-
@classmethod
51+
@properties.classproperty
5352
def priority(cls):
5453
with ExceptionRaisedContext() as exc:
5554
Secret.__name__

keyring/backends/macOS/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ...errors import PasswordDeleteError
88
from ...errors import KeyringLocked
99
from ...errors import KeyringError
10-
from ...util import properties
10+
from ..._compat import properties
1111

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

24-
@properties.ClassProperty
25-
@classmethod
24+
@properties.classproperty
2625
def priority(cls):
2726
"""
2827
Preferred for all macOS environments.

keyring/util/properties.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)