From 401081aa38cfe834ff1e838296c06d08170e170d Mon Sep 17 00:00:00 2001 From: Yao Ding Date: Tue, 5 May 2026 14:46:18 -0400 Subject: [PATCH 1/2] make Compiler thread-safe by moving CodeBuilder to instance scope --- pybars/_compiler.py | 5 +++-- tests/test__compiler.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pybars/_compiler.py b/pybars/_compiler.py index becc692..996a956 100644 --- a/pybars/_compiler.py +++ b/pybars/_compiler.py @@ -776,12 +776,13 @@ class Compiler: """ _handlebars = OMeta.makeGrammar(handlebars_grammar, {}, 'handlebars') - _builder = CodeBuilder() - _compiler = OMeta.makeGrammar(compile_grammar, {'builder': _builder}) def __init__(self): self._helpers = {} self.template_counter = 1 + self._builder = CodeBuilder() + self._compiler = OMeta.makeGrammar( + compile_grammar, {'builder': self._builder}) def _extract_word(self, source, position): """ diff --git a/tests/test__compiler.py b/tests/test__compiler.py index a44ec1f..d0573b7 100644 --- a/tests/test__compiler.py +++ b/tests/test__compiler.py @@ -22,6 +22,7 @@ str_class = str import sys +import threading from unittest import TestCase @@ -134,3 +135,34 @@ def test_compile_with_path(self): # recompile and check that a new path is used self.assertEqual(result, compiler.compile(template, path=path)(context)) self.assertTrue(sys.modules.get('pybars._templates._project_widgets_templates_1') is not None) + + def test_distinct_compilers_are_thread_safe(self): + templates = [ + (u"Hello {{name}}!", {'name': 'world'}, u"Hello world!"), + (u"{{#each items}}{{this}},{{/each}}", + {'items': [1, 2, 3]}, u"1,2,3,"), + (u"{{#if flag}}yes{{else}}no{{/if}}", + {'flag': True}, u"yes"), + (u"{{a.b.c}}", {'a': {'b': {'c': 'deep'}}}, u"deep"), + ] + + errors = [] + barrier = threading.Barrier(8) + + def worker(): + try: + barrier.wait() + for _ in range(10): + for src, ctx, expected in templates: + fn = Compiler().compile(src) + self.assertEqual(expected, str_class(fn(ctx))) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=worker) for _ in range(8)] + for t in threads: + t.start() + for t in threads: + t.join() + + self.assertEqual([], errors) From 813414663835ddb511e418a47b0d4687188f1216 Mon Sep 17 00:00:00 2001 From: Yao Ding Date: Tue, 5 May 2026 14:52:15 -0400 Subject: [PATCH 2/2] fmt --- pybars/_compiler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pybars/_compiler.py b/pybars/_compiler.py index 996a956..5df3b19 100644 --- a/pybars/_compiler.py +++ b/pybars/_compiler.py @@ -781,8 +781,7 @@ def __init__(self): self._helpers = {} self.template_counter = 1 self._builder = CodeBuilder() - self._compiler = OMeta.makeGrammar( - compile_grammar, {'builder': self._builder}) + self._compiler = OMeta.makeGrammar(compile_grammar, {'builder': self._builder}) def _extract_word(self, source, position): """