Skip to content

Commit 48fd8d1

Browse files
committed
Introduce typing annotations
The goal is to initiate the effort of providing full annotations for the library. Which is not yet the case, but we need to start small and progress incrementally. Hopefully this will create incentive from other contributors to add up to this effort, even in tiny steps, until we can take advantage of the multiple type checkers out there.
1 parent 52ece8e commit 48fd8d1

File tree

3 files changed

+35
-23
lines changed

3 files changed

+35
-23
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ global-exclude *.py[cod] __pycache__
88
global-exclude .coverage
99
global-exclude .DS_Store
1010
exclude .pre-commit-config.yaml tox.ini
11+
recursive-include rules *.typed

rules/predicates.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import threading
44
from functools import partial, update_wrapper
55
from inspect import getfullargspec, isfunction, ismethod
6+
from typing import Any, Callable, List, Optional, Tuple, Union
67

78
logger = logging.getLogger("rules")
89

910

10-
def assert_has_kwonlydefaults(fn, msg):
11+
def assert_has_kwonlydefaults(fn: Callable[..., Any], msg: str) -> None:
1112
argspec = getfullargspec(fn)
1213
if hasattr(argspec, "kwonlyargs"):
1314
if not argspec.kwonlyargs:
@@ -19,21 +20,21 @@ def assert_has_kwonlydefaults(fn, msg):
1920

2021

2122
class Context(dict):
22-
def __init__(self, args):
23+
def __init__(self, args: Tuple[Any, ...]) -> None:
2324
super(Context, self).__init__()
2425
self.args = args
2526

2627

2728
class localcontext(threading.local):
28-
def __init__(self):
29-
self.stack = []
29+
def __init__(self) -> None:
30+
self.stack: List[Context] = []
3031

3132

3233
_context = localcontext()
3334

3435

3536
class NoValueSentinel(object):
36-
def __bool__(self):
37+
def __bool__(self) -> bool:
3738
return False
3839

3940
__nonzero__ = __bool__ # python 2
@@ -45,7 +46,17 @@ def __bool__(self):
4546

4647

4748
class Predicate(object):
48-
def __init__(self, fn, name=None, bind=False):
49+
fn: Callable[..., Any]
50+
num_args: int
51+
var_args: bool
52+
name: str
53+
54+
def __init__(
55+
self,
56+
fn: Union["Predicate", Callable[..., Any]],
57+
name: Optional[str] = None,
58+
bind: bool = False,
59+
) -> None:
4960
# fn can be a callable with any of the following signatures:
5061
# - fn(obj=None, target=None)
5162
# - fn(obj=None)
@@ -98,13 +109,13 @@ def __init__(self, fn, name=None, bind=False):
98109
self.name = name or fn.__name__
99110
self.bind = bind
100111

101-
def __repr__(self):
112+
def __repr__(self) -> str:
102113
return "<%s:%s object at %s>" % (type(self).__name__, str(self), hex(id(self)))
103114

104-
def __str__(self):
115+
def __str__(self) -> str:
105116
return self.name
106117

107-
def __call__(self, *args, **kwargs):
118+
def __call__(self, *args, **kwargs) -> Any:
108119
# this method is defined as variadic in order to not mask the
109120
# underlying callable's signature that was most likely decorated
110121
# as a predicate. internally we consistently call ``_apply`` that
@@ -114,7 +125,7 @@ def __call__(self, *args, **kwargs):
114125
return self.fn(*args, **kwargs)
115126

116127
@property
117-
def context(self):
128+
def context(self) -> Optional[Context]:
118129
"""
119130
The currently active invocation context. A new context is created as a
120131
result of invoking ``test()`` and is only valid for the duration of
@@ -150,7 +161,7 @@ def context(self):
150161
except IndexError:
151162
return None
152163

153-
def test(self, obj=NO_VALUE, target=NO_VALUE):
164+
def test(self, obj: Any = NO_VALUE, target: Any = NO_VALUE) -> bool:
154165
"""
155166
The canonical method to invoke predicates.
156167
"""
@@ -162,25 +173,25 @@ def test(self, obj=NO_VALUE, target=NO_VALUE):
162173
finally:
163174
_context.stack.pop()
164175

165-
def __and__(self, other):
176+
def __and__(self, other) -> "Predicate":
166177
def AND(*args):
167178
return self._combine(other, operator.and_, args)
168179

169180
return type(self)(AND, "(%s & %s)" % (self.name, other.name))
170181

171-
def __or__(self, other):
182+
def __or__(self, other) -> "Predicate":
172183
def OR(*args):
173184
return self._combine(other, operator.or_, args)
174185

175186
return type(self)(OR, "(%s | %s)" % (self.name, other.name))
176187

177-
def __xor__(self, other):
188+
def __xor__(self, other) -> "Predicate":
178189
def XOR(*args):
179190
return self._combine(other, operator.xor, args)
180191

181192
return type(self)(XOR, "(%s ^ %s)" % (self.name, other.name))
182193

183-
def __invert__(self):
194+
def __invert__(self) -> "Predicate":
184195
def INVERT(*args):
185196
result = self._apply(*args)
186197
return None if result is None else not result
@@ -208,7 +219,7 @@ def _combine(self, other, op, args):
208219

209220
return op(self_result, other_result)
210221

211-
def _apply(self, *args):
222+
def _apply(self, *args) -> Optional[bool]:
212223
# Internal method that is used to invoke the predicate with the
213224
# proper number of positional arguments, inside the current
214225
# invocation context.
@@ -268,12 +279,12 @@ def inner(fn):
268279
always_deny = predicate(lambda: False, name="always_deny")
269280

270281

271-
def is_bool_like(obj):
282+
def is_bool_like(obj) -> bool:
272283
return hasattr(obj, "__bool__") or hasattr(obj, "__nonzero__")
273284

274285

275286
@predicate
276-
def is_authenticated(user):
287+
def is_authenticated(user) -> bool:
277288
if not hasattr(user, "is_authenticated"):
278289
return False # not a user model
279290
if not is_bool_like(user.is_authenticated): # pragma: no cover
@@ -283,27 +294,27 @@ def is_authenticated(user):
283294

284295

285296
@predicate
286-
def is_superuser(user):
297+
def is_superuser(user) -> bool:
287298
if not hasattr(user, "is_superuser"):
288299
return False # swapped user model, doesn't support is_superuser
289300
return user.is_superuser
290301

291302

292303
@predicate
293-
def is_staff(user):
304+
def is_staff(user) -> bool:
294305
if not hasattr(user, "is_staff"):
295306
return False # swapped user model, doesn't support is_staff
296307
return user.is_staff
297308

298309

299310
@predicate
300-
def is_active(user):
311+
def is_active(user) -> bool:
301312
if not hasattr(user, "is_active"):
302313
return False # swapped user model, doesn't support is_active
303314
return user.is_active
304315

305316

306-
def is_group_member(*groups):
317+
def is_group_member(*groups) -> Callable[..., Any]:
307318
assert len(groups) > 0, "You must provide at least one group name"
308319

309320
if len(groups) > 3:
@@ -314,7 +325,7 @@ def is_group_member(*groups):
314325
name = "is_group_member:%s" % ",".join(g)
315326

316327
@predicate(name)
317-
def fn(user):
328+
def fn(user) -> bool:
318329
if not hasattr(user, "groups"):
319330
return False # swapped user model, doesn't support groups
320331
if not hasattr(user, "_group_names_cache"): # pragma: no cover

rules/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)