Skip to content

Commit 6e43272

Browse files
committed
refactor: change some classes to use __slots__
Speed up access to and reduce size of some low level classes. A few classes in security.py, rest.py are heavily used. But for all, it prevents adding random properties to lower level classes that people shouldn't be mucking with. While doing this I found some test cases accessing an invalid property name and this change caused the cases to crash. admin.py: Use new method Role.props_dict() and Permission.props_dict() where original code just referenced __dict__ when printing Role/Permission. mlink_expr.py: Add slots to multiple classes. Classes Binary and Unary set real properties/attributes. Classes that inherit from them (Equals, Empty, Not, Or, And) define empty slots tuple to eliminate need for __dict__. Class Expression also gets a slot. rate_limit.py: RateLimit and Gcra classes get slots. A couple of pep8 fixes: sort imports, remove trailing spaces on a line, remove unused noqa comment. rest.py: Add slots to class SimulateFieldStorageFromJson and FsValue classes. The memory savings from this could be useful as well as speedier access to the attributes. security.py: Add slots to Permission class. To prevent conflict between slot limit_perm_to_props_only and the class variable of the same name, rename the class variable to limit_perm_to_props_only_default. Also define method props_dict() to allow other code to get a dict to iterate over when checking permissions. Add slots to class Role along with props_dict() method. Add slots to class Security. Also have to add explicit __dict__ slot to support test override of the hasPermission() method. Add props_dict() method, currently unused, but added for symmetry. support.py: TruthDict and PrioList gets slots. test/test_cgi.py: Fix incorrect setting of permission property. Was setting permissions. So testing may not have been doing what we thought it was. Multiple places found with this typo. Remove setting of permissions in some places where it should have no effect on the test and looks like it was just copypasta. test/test_xmlrpc.py Remove setting of permissions in some places where it should have no effect on the test and looks like it was just copypasta.
1 parent 410691d commit 6e43272

File tree

9 files changed

+82
-15
lines changed

9 files changed

+82
-15
lines changed

CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ Fixed:
3535
Rouillard)
3636
- replaced hostname localhost with 127.0.0.1 in docker healthcheck
3737
script. Found/patch by Norbert Schlemmer. (John Rouillard)
38-
38+
- change some internal classes to use __slots__ for hopefully a small
39+
performance improvement. (John Rouillard)
40+
3941
Features:
4042

4143
- add support for authorized changes. User can be prompted to enter

roundup/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def help_commands(self):
250250
if seq in h:
251251
commands.append(' ' + h.split(seq, 1)[1].lstrip())
252252
break
253-
253+
254254
commands.sort()
255255
commands.append(_(
256256
"""Commands may be abbreviated as long as the abbreviation
@@ -2084,9 +2084,9 @@ def do_security(self, args):
20842084
sys.stdout.write(_('New Email users get the Role "%(role)s"\n') % locals())
20852085
roles.sort()
20862086
for _rolename, role in roles:
2087-
sys.stdout.write(_('Role "%(name)s":\n') % role.__dict__)
2087+
sys.stdout.write(_('Role "%(name)s":\n') % role.props_dict())
20882088
for permission in role.permission_list():
2089-
d = permission.__dict__
2089+
d = permission.props_dict()
20902090
if permission.klass:
20912091
if permission.properties:
20922092
sys.stdout.write(_(

roundup/mlink_expr.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def __repr__(self):
6060

6161
class Binary:
6262

63+
__slots__ = ("x", "y")
64+
6365
def __init__(self, x, y):
6466
self.x = x
6567
self.y = y
@@ -71,6 +73,8 @@ def visit(self, visitor):
7173

7274
class Unary:
7375

76+
__slots__ = ("x",)
77+
7478
def __init__(self, x):
7579
self.x = x
7680

@@ -83,6 +87,8 @@ def visit(self, visitor):
8387

8488
class Equals(Unary):
8589

90+
__slots__ = ()
91+
8692
def evaluate(self, v):
8793
return self.x in v
8894

@@ -95,6 +101,8 @@ def __repr__(self):
95101

96102
class Empty(Unary):
97103

104+
__slots__ = ()
105+
98106
def evaluate(self, v):
99107
return not v
100108

@@ -107,6 +115,8 @@ def __repr__(self):
107115

108116
class Not(Unary):
109117

118+
__slots__ = ()
119+
110120
def evaluate(self, v):
111121
return not self.x.evaluate(v)
112122

@@ -119,6 +129,8 @@ def __repr__(self):
119129

120130
class Or(Binary):
121131

132+
__slots__ = ()
133+
122134
def evaluate(self, v):
123135
return self.x.evaluate(v) or self.y.evaluate(v)
124136

@@ -133,6 +145,8 @@ def __repr__(self):
133145

134146
class And(Binary):
135147

148+
__slots__ = ()
149+
136150
def evaluate(self, v):
137151
return self.x.evaluate(v) and self.y.evaluate(v)
138152

@@ -185,6 +199,8 @@ def compile_expression(opcodes):
185199

186200
class Expression:
187201

202+
__slots__ = ("evaluate",)
203+
188204
def __init__(self, v, is_link=False):
189205
try:
190206
opcodes = [int(x) for x in v]

roundup/rate_limit.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# set/get_tat and marshaling as string, support for testonly
55
# and status method.
66

7-
from datetime import timedelta, datetime
7+
from datetime import datetime, timedelta
88

99
try:
1010
# used by python 3.11 and newer use tz aware dates
@@ -19,12 +19,15 @@
1919
dt_epoch = datetime(1970, 1, 1)
2020
def fromisoformat(date):
2121
# only for naive dates
22-
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")
22+
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")
2323

2424
from roundup.anypy.datetime_ import utcnow
2525

2626

2727
class RateLimit: # pylint: disable=too-few-public-methods
28+
29+
__slots__ = ("count", "period")
30+
2831
def __init__(self, count, period):
2932
self.count = count
3033
self.period = period
@@ -35,6 +38,9 @@ def inverse(self):
3538

3639

3740
class Gcra:
41+
42+
__slots__ = ("memory",)
43+
3844
def __init__(self):
3945
self.memory = {}
4046

@@ -124,7 +130,7 @@ def status(self, key, limit):
124130
# One item is dequeued every limit.inverse seconds.
125131
ret['Retry-After'] = str(int(limit.inverse))
126132
ret['Retry-After-Timestamp'] = "%s" % \
127-
(now + timedelta(seconds=limit.inverse)) # noqa: E127
133+
(now + timedelta(seconds=limit.inverse))
128134
else:
129135
# if we are not rejected, the user can post another
130136
# attempt immediately.

roundup/rest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ def execute(cls, instance, path, method, input_payload):
426426
func = func_obj['func']
427427

428428
# zip the varlist into a dictionary, and pass it to the caller
429+
# FIXME: 3.10 is min version- add strict=True to zip
430+
# also wrap with try/except ValueError if different number of
431+
# items (which should never happen).
429432
args = dict(zip(list_vars, match_obj.groups()))
430433
args['input_payload'] = input_payload
431434
return func(instance, **args)
@@ -2741,6 +2744,9 @@ class SimulateFieldStorageFromJson():
27412744
string.
27422745
27432746
'''
2747+
2748+
__slots__ = ("json_dict", "value")
2749+
27442750
def __init__(self, json_string):
27452751
'''Parse the json string into an internal dict.
27462752
@@ -2764,6 +2770,9 @@ def raise_error_on_constant(x):
27642770
raise ValueError(e.args[0] + ". JSON is: " + json_string)
27652771

27662772
class FsValue:
2773+
2774+
__slots__ = ("name", "value")
2775+
27672776
'''Class that does nothing but response to a .value property '''
27682777
def __init__(self, name, val):
27692778
self.name = u2s(name)

roundup/security.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Permission:
1818
- properties (optional)
1919
- check function (optional)
2020
- props_only (optional, internal field is limit_perm_to_props_only)
21+
default value taken from Permission.limit_perm_to_props_only_default.
2122
- filter function (optional) returns filter arguments for
2223
determining which records are visible by the user. The filter
2324
function comes into play when determining if a set of nodes
@@ -58,7 +59,18 @@ class Permission:
5859
with a True or False value.
5960
'''
6061

61-
limit_perm_to_props_only = False
62+
__slots__ = (
63+
"_properties_dict",
64+
"check",
65+
"check_version",
66+
"description",
67+
"filter",
68+
"klass",
69+
"limit_perm_to_props_only",
70+
"name",
71+
"properties")
72+
73+
limit_perm_to_props_only_default = False
6274

6375
def __init__(self, name='', description='', klass=None,
6476
properties=None, check=None, props_only=None, filter=None):
@@ -80,7 +92,7 @@ def __init__(self, name='', description='', klass=None,
8092
# a == b will be true.
8193
if props_only is None:
8294
self.limit_perm_to_props_only = \
83-
Permission.limit_perm_to_props_only
95+
Permission.limit_perm_to_props_only_default
8496
else:
8597
# see note on use of bool() in set_props_only_default()
8698
self.limit_perm_to_props_only = bool(props_only)
@@ -123,6 +135,9 @@ def check(db, userid, itemid):
123135

124136
return check
125137

138+
def props_dict(self):
139+
return {name: getattr(self, name) for name in self.__slots__}
140+
126141
def test(self, db, permission, classname, property, userid, itemid):
127142
''' Test permissions 5 args:
128143
permission - string like Edit, Register etc. Required, no wildcard.
@@ -222,6 +237,9 @@ class Role:
222237
- description
223238
- permissions
224239
'''
240+
241+
__slots__ = ("_permissions", "description", "name")
242+
225243
def __init__(self, name='', description='', permissions=None):
226244
self.name = name.lower()
227245
self.description = description
@@ -301,6 +319,9 @@ def permission_list(self):
301319
perm_list.sort(key=lambda x: (x.klass or '', x.name))
302320
return perm_list
303321

322+
def props_dict(self):
323+
return {name: getattr(self, name) for name in self.__slots__}
324+
304325
def searchable(self, classname, propname):
305326
for perm_name in 'View', 'Search':
306327
# Only permissions without a check method
@@ -321,6 +342,12 @@ def searchable(self, classname, propname):
321342

322343

323344
class Security:
345+
346+
# __dict__ is needed to allow mocking of db.security.hasPermission
347+
# in test/test_templating.py. Define slots for properties used in
348+
# production to increase speed.
349+
__slots__ = ("__dict__", "db", "permission", "role")
350+
324351
def __init__(self, db):
325352
''' Initialise the permission and role classes, and add in the
326353
base roles (for admin user).
@@ -452,6 +479,9 @@ def is_filterable(self, permission, userid, classname):
452479
return False
453480
return True
454481

482+
def props_dict(self):
483+
return {name: getattr(self, name) for name in self.__slots__}
484+
455485
def roleHasSearchPermission(self, classname, property, *rolenames):
456486
""" For each of the given roles, check the permissions.
457487
Property can be a transitive property.
@@ -544,11 +574,11 @@ def set_props_only_default(self, props_only=None):
544574
# will be compared as part of tuple == tuple and
545575
# (3,) == (True,) is False even though 3 is a True value
546576
# in a boolean context. So use bool() to coerce value.
547-
Permission.limit_perm_to_props_only = \
577+
Permission.limit_perm_to_props_only_default = \
548578
bool(props_only)
549579

550580
def get_props_only_default(self):
551-
return Permission.limit_perm_to_props_only
581+
return Permission.limit_perm_to_props_only_default
552582

553583
def addPermissionToRole(self, rolename, permission, classname=None,
554584
properties=None, check=None, props_only=None):

roundup/support.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
class TruthDict:
1212
'''Returns True for valid keys, False for others.
1313
'''
14+
15+
__slots__ = ('keys',)
16+
1417
def __init__(self, keys):
1518
if keys:
1619
self.keys = {}
@@ -49,6 +52,9 @@ class PrioList:
4952
7
5053
5154
'''
55+
56+
__slots__ = ('key', 'list', 'sorted')
57+
5258
def __init__(self, key=None):
5359
self.list = []
5460
self.key = key

test/test_cgi.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,7 +1973,7 @@ def testClassPermission(self):
19731973
actions.EditItemAction(cl).handle)
19741974

19751975
def testCheckAndPropertyPermission(self):
1976-
self.db.security.permissions = {}
1976+
self.db.security.permission = {}
19771977
def own_record(db, userid, itemid):
19781978
return userid == itemid
19791979
p = self.db.security.addPermission(name='Edit', klass='user',
@@ -2004,7 +2004,7 @@ def own_record(db, userid, itemid):
20042004
def testCreatePermission(self):
20052005
# this checks if we properly differentiate between create and
20062006
# edit permissions
2007-
self.db.security.permissions = {}
2007+
self.db.security.permission = {}
20082008
self.db.security.addRole(name='UserAdd')
20092009
# Don't allow roles
20102010
p = self.db.security.addPermission(name='Create', klass='user',
@@ -2061,7 +2061,6 @@ def testCreatePermission(self):
20612061

20622062
def testSearchPermission(self):
20632063
# this checks if we properly check for search permissions
2064-
self.db.security.permissions = {}
20652064
self.db.security.addRole(name='User')
20662065
self.db.security.addRole(name='Project')
20672066
self.db.security.addPermissionToRole('User', 'Web Access')

test/test_xmlrpc.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ def testAuthAllowedCreate(self):
228228

229229
def testAuthFilter(self):
230230
# this checks if we properly check for search permissions
231-
self.db.security.permissions = {}
232231
# self.db.security.set_props_only_default(props_only=False)
233232
self.db.security.addRole(name='User')
234233
self.db.security.addRole(name='Project')

0 commit comments

Comments
 (0)