Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,30 @@
# For the licensing terms see $ROOTSYS/LICENSE. #
# For the list of contributors see $ROOTSYS/README/CREDITS. #
################################################################################

import functools

from . import pythonization

def _TColor_constructor(self, *args, **kwargs):

def _tcolor_constructor(original_init):
"""
Forward the arguments to the C++ constructor and retain ownership. This
helps avoiding double deletes due to ROOT automatic memory management.
Wrapper for TColor constructor that retains ownership to avoid double deletes.
Uses functools.wraps to preserve the original function's attributes.
"""
self._cpp_constructor(*args, **kwargs)
import ROOT
ROOT.SetOwnership(self, False)
@functools.wraps(original_init)
def wrapper(self, *args, **kwargs):
"""
Forward the arguments to the C++ constructor and retain ownership. This
helps avoiding double deletes due to ROOT automatic memory management.
"""
original_init(self, *args, **kwargs)
import ROOT
ROOT.SetOwnership(self, False)
return wrapper


@pythonization("TColor")
def pythonize_tcolor(klass):
klass._cpp_constructor = klass.__init__
klass.__init__ = _TColor_constructor
klass.__init__ = _tcolor_constructor(klass.__init__)

3 changes: 2 additions & 1 deletion roottest/python/JupyROOT/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ set(NOTEBOOKS importROOT.ipynb
simpleCppMagic.ipynb
thread_local.ipynb
ROOT_kernel.ipynb
tpython.ipynb)
tpython.ipynb
tcolor_definedcolors.ipynb)

# Test all modules with doctest. All new tests will be automatically picked up
file(GLOB pyfiles ${MODULES_LOCATION}/*.py)
Expand Down
82 changes: 82 additions & 0 deletions roottest/python/JupyROOT/tcolor_definedcolors.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "b8ef0e09",
"metadata": {},
"outputs": [],
"source": [
"import ROOT"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ac871508",
"metadata": {},
"outputs": [],
"source": [
"ROOT.TColor.DefinedColors(1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4800d2c7",
"metadata": {},
"outputs": [],
"source": [
"c = ROOT.TCanvas(\"c\", \"Basic ROOT Plot\", 800, 600)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f3900d8a",
"metadata": {},
"outputs": [],
"source": [
"h = ROOT.TH1F(\"h\", \"Example Histogram;X axis;Entries\", 100, 0, 10)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d83cc9da",
"metadata": {},
"outputs": [],
"source": [
"for _ in range(10000):\n",
" h.Fill(ROOT.gRandom.Gaus(5, 1))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1df7d032",
"metadata": {},
"outputs": [],
"source": [
"h.Draw()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3743fddc",
"metadata": {},
"outputs": [],
"source": [
"c.Draw()"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
111 changes: 111 additions & 0 deletions roottest/python/JupyROOT/test_tcolor_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Test script to verify that TColor.DefinedColors works correctly in Jupyter-like environments.
This test verifies the fix for issue #20018.
"""
import sys


def test_tcolor_metadata_preservation():
"""
Test that TColor.__init__ preserves metadata after pythonization.
This ensures that introspection-heavy environments like Jupyter can
properly inspect the method.
"""
import ROOT

# Check that __init__ has the expected attributes from functools.wraps
assert hasattr(ROOT.TColor.__init__, '__wrapped__'), \
"TColor.__init__ should have __wrapped__ attribute from functools.wraps"

# Check that __name__ is preserved
assert hasattr(ROOT.TColor.__init__, '__name__'), \
"TColor.__init__ should have __name__ attribute"

# Check that __doc__ is preserved
assert hasattr(ROOT.TColor.__init__, '__doc__'), \
"TColor.__init__ should have __doc__ attribute"

print("Metadata preservation test: PASSED")
return True

def test_tcolor_definedcolors():
"""
Test the original issue: TColor.DefinedColors(1) should work without errors.
"""
import ROOT

try:
# This was the problematic call in Jupyter notebooks
result = ROOT.TColor.DefinedColors(1)
print(f"TColor.DefinedColors(1) returned: {result}")
print("DefinedColors test: PASSED")
return True
except Exception as e:
print(f"TColor.DefinedColors(1) failed with error: {e}")
print("DefinedColors test: FAILED")
return False

def test_tcolor_full_workflow():
"""
Test the full workflow from the original issue report.
"""
import ROOT

try:
# Create a canvas
ROOT.TColor.DefinedColors(1)
c = ROOT.TCanvas("c", "Basic ROOT Plot", 800, 600)

# Create a histogram with 100 bins from 0 to 10
h = ROOT.TH1F("h", "Example Histogram;X axis;Entries", 100, 0, 10)

# Fill histogram with random Gaussian numbers
for _ in range(10000):
h.Fill(ROOT.gRandom.Gaus(5, 1))

# Draw the histogram
h.Draw()

# Draw canvas
c.Draw()

print("Full workflow test: PASSED")
return True
except Exception as e:
print(f"Full workflow test failed with error: {e}")
print("Full workflow test: FAILED")
return False

def main():
"""
Run all tests and return exit code.
"""
print("=" * 60)
print("Testing TColor metadata preservation fix for issue #20018")
print("=" * 60)

tests = [
test_tcolor_metadata_preservation,
test_tcolor_definedcolors,
test_tcolor_full_workflow
]

results = []
for test in tests:
print(f"\nRunning {test.__name__}...")
try:
result = test()
results.append(result)
except Exception as e:
print(f"Test {test.__name__} raised exception: {e}")
results.append(False)

print("\n" + "=" * 60)
print(f"Test Results: {sum(results)}/{len(results)} passed")
print("=" * 60)

return 0 if all(results) else 1

if __name__ == "__main__":
sys.exit(main())
Loading
Loading