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 02:57:21 2026 +0000 @@ -3741,18 +3741,38 @@ """ - 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') + + if (hasattr(client.instance, 'templating_utils') and + (name in client.instance.templating_utils) ): + return client.instance.templating_utils[name] + + # a lot of client lookups are done, we already have it + # just return it. + if name == "client": return client + + return super().__getattribute__(name) def keywords_expressions(self, request): return render_keywords_expression_editor(request) + @staticmethod + def localReplace(message): + """Default no-op for localReplace. + + Override with real definition in extensions. + + This allows templates to ship with localReplace(....) + and have an: + + instance.registerUtil('localReplace', local_replace) + + define localReplace with the same call signature (i.e. no self) + in the extension definition. + """ + return message + def html_calendar(self, request): """Generate a HTML calendar. 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 02:57:21 2026 +0000 @@ -262,6 +262,16 @@ instead. """ + # prevent overriding of internal TemplatingUtil methods/vars. + if name.startswith('_'): + import inspect + caller = inspect.stack()[1] + raise ValueError( + "Can't define template utility named '%(name)s' at:\n" + " %(filename)s:%(lineno)d\n" + " Utility names must not start with '_'.\n" + % {**caller._asdict(), "name": name}) + self.templating_utils[name] = function def registerUtilMethod(self, name, function): @@ -275,6 +285,16 @@ `self` is a TemplatingUtils object. You can use self.client to access the client object for your request. """ + # prevent overriding of internal TemplatingUtil methods/vars. + if name.startswith('_'): + 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 '_'.\n" + % {**caller._asdict(), "name": name}) + setattr(self.TemplatingUtils, name, function)