Skip to content

Commit 731817f

Browse files
authored
Merge branch 'master' into bvh2
2 parents ae0cce8 + 69badd2 commit 731817f

90 files changed

Lines changed: 4361 additions & 1053 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/cibuildwheel-impl/action.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ runs:
1212
using: "composite"
1313
steps:
1414
- name: Build wheel
15-
uses: pypa/cibuildwheel@v4.1.0
15+
uses: pypa/cibuildwheel@v4.3.0
1616
env:
1717
CIBW_BUILD: ${{ inputs.build-tag }}
1818

‎.github/workflows/python_wheel_build.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
strategy:
2929
fail-fast: false
3030
matrix:
31-
python: [cp311, cp312, cp313, cp314]
31+
python: [cp311, cp312, cp313, cp314, cp315]
3232
config:
3333
# `label` is a unique artifact name for each build
3434
- runner: macos-15
@@ -79,7 +79,7 @@ jobs:
7979
strategy:
8080
fail-fast: false
8181
matrix:
82-
python: ["3.11", "3.12", "3.13", "3.14"]
82+
python: ["3.11", "3.12", "3.13", "3.14", "3.15"]
8383
test_config:
8484
# each runner tests the wheel built on it
8585
- runner: macos-15

‎.github/workflows/root-ci-config/buildconfig/global.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ asan=OFF
44
asimage=ON
55
asserts=OFF
66
builtin_bvh=ON
7+
builtin_cdt=ON
78
builtin_cfitsio=OFF
89
builtin_civetweb=OFF
910
builtin_clang=ON

‎.github/workflows/utilities/sync_cling.py‎

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#!/usr/bin/env python3
22

3+
import subprocess
34
import sys
45

5-
from common import printError, printWarning, printInfo, execCommand
6+
from common import printWarning, printInfo, execCommand
67

78
# Parameters
89
CLING_TAG_ROOT_HASH_PREFIX = "__internal-root-"
@@ -50,11 +51,30 @@ def getStartingRootHash(rootTag):
5051
prefixLen = len(CLING_TAG_ROOT_HASH_PREFIX)
5152
defHashLen = len(DEFAULT_STARTING_ROOT_HASH)
5253
hash = rootTag[prefixLen : prefixLen + defHashLen]
54+
# A tag made from a local ROOT branch names a commit that is not in ROOT's history.
55+
# Then start from the ROOT commit that cling's last commit came from.
56+
if subprocess.run(["git", "merge-base", "--is-ancestor", hash, "HEAD"], cwd=ROOT_REPO_DIR_NAME).returncode != 0:
57+
printWarning(f"The sync tag names {hash}, which is not in ROOT's history")
58+
hash = findRootCommit(execCommand("git log -1 --format=%ae%x09%ad --date=raw HEAD", CLING_REPO_DIR_NAME))
5359
return hash
5460

5561

62+
def findRootCommit(identity):
63+
"""
64+
Find the ROOT commit with the given author identity. git am keeps the author
65+
and the author date, so they identify a ROOT commit in cling.
66+
"""
67+
out = execCommand(f"git log --format=%h%x09%ae%x09%ad --date=raw HEAD -- {INTERP_DIR_NAME}", ROOT_REPO_DIR_NAME)
68+
for line in out.split("\n"):
69+
hash, commitIdentity = line.split("\t", 1)
70+
if commitIdentity == identity:
71+
printInfo(f"Cling's last commit comes from ROOT's {hash}")
72+
return hash
73+
raise ValueError(f"No ROOT commit matches cling's last commit ({identity})")
74+
75+
5676
def getHashes(repoDirName, startingHash, dirInRepoName=""):
57-
out = execCommand(f"git log --oneline {startingHash}..HEAD {dirInRepoName}", thisCwd=repoDirName)
77+
out = execCommand(f"git log --oneline {startingHash}..HEAD -- {dirInRepoName}", thisCwd=repoDirName)
5878
hashes = []
5979
if not out:
6080
return hashes
@@ -75,12 +95,19 @@ def createPatches(rootHashes, interpHashes):
7595
# We loop on ROOT hashes to sync, from oldest to newest
7696
# to return a list of triples [dirInRepo, hash, patchAsSting], where dirInRepo is the
7797
# directory and hash is there for debugging purposes.
98+
# Commits that cling already has, e.g. because the sync tag names an older ROOT
99+
# commit than the one cling was synchronized to, are skipped.
100+
syncedIdentities = set(execCommand("git log -n 1000 --format=%ae%x09%ad --date=raw HEAD", CLING_REPO_DIR_NAME).split("\n"))
78101
for rootHashtoSync in reversed(rootHashesToSync):
102+
identity = execCommand(f"git log -1 --format=%ae%x09%ad --date=raw {rootHashtoSync}", ROOT_REPO_DIR_NAME)
103+
if identity in syncedIdentities:
104+
printWarning(f"Skipping {rootHashtoSync}: cling already has it")
105+
continue
79106
keys = []
80107
if rootHashtoSync in interpHashesSet:
81108
keys.append(INTERP_DIR_NAME)
82109
for key in keys:
83-
patchAsStr = execCommand(f"git format-patch -1 {rootHashtoSync} {key} --stdout", ROOT_REPO_DIR_NAME)
110+
patchAsStr = execCommand(f"git format-patch -1 {rootHashtoSync} --stdout -- {key}", ROOT_REPO_DIR_NAME)
84111
patches.append([key, rootHashtoSync, patchAsStr])
85112
return patches
86113

@@ -99,27 +126,20 @@ def syncTagAndPush(oldSyncTag, rootSyncHash):
99126
Replace the tag mentioning the previous root commit to which the repo was synchronized.
100127
Push the changes and tags upstream.
101128
"""
102-
# We fetch the remote and local tags to make the following operations more resilient
103-
remoteTags = execCommand("git ls-remote --tags origin", CLING_REPO_DIR_NAME)
104-
localTags = execCommand("git tag", CLING_REPO_DIR_NAME)
105-
106-
# Clean the old tag
107-
printInfo(f"Found a sync tag ({oldSyncTag}): deleting it.")
108-
if oldSyncTag in localTags:
109-
execCommand(f"git tag -d {oldSyncTag}", CLING_REPO_DIR_NAME)
110-
if oldSyncTag in remoteTags:
111-
execCommand(f"git push --delete origin {oldSyncTag}", CLING_REPO_DIR_NAME)
112-
else:
113-
printWarning(f"Tag {oldSyncTag} was not found in the upstream repository.")
114-
115-
# Time to push the sync commits!
116-
execCommand("git push", CLING_REPO_DIR_NAME)
117-
118-
# And finally we tag and push the tag
129+
# Push the commits and the new tag together, so that cling never has the
130+
# commits without a tag that names them. Only then remove the old tag.
119131
newTag = CLING_TAG_ROOT_HASH_PREFIX + rootSyncHash
120132
printInfo(f"Creating a new tag ({newTag}).")
121133
execCommand(f"git tag {newTag}", CLING_REPO_DIR_NAME)
122-
execCommand(f"git push origin tag {newTag}", CLING_REPO_DIR_NAME)
134+
execCommand(f"git push --atomic origin HEAD tag {newTag}", CLING_REPO_DIR_NAME)
135+
136+
printInfo(f"Deleting the previous sync tag ({oldSyncTag}).")
137+
execCommand(f"git tag -d {oldSyncTag}", CLING_REPO_DIR_NAME)
138+
remoteTags = execCommand("git ls-remote --tags origin", CLING_REPO_DIR_NAME)
139+
if f"refs/tags/{oldSyncTag}" in remoteTags:
140+
execCommand(f"git push --delete origin tag {oldSyncTag}", CLING_REPO_DIR_NAME)
141+
else:
142+
printWarning(f"Tag {oldSyncTag} was not found in the upstream repository.")
123143

124144

125145
def principal():
@@ -146,8 +166,7 @@ def principal():
146166

147167
# We now apply the patches
148168
if not patches:
149-
printError("No patch was distilled: this status should not be reachable")
150-
return 1
169+
printInfo("Cling already has all these commits: only moving the sync tag.")
151170

152171
# We now need to apply patches, update the tag that mentions the ROOT commit
153172
# to which the cling repo was synchronised and push everything.

‎CMakeLists.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,6 @@ add_subdirectory (core)
408408
add_subdirectory (builtins/zip) # hard coded builtin for core/zip
409409
add_subdirectory (builtins/clib) # hard coded builtin for core
410410
add_subdirectory (builtins/mixmax) # hard coded builtin for math/mathcore
411-
add_subdirectory (builtins/CDT) # hard coded builtin for math/mathcore
412411
add_subdirectory (math)
413412
if (fortran AND CMAKE_Fortran_COMPILER)
414413
add_subdirectory(builtins/cernlib) # hard coded builtin for hist and main

‎bindings/pyroot/pythonizations/CMakeLists.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
set(cpp_sources
1212
src/PyROOTModule.cxx
13-
src/RPyROOTApplication.cxx
1413
src/GenericPyz.cxx
1514
src/CPPInstancePyz.cxx
1615
)

‎bindings/pyroot/pythonizations/python/ROOT/_application.py‎

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
# For the list of contributors see $ROOTSYS/README/CREDITS. #
99
################################################################################
1010

11+
import ctypes
12+
import os
1113
import sys
1214
import time
1315

@@ -16,16 +18,75 @@ class PyROOTApplication(object):
1618
"""
1719
Application class for PyROOT.
1820
Configures the interactive usage of ROOT from Python.
21+
22+
It is created while the ROOT module is set up, so it reaches C++ through
23+
cppyy.gbl, as attribute lookups on the ROOT module would restart the setup.
1924
"""
2025

2126
def __init__(self, config, is_ipython):
22-
from ROOT.libROOTPythonizations import InitApplication
27+
import cppyy
28+
29+
if not cppyy.gbl.gApplication:
30+
from ROOT.libROOTPythonizations import InstallWarningHandler
2331

24-
# Construct a TApplication for PyROOT
25-
InitApplication(config.IgnoreCommandLineOptions)
32+
self._create_application(config.IgnoreCommandLineOptions)
33+
self._init_root_globals()
34+
InstallWarningHandler()
2635

2736
self._is_ipython = is_ipython
2837

38+
@staticmethod
39+
def _create_application(ignore_cmd_line_opts):
40+
"""
41+
Create the TApplication. Unless ignore_cmd_line_opts is set, it gets
42+
the command line options in sys.argv, up to a "-" or "--" that
43+
separates them from the options for the Python script. For example,
44+
to enable batch mode from the command line:
45+
46+
python script_name.py -b -- user_arg1 ... user_argn
47+
48+
or, if the script takes no options:
49+
50+
python script_name.py -b
51+
"""
52+
import cppyy
53+
54+
gbl = cppyy.gbl
55+
56+
args = ["python"]
57+
if not ignore_cmd_line_opts:
58+
for arg in getattr(sys, "argv", [])[1:]:
59+
if arg in ("-", "--"):
60+
break
61+
args.append(arg)
62+
63+
# Bytes as given on the command line, and argv[argc] null like for main()
64+
args = [os.fsencode(arg) for arg in args]
65+
argc = ctypes.c_int(len(args))
66+
argv = (ctypes.c_char_p * (len(args) + 1))(*args, None)
67+
app = gbl.TApplication("PyROOT", argc, ctypes.cast(argv, ctypes.POINTER(ctypes.c_char_p)))
68+
# It is gApplication from now on
69+
cppyy._backend.SetOwnership(app, False)
70+
71+
# Prevent crashes on accessing history
72+
gbl.Gl_histinit("-")
73+
74+
# Prevent ROOT from exiting Python
75+
app.SetReturnFromRun(True)
76+
77+
@staticmethod
78+
def _init_root_globals():
79+
"""Set up gBenchmark, the one ROOT global that neither TROOT nor TApplication sets"""
80+
import cppyy
81+
82+
gbl = cppyy.gbl
83+
84+
if not gbl.gBenchmark:
85+
# The global owns the object, so Python must not delete it
86+
benchmark = gbl.TBenchmark()
87+
cppyy._backend.SetOwnership(benchmark, False)
88+
gbl.gBenchmark = benchmark
89+
2990
@staticmethod
3091
def _ipython_config():
3192
# Integrate IPython >= 5 with ROOT's event loop

‎bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@
88
# For the list of contributors see $ROOTSYS/README/CREDITS. #
99
################################################################################
1010

11+
r"""
12+
\pythondoc RNTupleWriter
13+
14+
RNTupleWriter is an interface to write RNTuples to storage.
15+
16+
A simple way to write an RNTuple in python is by:
17+
- creating a model and adding fields to it
18+
- opening an RNTupleWriter
19+
- creating entries and filling them with data
20+
- committing entries to the RNTuple via writer.Fill(entry)
21+
22+
\code{.py}
23+
import ROOT
24+
25+
# Create the model
26+
model = ROOT.RNTupleModel.Create()
27+
28+
# Define the schema by adding fields to the model.
29+
model.MakeField['int']("foo")
30+
31+
# Create a writer as a context manager
32+
with ROOT.RNTupleWriter.Recreate(model, "myNtuple", "file.root") as writer:
33+
# create an entry and use it to write data to the RNTuple
34+
entry = writer.CreateEntry()
35+
for i in range(0,10):
36+
entry['foo'] = i
37+
writer.Fill(entry)
38+
# On destruction, the writer will flush the written data to disk
39+
40+
\endcode
41+
42+
\endpythondoc
43+
"""
44+
1145
from . import pythonization
1246
from ._pyz_utils import MethodTemplateGetter, MethodTemplateWrapper
1347

‎bindings/pyroot/pythonizations/src/PyROOTModule.cxx‎

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111

1212
// Bindings
1313
#include "PyROOTPythonize.h"
14-
#include "RPyROOTApplication.h"
1514

1615
// Cppyy
1716
#include "cpyrt/API.h"
1817

1918
// ROOT
19+
#include "TError.h"
2020
#include "TInterpreter.h"
2121
#include "TROOT.h"
2222
#include "TSystem.h"
23+
#include "TVirtualPad.h"
2324
#include "RConfigure.h"
2425

2526
// Standard
@@ -98,15 +99,68 @@ PyObject *PyObjRefCounterAsStdAny(PyObject * /*self*/, PyObject *args)
9899

99100
} // namespace PyROOT
100101

102+
namespace {
103+
104+
/// Turns a ROOT warning into a Python warning, see ROOT::Internal::SetWarningHandler().
105+
bool WarningHandler(const char *location, const char *msg)
106+
{
107+
PyGILState_STATE state = PyGILState_Ensure();
108+
// Python code can't run while an exception is pending, e.g. a previous
109+
// warning turned into an error: decline, and ROOT prints the warning.
110+
bool handled = !PyErr_Occurred();
111+
if (handled) {
112+
// Prints or, per the warnings filters, raises: the exception stays
113+
// pending until the call into C++ returns to Python.
114+
PyErr_WarnExplicit(nullptr, msg, location, 0, "ROOT", nullptr);
115+
}
116+
PyGILState_Release(state);
117+
return handled;
118+
}
119+
120+
int (*gPreviousInputHook)() = nullptr;
121+
122+
/// Processes GUI events while the Python prompt waits for input. Runs no
123+
/// Python code, so a Ctrl-C arriving meanwhile stays pending for the prompt.
124+
int GUIEventInputHook()
125+
{
126+
PyGILState_STATE state = PyGILState_Ensure();
127+
if (gPad && gPad->IsWeb())
128+
gPad->UpdateAsync();
129+
gSystem->ProcessEvents();
130+
PyGILState_Release(state);
131+
132+
return gPreviousInputHook ? gPreviousInputHook() : 0;
133+
}
134+
135+
/// Turn ROOT warnings into Python warnings.
136+
PyObject *InstallWarningHandler(PyObject * /*self*/, PyObject * /*args*/)
137+
{
138+
ROOT::Internal::SetWarningHandler(&WarningHandler);
139+
Py_RETURN_NONE;
140+
}
141+
142+
/// Install the PyOS_InputHook that processes GUI events, chaining to the
143+
/// previous hook.
144+
PyObject *InstallGUIEventInputHook(PyObject * /*self*/, PyObject * /*args*/)
145+
{
146+
if (PyOS_InputHook != &GUIEventInputHook) {
147+
gPreviousInputHook = PyOS_InputHook;
148+
PyOS_InputHook = &GUIEventInputHook;
149+
}
150+
Py_RETURN_NONE;
151+
}
152+
153+
} // namespace
154+
101155
// Methods offered by the interface
102156
static PyMethodDef gPyROOTMethods[] = {
103157
{"AddCPPInstancePickling", (PyCFunction)PyROOT::AddCPPInstancePickling, METH_NOARGS,
104158
"Add a custom pickling mechanism for Cppyy Python proxy objects"},
105159
{"AddPrettyPrintingPyz", (PyCFunction)PyROOT::AddPrettyPrintingPyz, METH_VARARGS,
106160
"Add pretty printing pythonization"},
107-
{"InitApplication", (PyCFunction)PyROOT::RPyROOTApplication::InitApplication, METH_VARARGS,
108-
"Initialize interactive ROOT use from Python"},
109-
{"InstallGUIEventInputHook", (PyCFunction)PyROOT::RPyROOTApplication::InstallGUIEventInputHook, METH_NOARGS,
161+
{"InstallWarningHandler", (PyCFunction)InstallWarningHandler, METH_NOARGS,
162+
"Turn ROOT warnings into Python warnings"},
163+
{"InstallGUIEventInputHook", (PyCFunction)InstallGUIEventInputHook, METH_NOARGS,
110164
"Install an input hook to process GUI events"},
111165
{"_CPPInstance__expand__", (PyCFunction)PyROOT::CPPInstanceExpand, METH_VARARGS,
112166
"Deserialize a pickled object"},

0 commit comments

Comments
 (0)