diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py index 8b68a8824d475..df40384ba1776 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcolor.py @@ -7,20 +7,23 @@ # 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): - """ - Forward the arguments to the C++ constructor and retain ownership. This - helps avoiding double deletes due to ROOT automatic memory management. - """ - self._cpp_constructor(*args, **kwargs) - import ROOT - ROOT.SetOwnership(self, False) +def _tcolor_constructor(original_init): + @functools.wraps(original_init) + def wrapper(self, *args, **kwargs): + 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__) diff --git a/roottest/python/JupyROOT/CMakeLists.txt b/roottest/python/JupyROOT/CMakeLists.txt index 828055119d733..c3bd55d15c88d 100644 --- a/roottest/python/JupyROOT/CMakeLists.txt +++ b/roottest/python/JupyROOT/CMakeLists.txt @@ -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) diff --git a/roottest/python/JupyROOT/tcolor_definedcolors.ipynb b/roottest/python/JupyROOT/tcolor_definedcolors.ipynb new file mode 100644 index 0000000000000..8363e165d6e87 --- /dev/null +++ b/roottest/python/JupyROOT/tcolor_definedcolors.ipynb @@ -0,0 +1,95 @@ +{ + "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": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/roottest/python/JupyROOT/test_tcolor_metadata.py b/roottest/python/JupyROOT/test_tcolor_metadata.py new file mode 100644 index 0000000000000..35d8e6cc64ffa --- /dev/null +++ b/roottest/python/JupyROOT/test_tcolor_metadata.py @@ -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())