From 558959fe2849fa8176f546e7f32cce344f4af18f Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 13 Nov 2025 11:22:45 +0100 Subject: [PATCH 1/2] [Python] Use local imports to delay pythonization library loading This should fix errors on Windows where the `libROOTPythonizations` is attempted to be loaded before the DLL directory is set correctly in `ROOT/__init__.py`. --- .../pyroot/pythonizations/python/ROOT/_facade.py | 10 ++++++---- .../python/ROOT/_pythonization/__init__.py | 4 ++-- .../python/ROOT/_pythonization/_generic.py | 12 +++++++----- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 4c005693be905..2b91ea4b6de0d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -7,10 +7,6 @@ import cppyy import cppyy.ll -from ._application import PyROOTApplication -from ._numbadeclare import _NumbaDeclareDecorator -from ._pythonization import pythonization - class PyROOTConfiguration(object): """Class for configuring PyROOT""" @@ -57,6 +53,8 @@ class ROOTFacade(types.ModuleType): """Facade class for ROOT module""" def __init__(self, module, is_ipython): + from ._pythonization import pythonization + types.ModuleType.__init__(self, module.__name__) self.module = module @@ -181,6 +179,8 @@ def _register_converters_and_executors(self): CPyCppyyRegisterExecutorAlias(name, target) def _finalSetup(self): + from ._application import PyROOTApplication + # Prevent this method from being re-entered through the gROOT wrapper self.__dict__["gROOT"] = cppyy.gbl.ROOT.GetROOT() @@ -443,6 +443,8 @@ def TMVA(self): # Create and overload Numba namespace @property def Numba(self): + from ._numbadeclare import _NumbaDeclareDecorator + cppyy.cppdef("namespace Numba {}") ns = self._fallback_getattr("Numba") ns.Declare = staticmethod(_NumbaDeclareDecorator) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/__init__.py index 49cd75788cab9..07fea57b052a4 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/__init__.py @@ -14,8 +14,6 @@ import cppyy -from ._generic import pythonize_generic - # \cond INTERNALS gbl_namespace = cppyy.gbl # \endcond @@ -70,6 +68,7 @@ def pythonization(class_name, ns="::", is_prefix=False): def passes_filter(class_name): return any(class_name.startswith(prefix) for prefix in target) + else: def passes_filter(class_name): @@ -114,6 +113,7 @@ def cppyy_pythonizor(klass, name): name (string): name of the class that is the current candidate to be pythonized. """ + from ._generic import pythonize_generic fqn = klass.__cpp_name__ diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_generic.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_generic.py index 86f4eee7d9a2c..838e4592bdfb4 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_generic.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_generic.py @@ -8,7 +8,6 @@ # For the list of contributors see $ROOTSYS/README/CREDITS. # ################################################################################ -from ROOT.libROOTPythonizations import AddPrettyPrintingPyz def _add_getitem_checked(klass): # Parameters: @@ -27,13 +26,16 @@ def getitem_checked(o, i): if i >= 0 and i < len(o): return o._getitem__unchecked(i) else: - raise IndexError('index out of range') + raise IndexError("index out of range") klass._getitem__unchecked = klass.__getitem__ klass.__getitem__ = getitem_checked + # Generic pythonizor for pretty printing that is applied to (almost) all classes def pythonize_generic(klass, name): + from ROOT.libROOTPythonizations import AddPrettyPrintingPyz + # Parameters: # klass: class to be pythonized # name: string containing the name of the class @@ -41,11 +43,11 @@ def pythonize_generic(klass, name): # Add pretty printing via setting the __str__ special function # Exclude classes which have the method __str__ already defined in C++ - m = getattr(klass, '__str__', None) - has_cpp_str = True if m is not None and type(m).__name__ == 'CPPOverload' else False + m = getattr(klass, "__str__", None) + has_cpp_str = True if m is not None and type(m).__name__ == "CPPOverload" else False # Exclude std::string with its own pythonization from cppyy - exclude = [ 'std::string' ] + exclude = ["std::string"] if name not in exclude and not has_cpp_str: AddPrettyPrintingPyz(klass) From 4ed483b76d3539891cd4bc47c1f529cf9b625384 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Wed, 12 Nov 2025 23:32:45 +0100 Subject: [PATCH 2/2] [Python] Make linter accept ROOT/__init__.py importing at the beginning --- .../pythonizations/python/ROOT/__init__.py | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/__init__.py index 45fafab663813..fd49fd369e7c4 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/__init__.py @@ -8,9 +8,21 @@ # For the list of contributors see $ROOTSYS/README/CREDITS. # ################################################################################ -import importlib +import atexit +import builtins import os import sys +import types +from importlib.abc import Loader, MetaPathFinder +from importlib.machinery import ModuleSpec +from typing import Optional, Union + +import cppyy +import cppyy.types + +from . import _asan # noqa: F401 # imported for side effects for setup specific to AddressSanitizer environments +from ._facade import ROOTFacade +from ._pythonization import _register_pythonizations # Prevent cppyy's check for extra header directory os.environ["CPPYY_API_PATH"] = "none" @@ -23,17 +35,11 @@ # the path of the ROOT library directory (only needed on Windows). For example, # if the ROOT Python module is in $ROOTSYS/bin/ROOT/__init__.py, the libraries # are usually in $ROOTSYS/bin. -if 'win32' in sys.platform: - root_module_path = os.path.dirname(__file__) # expected to be ${CMAKE_INSTALL_PYTHONDIR}/ROOT - root_install_pythondir = os.path.dirname(root_module_path) # expected to be ${CMAKE_INSTALL_PYTHONDIR} +if "win32" in sys.platform: + root_module_path = os.path.dirname(__file__) # expected to be ${CMAKE_INSTALL_PYTHONDIR}/ROOT + root_install_pythondir = os.path.dirname(root_module_path) # expected to be ${CMAKE_INSTALL_PYTHONDIR} os.add_dll_directory(root_install_pythondir) -# Do setup specific to AddressSanitizer environments -from . import _asan - -import cppyy -import cppyy.types - # Build cache of commonly used python strings (the cache is python intern, so # all strings are shared python-wide, not just in PyROOT). # See: https://docs.python.org/3.2/library/sys.html?highlight=sys.intern#sys.intern @@ -41,14 +47,11 @@ for s in ["Branch", "FitFCN", "ROOT", "SetBranchAddress", "SetFCN", "_TClass__DynamicCast", "__class__"]: _cached_strings.append(sys.intern(s)) -# Trigger the addition of the pythonizations -from ._pythonization import _register_pythonizations +# Trigger the addition of the pythonizations _register_pythonizations() # Check if we are in the IPython shell -import builtins - _is_ipython = hasattr(builtins, "__IPYTHON__") @@ -72,9 +75,6 @@ def __getitem__(self, _): __all__ = _PoisonedDunderAll() # Configure ROOT facade module -import sys -from ._facade import ROOTFacade - _root_facade = ROOTFacade(sys.modules[__name__], _is_ipython) sys.modules[__name__] = _root_facade @@ -84,9 +84,6 @@ def __getitem__(self, _): # * https://docs.python.org/3/library/importlib.html#module-importlib.abc # # * https://python.plainenglish.io/metapathfinders-or-how-to-change-python-import-behavior-a1cf3b5a13ec -from importlib.abc import Loader, MetaPathFinder -from importlib.machinery import ModuleSpec -from importlib.util import spec_from_loader def _can_be_module(obj) -> bool: @@ -108,10 +105,6 @@ def _can_be_module(obj) -> bool: return False -from typing import Optional, Union -import types - - def _lookup_root_module(fullname: str) -> Optional[Union[types.ModuleType, cppyy.types.Scope]]: """ Recursively looks up attributes of the ROOT facade, using a full module @@ -160,6 +153,8 @@ class _RootNamespaceFinder(MetaPathFinder): """ def find_spec(self, fullname: str, path, target=None) -> ModuleSpec: + from importlib.util import spec_from_loader + if not fullname.startswith("ROOT."): # This finder only finds ROOT.* return None @@ -178,11 +173,11 @@ def find_spec(self, fullname: str, path, target=None) -> ModuleSpec: ip = get_ipython() if hasattr(ip, "kernel"): - import JupyROOT + import JupyROOT # noqa: F401 # imported the side effect of setting up JupyROOT + # from . import JsMVA # Register cleanup -import atexit def cleanup(): @@ -192,4 +187,5 @@ def cleanup(): facade.__dict__["app"].keep_polling = False facade.__dict__["app"].process_root_events.join() + atexit.register(cleanup)