-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy path__init__.py
More file actions
245 lines (189 loc) · 8.96 KB
/
Copy path__init__.py
File metadata and controls
245 lines (189 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# Author: Enric Tejedor, Danilo Piparo CERN 06/2018
################################################################################
# Copyright (C) 1995-2018, Rene Brun and Fons Rademakers. #
# All rights reserved. #
# #
# For the licensing terms see $ROOTSYS/LICENSE. #
# For the list of contributors see $ROOTSYS/README/CREDITS. #
################################################################################
from __future__ import annotations
import atexit
import builtins
import os
import platform
import sys
import types
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
from importlib.metadata import PackageNotFoundError, version
from . import _asan # noqa: F401 # imported for side effects for setup specific to AddressSanitizer environments
from ._facade import ROOTFacade
from ._python_version import _root_python_version
_runtime_version = platform.python_version()
def _major_minor(v):
return ".".join(v.split(".")[:2])
# Check for Python ABI compatibility with this ROOT build. This check prevents
# hard crashes and undefined behavior, yielding helpful error messages instead.
if _major_minor(_runtime_version) != _major_minor(_root_python_version):
import textwrap
message = f"""
ROOT was built for Python {_root_python_version}, but you are running Python {_runtime_version}.
Python major.minor versions must match. Use a matching Python or ROOT build.
"""
raise ImportError(textwrap.dedent(message))
# Prevent cppyy's check for extra header directory
os.environ["CPPYY_API_PATH"] = "none"
# Prevent cppyy from filtering ROOT libraries
os.environ["CPPYY_NO_ROOT_FILTER"] = "1"
# The libROOTPythonizations CPython extension is in the same directory as the
# ROOT Python module, but to find the other ROOT libraries we need to also add
# 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}
os.add_dll_directory(root_install_pythondir)
# 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
_cached_strings = []
for s in ["Branch", "FitFCN", "ROOT", "SetBranchAddress", "SetFCN", "_TClass__DynamicCast", "__class__"]:
_cached_strings.append(sys.intern(s))
# Check if we are in the IPython shell
_is_ipython = hasattr(builtins, "__IPYTHON__")
class _PoisonedDunderAll:
"""
Dummy class used to trigger an ImportError on wildcard imports if the
`__all__` attribute of a module is an instance of this class.
"""
def __getitem__(self, _):
import textwrap
message = """
Wildcard import e.g. `from module import *` is bad practice, so it is disallowed in ROOT. Please import explicitly.
"""
raise ImportError(textwrap.dedent(message))
# Prevent `from ROOT import *` by setting the __all__ attribute to something
# that will raise an ImportError on item retrieval.
__all__ = _PoisonedDunderAll()
# Configure ROOT facade module
_root_facade = ROOTFacade(sys.modules[__name__], _is_ipython)
sys.modules[__name__] = _root_facade
# Configure meta-path finder for ROOT namespaces, following the Python
# documentation and an example:
#
# * https://docs.python.org/3/library/importlib.html#module-importlib.abc
#
# * https://python.plainenglish.io/metapathfinders-or-how-to-change-python-import-behavior-a1cf3b5a13ec
def _can_be_module(obj) -> bool:
"""
Determine if an object can be used as a Python module. This is the case for
objects that are actually of ModuleType, or C++ namespaces from cppyy.
"""
# If the type is the module type, it can trivially be a module.
if isinstance(obj, types.ModuleType):
return True
# Check if the object represents a C++ namespace. Since cppyy has no
# dedicated Python type for C++ namespaces, we check for this using the
# representation of the object.
if repr(obj).startswith("<namespace "):
return True
return False
def _lookup_root_module(fullname: str) -> Optional[Union[types.ModuleType, cppyy.types.Scope]]: # noqa: F821
"""
Recursively looks up attributes of the ROOT facade, using a full module
name, and return it if it can be used as a ROOT submodule. This is the case
if the attribute is a C++ namespace or an actual Python module type. If no
matching attribute is found, return None.
"""
keys = fullname.split(".")[1:]
ret = _root_facade
for part in keys:
ret = getattr(ret, part, None)
if ret is None or not _can_be_module(ret):
return None
return ret
class _RootNamespaceLoader(Loader):
"""
Custom loader for modules under the ROOT namespace.
"""
def is_package(self, fullname: str) -> bool:
"""
Indicates whether the given attribute of the ROOT facade can be
considered a package.
This is decided by the _lookup_root_module function.
"""
return _lookup_root_module(fullname) is not None
def create_module(self, spec: ModuleSpec):
out = _lookup_root_module(spec.name)
# Prevent wildcard import for the submodule by setting the __all__
# attribute to something that will raise an ImportError on item
# retrieval.
out.__all__ = _PoisonedDunderAll()
return out
def exec_module(self, module):
pass
class _RootNamespaceFinder(MetaPathFinder):
"""
Finder for modules under the ROOT namespace.
"""
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
if _lookup_root_module(fullname) is None:
return None
return spec_from_loader(fullname, _RootNamespaceLoader())
namespace_finder = _RootNamespaceFinder()
if namespace_finder not in sys.meta_path:
sys.meta_path.append(namespace_finder)
# Configuration for usage from Jupyter notebooks
if _is_ipython:
from IPython import get_ipython
ip = get_ipython()
if hasattr(ip, "kernel"):
from . import _jupyroot # noqa: F401 # imported the side effect of setting up JupyROOT
# from . import JsMVA
# importlib.metadata.version reads distribution metadata from the package's
# .dist-info folder, which is typically generated by the package manager.
# An installed ROOT wheel distribution will contain this metadata,
# and version("ROOT") will succeed. However, ROOT built or installed
# through any other channel (e.g. from source) has no .dist-info folder,
# so the corresponding metadata is missing, therefore version("ROOT") raises
# PackageNotFoundError, despite `import ROOT` succeeding in the same environment.
# See: https://docs.python.org/3/library/importlib.metadata.html
try:
if "a" in version("ROOT"):
import warnings
warnings.warn(
"This distribution of ROOT is in alpha stage. Feedback is welcome and appreciated. "
"Feel free to reach out to the user forum for questions and general feedback at "
"https://github.com/root-project/root/issues. "
"Do not rely on this distribution for production purposes.",
stacklevel=2, # emit the warning in the caller
)
except PackageNotFoundError:
pass
# Build every C++ module once per installation
# needed for the wheels, in other cases this is done in the CMake build step, so we skip it here
from . import _pcm_warmup
_pcm_warmup.warmup(_root_facade)
def _cleanup():
# Delete TBrowser instances while the GUI event loop is still alive,
# which fixed https://github.com/root-project/root/issues/21912.
#
# The cleanup is kept tight on purpose. A previous version called
# TROOT::EndOfProcessCleanups() outright (removed in commit e9d2803), which
# also ran gInterpreter->ResetGlobals() and ShutDown() and interfered with
# Python objects still alive at exit time, by cleaning up objects that
# might be referenced by other Python proxies outside the control of gROOT.
facade = sys.modules[__name__]
# Skip if the C++ runtime was never initialized (i.e. _finalSetup did
# not run): nothing to clean up, and we don't want to drag cppyy in.
if "_cppyy" not in facade.__dict__:
return
if not getattr(facade.PyConfig, "ShutDown", True):
return
facade.gROOT.GetListOfBrowsers().Delete()
atexit.register(_cleanup)