diff -r 75abdd7f2568 roundup/cgi/templating.py --- a/roundup/cgi/templating.py Thu Jul 30 17:16:32 2026 -0400 +++ b/roundup/cgi/templating.py Mon Aug 03 03:58:40 2026 +0000 @@ -3607,6 +3607,10 @@ def __init__(self, client): self.client = client self._ = self.client._ + self._bound_methods = {} + + for name, method in self.client.instance.templating_util_methods.items(): + self._bound_methods[name] = method.__get__(self) def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): return Batch(self.client, sequence, size, start, end, orphan, @@ -3741,14 +3745,22 @@ """ - def __getattr__(self, name): - """Try the tracker's templating_utils.""" - if not hasattr(self.client.instance, 'templating_utils'): - # backwards-compatibility - raise AttributeError(name) - if name not in self.client.instance.templating_utils: - raise AttributeError(name) - return self.client.instance.templating_utils[name] + def __getattribute__(self, name): + client = super().__getattribute__('client') + + # a lot of client lookups are done, we already have it + # just return it. + if name == "client": return client + + if (hasattr(client.instance, 'templating_utils') and + (name in client.instance.templating_utils) ): + return client.instance.templating_utils[name] + + bound_methods = super().__getattribute__('_bound_methods') + if name in bound_methods: + return bound_methods[name] + + return super().__getattribute__(name) def keywords_expressions(self, request): return render_keywords_expression_editor(request) diff -r 75abdd7f2568 roundup/instance.py --- a/roundup/instance.py Thu Jul 30 17:16:32 2026 -0400 +++ b/roundup/instance.py Mon Aug 03 03:58:40 2026 +0000 @@ -59,6 +59,7 @@ self.actions = {} self.cgi_actions = {} self.templating_utils = {} + self.templating_util_methods = {} libdir = os.path.join(self.tracker_home, 'lib') self.libdir = (os.path.isdir(libdir) and libdir) or '' @@ -262,6 +263,17 @@ instead. """ + # prevent overriding of internal TemplatingUtil methods/vars. + if name.startswith('_') or name == "client": + import inspect + caller = inspect.stack()[1] + raise ValueError( + "Can't define template utility named '%(name)s' at:\n" + " %(filename)s:%(lineno)d\n" + " Utility method names must not start with '_' or " + " be called client.\n" + % {**caller._asdict(), "name": name}) + self.templating_utils[name] = function def registerUtilMethod(self, name, function): @@ -275,9 +287,18 @@ `self` is a TemplatingUtils object. You can use self.client to access the client object for your request. """ - setattr(self.TemplatingUtils, - name, - function) + # prevent overriding of internal TemplatingUtil methods/vars. + if name.startswith('_') or name == "client": + import inspect + caller = inspect.stack()[1] + raise ValueError( + "Can't define template utility method named '%(name)s' at:\n" + " %(filename)s:%(lineno)d\n" + " Utility method names must not start with '_' or " + " be called client.\n" + % {**caller._asdict(), "name": name}) + + self.templating_util_methods[name] = function class TrackerError(RoundupException):