From 40871452f2db5f269093b43035b7499cc0912068 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 18:48:08 +0000 Subject: [PATCH 1/2] Add threading lock to serialize Pango/Cairo and font registration calls The registered_fonts global set is read during text2svg rendering and written during font registration, with no synchronization. Concurrent calls can cause RuntimeError from set mutation during iteration. Additionally, Pango's default font map is shared global state accessed by all rendering calls. Wrap text2svg, MarkupUtils.text2svg, register_font, unregister_font, fc_register_font, fc_unregister_font, and list_fonts with a single module-level threading.Lock to serialize access to these shared resources. https://claude.ai/code/session_012X3a138VZEKecNvQuoyFmm --- manimpango/__init__.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/manimpango/__init__.py b/manimpango/__init__.py index 70987451..25daf173 100644 --- a/manimpango/__init__.py +++ b/manimpango/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os import sys +import threading from ._version import __version__ # noqa: F403,F401 @@ -10,6 +11,13 @@ f"{os.pathsep}" f"{os.environ['PATH']}" ) + +# Module-level lock for thread safety. Pango/Cairo use global state (the default +# PangoCairoFontMap, Fontconfig config, etc.) that is not thread-safe. Additionally, +# the `registered_fonts` set is shared mutable state read during rendering and written +# during font registration. This lock serializes all access to these shared resources. +_pango_lock = threading.Lock() + try: from .register_font import * # isort:skip # noqa: F403,F401 from .cmanimpango import * # noqa: F403,F401 @@ -33,3 +41,50 @@ Original error: {ie} """ raise ImportError(msg) +else: + # Wrap public API functions with the lock for thread safety. + # The star imports above bring in the unlocked implementations; + # the definitions below shadow them with locked versions. + + from .cmanimpango import text2svg as _text2svg_impl + from .cmanimpango import MarkupUtils as _MarkupUtils + from .register_font import ( + register_font as _register_font_impl, + unregister_font as _unregister_font_impl, + fc_register_font as _fc_register_font_impl, + fc_unregister_font as _fc_unregister_font_impl, + list_fonts as _list_fonts_impl, + ) + + def text2svg(*args, **kwargs): + with _pango_lock: + return _text2svg_impl(*args, **kwargs) + + def register_font(font_path): + with _pango_lock: + return _register_font_impl(font_path) + + def unregister_font(font_path): + with _pango_lock: + return _unregister_font_impl(font_path) + + def fc_register_font(font_path): + with _pango_lock: + return _fc_register_font_impl(font_path) + + def fc_unregister_font(font_path): + with _pango_lock: + return _fc_unregister_font_impl(font_path) + + def list_fonts(): + with _pango_lock: + return _list_fonts_impl() + + # Wrap MarkupUtils.text2svg (a @staticmethod on a plain Python class) + _markup_text2svg_impl = _MarkupUtils.text2svg + + def _locked_markup_text2svg(*args, **kwargs): + with _pango_lock: + return _markup_text2svg_impl(*args, **kwargs) + + _MarkupUtils.text2svg = staticmethod(_locked_markup_text2svg) From 334fdde5ebf1bf307cbb142b5c6d656b69e73db0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 18:51:39 +0000 Subject: [PATCH 2/2] Refactor: replace star imports with explicit imports and _synchronized decorator Replace the star-import-then-shadow pattern with explicit imports for all public names, and a reusable _synchronized decorator that wraps functions with the thread-safety lock. This eliminates the try/except/else gymnastics and makes the wrapping declarative and readable. https://claude.ai/code/session_012X3a138VZEKecNvQuoyFmm --- manimpango/__init__.py | 100 +++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 54 deletions(-) diff --git a/manimpango/__init__.py b/manimpango/__init__.py index 25daf173..b1481f22 100644 --- a/manimpango/__init__.py +++ b/manimpango/__init__.py @@ -2,8 +2,9 @@ import os import sys import threading +from functools import wraps -from ._version import __version__ # noqa: F403,F401 +from ._version import __version__ # noqa: F401 if os.name == "nt": # pragma: no cover os.environ["PATH"] = ( @@ -12,16 +13,46 @@ f"{os.environ['PATH']}" ) -# Module-level lock for thread safety. Pango/Cairo use global state (the default -# PangoCairoFontMap, Fontconfig config, etc.) that is not thread-safe. Additionally, -# the `registered_fonts` set is shared mutable state read during rendering and written -# during font registration. This lock serializes all access to these shared resources. +# Module-level lock for thread safety. Pango/Cairo access global state (the default +# PangoCairoFontMap, Fontconfig config, etc.) and the `registered_fonts` set is shared +# mutable state read during rendering and written during font registration. This lock +# serializes all access to these shared resources. _pango_lock = threading.Lock() + +def _synchronized(func): + @wraps(func) + def wrapper(*args, **kwargs): + with _pango_lock: + return func(*args, **kwargs) + return wrapper + + try: - from .register_font import * # isort:skip # noqa: F403,F401 - from .cmanimpango import * # noqa: F403,F401 - from .enums import * # noqa: F403,F401 + from .register_font import ( # noqa: F401 + registered_fonts, + RegisteredFont, + ) + from .cmanimpango import ( # noqa: F401 + TextSetting, + MarkupUtils, + pango_version, + cairo_version, + ) + from .cmanimpango import text2svg as _text2svg_impl + from .register_font import ( + register_font as _register_font_impl, + unregister_font as _unregister_font_impl, + fc_register_font as _fc_register_font_impl, + fc_unregister_font as _fc_unregister_font_impl, + list_fonts as _list_fonts_impl, + ) + from .enums import ( # noqa: F401 + Style, + Weight, + Variant, + Alignment, + ) except ImportError as ie: # pragma: no cover py_ver = ".".join(map(str, sys.version_info[:3])) msg = f""" @@ -41,50 +72,11 @@ Original error: {ie} """ raise ImportError(msg) -else: - # Wrap public API functions with the lock for thread safety. - # The star imports above bring in the unlocked implementations; - # the definitions below shadow them with locked versions. - - from .cmanimpango import text2svg as _text2svg_impl - from .cmanimpango import MarkupUtils as _MarkupUtils - from .register_font import ( - register_font as _register_font_impl, - unregister_font as _unregister_font_impl, - fc_register_font as _fc_register_font_impl, - fc_unregister_font as _fc_unregister_font_impl, - list_fonts as _list_fonts_impl, - ) - - def text2svg(*args, **kwargs): - with _pango_lock: - return _text2svg_impl(*args, **kwargs) - - def register_font(font_path): - with _pango_lock: - return _register_font_impl(font_path) - - def unregister_font(font_path): - with _pango_lock: - return _unregister_font_impl(font_path) - - def fc_register_font(font_path): - with _pango_lock: - return _fc_register_font_impl(font_path) - - def fc_unregister_font(font_path): - with _pango_lock: - return _fc_unregister_font_impl(font_path) - - def list_fonts(): - with _pango_lock: - return _list_fonts_impl() - - # Wrap MarkupUtils.text2svg (a @staticmethod on a plain Python class) - _markup_text2svg_impl = _MarkupUtils.text2svg - - def _locked_markup_text2svg(*args, **kwargs): - with _pango_lock: - return _markup_text2svg_impl(*args, **kwargs) - _MarkupUtils.text2svg = staticmethod(_locked_markup_text2svg) +text2svg = _synchronized(_text2svg_impl) +register_font = _synchronized(_register_font_impl) +unregister_font = _synchronized(_unregister_font_impl) +fc_register_font = _synchronized(_fc_register_font_impl) +fc_unregister_font = _synchronized(_fc_unregister_font_impl) +list_fonts = _synchronized(_list_fonts_impl) +MarkupUtils.text2svg = staticmethod(_synchronized(MarkupUtils.text2svg))