Skip to content

Review/memory management #1439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 12, 2025
Merged

Review/memory management #1439

merged 4 commits into from
Jun 12, 2025

Conversation

tpaviot
Copy link
Owner

@tpaviot tpaviot commented Jun 12, 2025

Summary by Sourcery

Modernize SWIG interface by dropping Python 2 support, refactoring IO stream conversions, and improving memory and exception management: add debug counters for handle ref tracking, refine handle utilities, map OpenCASCADE exceptions to precise Python errors, and update tests accordingly.

New Features:

  • Introduce DEBUG_MEMORY counters for SWIG handle creation and deletion
  • Add SWIG helpers Handle_ForceRelease and Handle_GetRefCount for handle introspection

Enhancements:

  • Refactor IOStream typemaps for Python 3 str/bytes handling and robust error checking
  • Improve std::ostream to Python return mapping, combining outputs into tuples
  • Enhance SWIG handle reference-count logic with null checks and detailed DownCast error messages
  • Remove Python 2 support from SWIG interfaces

Tests:

  • Remove outdated exception tests and add new tests mapping OpenCASCADE errors to specific Python exceptions

Copy link

sourcery-ai bot commented Jun 12, 2025

Reviewer's Guide

This PR refactors the SWIG interface to target Python 3 only, overhauling IO stream typemaps for strict str/bytes handling, extends handle typemaps with debug reference-count tracking and utility methods, and enhances exception wrappers to map OpenCASCADE errors to specific Python exception types with richer context.

Sequence Diagram for Enhanced OpenCASCADE Exception Handling

sequenceDiagram
    actor Developer
    Developer->>PythonApp: Call OCC_Operation()
    PythonApp->>SWIGWrapper: OCC_Operation()
    SWIGWrapper->>OpenCASCADELib: NativeCall()
    OpenCASCADELib-->>SWIGWrapper: throws Standard_Failure (e.g., Standard_OutOfRange)
    SWIGWrapper->>ExceptionHandler: process_opencascade_exception(error, "$name", "$parentclassname")
    ExceptionHandler->>ExceptionHandler: get_exception_type(error)
    ExceptionHandler-->>SWIGWrapper: Returns PyExc_IndexError (example)
    SWIGWrapper->>PythonApp: Raises IndexError("OpenCASCADE Error [Standard_OutOfRange]: Description (in Class::Method)")
    PythonApp-->>Developer: Receives specific Python IndexError
Loading

Sequence Diagram for Refactored Python to C++ Stream Input

sequenceDiagram
    actor Developer
    Developer->>PythonApp: call_occ_func_with_stream_data("some string data")
    PythonApp->>SWIGTypemap_IStream: Pass Python str/bytes for std::istream&
    alt Input is Python str
        SWIGTypemap_IStream->>SWIGTypemap_IStream: PyUnicode_AsEncodedString(input, "UTF-8", "strict")
        opt Encoding Fails
            SWIGTypemap_IStream-->>PythonApp: Raise UnicodeError
        end
        SWIGTypemap_IStream->>SWIGTypemap_IStream: PyBytes_AsString(encoded_bytes)
    else Input is Python bytes
        SWIGTypemap_IStream->>SWIGTypemap_IStream: PyBytes_AsString(input)
    end
    opt Data Extraction Fails
        SWIGTypemap_IStream-->>PythonApp: Raise ValueError
    end
    SWIGTypemap_IStream->>SWIGTypemap_IStream: Create std::string cpp_data(data_ptr)
    SWIGTypemap_IStream->>OpenCASCADELib: Pass new std::stringstream(cpp_data)
    OpenCASCADELib->>OpenCASCADELib: Process stream data
    OpenCASCADELib-->>SWIGTypemap_IStream: Return
    SWIGTypemap_IStream->>SWIGTypemap_IStream: delete stringstream (freearg)
    SWIGTypemap_IStream-->>PythonApp: Return from function
    PythonApp-->>Developer: Result
Loading

Class Diagram for OpenCASCADE Exception to Python Exception Mapping

classDiagram
  class Standard_Failure {
    <<OpenCASCADE Error>>
    +DynamicType() Name
    +GetMessageString()
  }
  class Standard_OutOfRange
  class Standard_TypeMismatch
  class Standard_NullObject
  class Standard_NotImplemented
  class Standard_OutOfMemory

  Standard_Failure <|-- Standard_OutOfRange
  Standard_Failure <|-- Standard_TypeMismatch
  Standard_Failure <|-- Standard_NullObject
  Standard_Failure <|-- Standard_NotImplemented
  Standard_Failure <|-- Standard_OutOfMemory

  class PythonException {
    <<Python Built-in>>
  }
  class IndexError
  class TypeError
  class ValueError
  class NotImplementedError
  class MemoryError

  PythonException <|-- IndexError
  PythonException <|-- TypeError
  PythonException <|-- ValueError
  PythonException <|-- NotImplementedError
  PythonException <|-- MemoryError

  class SWIGExceptionMapper {
    <<SWIG Utility>>
    +get_exception_type(occ_error: Standard_Failure) : PythonExceptionType
    +process_opencascade_exception(occ_error: Standard_Failure, method: string, class: string)
  }

  SWIGExceptionMapper ..> Standard_OutOfRange : maps to IndexError
  SWIGExceptionMapper ..> Standard_TypeMismatch : maps to TypeError
  SWIGExceptionMapper ..> Standard_NullObject : maps to ValueError
  SWIGExceptionMapper ..> Standard_NotImplemented : maps to NotImplementedError
  SWIGExceptionMapper ..> Standard_OutOfMemory : maps to MemoryError
Loading

File-Level Changes

Change Details Files
Refactor IOStream typemaps for Python 3-only strict str/bytes support
  • Added runtime checks for Unicode/bytes inputs
  • Unified encoding logic with detailed error messages
  • Rewrote input/output typemaps to use stringstream with proper memory management
  • Improved tuple-combination logic in output mapping
  • Simplified freearg cleanup using static_cast
src/SWIG_files/common/IOStream.i
Extend OccHandle typemaps with debug counters and utility methods
  • Introduced global creation/deletion counters under DEBUG_MEMORY
  • Hooked into all out typemaps to increment creation count
  • Added unref feature that increments deletion count when handles are deleted
  • Exposed Handle_ForceRelease and Handle_GetRefCount in the interface
  • Enhanced downcast to report dynamic type in error messages
src/SWIG_files/common/OccHandle.i
Enhance OpenCASCADE exception mapping to Python error types
  • Introduced helper functions for readable class/method names
  • Mapped Standard_Failure subclasses to specific Python exceptions
  • Rewrote process_exception into process_opencascade_exception with richer context
  • Added std::bad_alloc catch mapping to MemoryError
  • Enabled optional debug logging via PYTHONOCC_DEBUG_EXCEPTIONS
src/SWIG_files/common/ExceptionCatcher.i
Update and add tests for exception-to-Python mapping
  • Removed outdated exception tests from wrapper features suite
  • Added dedicated test_core_exception.py covering multiple error mappings
  • Validated IndexError, ValueError, KeyError, MemoryError and RuntimeError cases
test/test_core_wrapper_features.py
test/test_core_exception.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@tpaviot tpaviot merged commit 4957e27 into master Jun 12, 2025
1 of 14 checks passed
@tpaviot tpaviot deleted the review/memory-management branch June 12, 2025 09:34
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tpaviot - I've reviewed your changes - here's some feedback:

  • The duplicated typemap(in) implementations for std::istream and std::stringstream could be refactored into a shared helper or macro to reduce code duplication.
  • Mapping exception types via substring matching on names is brittle—consider using direct DynamicType() comparisons or a lookup table for more robust exception mapping.
  • The global DEBUG_MEMORY counters are not thread-safe; if the library may be used in multi-threaded contexts consider using atomic counters or synchronization.
Here's what I looked at during the review
  • 🟡 General issues: 4 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

// Standard_IStream
//=============================================================================
// Input stream conversion: Python str/bytes -> std::istream&
//=============================================================================
%typemap(in) std::istream& {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Duplicate typemap logic for istream and stringstream

Refactor the common logic into a shared helper or macro to reduce duplication and ease future maintenance.

Suggested implementation:

// Input stream conversion: Python str/bytes -> std::istream&
//=============================================================================
%typemap(in) std::istream& {
    if (!PyUnicode_Check($input) && !PyBytes_Check($input)) {
        PyErr_SetString(PyExc_TypeError, "Expected str or bytes object");
        SWIG_fail;
    }

    PyObject* encoded_bytes = nullptr;
    const char* data_ptr = nullptr;

=======
%define PYTHON_STR_OR_BYTES_TO_ISTREAM(VAR_NAME)
    if (!PyUnicode_Check($input) && !PyBytes_Check($input)) {
        PyErr_SetString(PyExc_TypeError, "Expected str or bytes object");
        SWIG_fail;
    }

    PyObject* encoded_bytes = nullptr;
    const char* data_ptr = nullptr;
    Py_ssize_t data_len = 0;

    if (PyUnicode_Check($input)) {
        encoded_bytes = PyUnicode_AsEncodedString($input, "UTF-8", "strict");
        if (!encoded_bytes) {
            SWIG_fail;
        }
        data_ptr = PyBytes_AsString(encoded_bytes);
        data_len = PyBytes_Size(encoded_bytes);
    } else {
        data_ptr = PyBytes_AsString($input);
        data_len = PyBytes_Size($input);
    }

    std::string VAR_NAME(data_ptr, data_len);
    if (encoded_bytes) {
        Py_DECREF(encoded_bytes);
    }
%enddef

//=============================================================================
// Input stream conversion: Python str/bytes -> std::istream&
//=============================================================================
%typemap(in) std::istream& {
    PYTHON_STR_OR_BYTES_TO_ISTREAM(_swig_istream_data)
    $1 = *(new std::istringstream(_swig_istream_data));
}

Add a similar typemap for std::stringstream& using the macro:

//=============================================================================
// Input stream conversion: Python str/bytes -> std::stringstream&
//=============================================================================
%typemap(in) std::stringstream& {
PYTHON_STR_OR_BYTES_TO_ISTREAM(_swig_stringstream_data)
$1 = *(new std::stringstream(_swig_stringstream_data));
}

If you have other stream types, you can use the macro in the same way, just changing the variable name and stream type as needed.

// global counter for dbug (enabled using DEBUG_MEMORY flag)
#ifdef DEBUG_MEMORY
%inline %{
static int handle_creation_count = 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Static debug counters get duplicated per module

To maintain a single global count, declare these counters as 'extern' in a header and define them in one .cpp file instead of using 'static' inside the inline block.

Suggested implementation:

%{
#include "OccHandleDebugCounters.h"
%}

%inline %{

    void reset_handle_counters() {
        handle_creation_count = 0;

  1. Create a header file OccHandleDebugCounters.h with the following content:
#ifdef DEBUG_MEMORY
extern int handle_creation_count;
extern int handle_deletion_count;
#endif
  1. In one .cpp file (e.g., OccHandleDebugCounters.cpp), add:
#ifdef DEBUG_MEMORY
int handle_creation_count = 0;
int handle_deletion_count = 0;
#endif
  1. Make sure all files that use these counters include OccHandleDebugCounters.h instead of declaring their own.

PyErr_SetString(PyExc_RuntimeError, "Failed to downcast to TYPE.");
return nullptr;
// Plus d'information dans l'erreur
PyErr_Format(PyExc_TypeError,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Error in DownCast isn’t propagated to Python

Use SWIG_fail or SWIG_exception_fail after setting the Python error to ensure the exception is properly propagated.

process_opencascade_exception(error, "$name", "$parentclassname");
SWIG_fail;
}
catch(const std::bad_alloc& e)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Add catch-all handlers for std::exception and ellipsis

Unhandled C++ exceptions may escape. Please add catch(const std::exception& e) and a final catch(...) to ensure all exceptions are converted to Python RuntimeError.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tpaviot - I've reviewed your changes - here's some feedback:

  • The string/bytes → stream conversion logic in IOStream.i is nearly identical for istream and stringstream—consider extracting the common encoding and error-checking steps into a helper to reduce duplication.
  • The DEBUG_MEMORY counters and inline functions in OccHandle.i are defined in a header—ensure they aren’t multiply instantiated across translation units and consider making them thread-safe if used in multithreaded contexts.
  • The exception-to-Python mapping in ExceptionCatcher.i could be simplified and made more maintainable by consolidating the type-name checks into a static lookup table or map rather than a long if/else chain.
Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +60 to +62
%inline %{
static int handle_creation_count = 0;
static int handle_deletion_count = 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): DEBUG_MEMORY counters are not thread-safe

If these counters are updated from multiple threads, use atomic types like std::atomic to prevent data races.

Suggested change
%inline %{
static int handle_creation_count = 0;
static int handle_deletion_count = 0;
%inline %{
#include <atomic>
static std::atomic<int> handle_creation_count{0};
static std::atomic<int> handle_deletion_count{0};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant