Uh oh!
There was an error while loading. Please reload this page.
FIX: prevent AttributeError in __del__ on partially-initialized Cursor - #646
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens mssql_python.cursor.Cursor lifecycle behavior to avoid unraisable exceptions during garbage collection when a Cursor is only partially initialized, and adds regression tests to prevent recurrence of the CI PytestUnraisableExceptionWarning seen in issue #642.
Changes:
- Establishes
Cursorcleanup invariants earlier in__init__(closed=False,hstmt=None) soclose()/__del__can run safely even after initialization failures. - Makes
Cursor.close()tolerant of instances missing theclosedattribute (e.g., objects created viaCursor.__new__). - Fixes
__del__finalization check to usesys.is_finalizing()and guards debug logging during interpreter shutdown; adds lifecycle regression tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
mssql_python/cursor.py | Makes cursor initialization/cleanup resilient to partial construction and interpreter shutdown edge cases. |
tests/test_005_connection_cursor_lifecycle.py | Adds regression tests for half-initialized cursor GC paths and failed cursor construction scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Saurabh Singh (saurabh500)
commented
Jun 27, 2026
Subrata (@subrata-ms) generally looks good to me. The copilot comment on the test is interesting. |
Gaurav Sharma (bewithgaurav)
left a comment
There was a problem hiding this comment.
fix itself lgtm but the tests aren't defending the same as of now
added a comment that should close that gap, copilot's comments are also working taking a look at
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql_python/cursor.pyLines 3271-3279 3271# Don't raise an exception in __del__, just log it3272# If interpreter is shutting down, we might not have logging set up3273importsys3274 ! 3275ifsysandsys.is_finalizing():
3276# Suppress logging during interpreter shutdown3277return3278# ``logger`` could be torn down or have its handlers closed3279# late in interpreter shutdown; guard so the debug callLines 3277-3285 3277return3278# ``logger`` could be torn down or have its handlers closed3279# late in interpreter shutdown; guard so the debug call3280# cannot itself raise an unraisable exception.
! 3281ifloggerisnotNone:
3282logger.debug("Exception during cursor cleanup in __del__: %s", e)
32833284defscroll(
3285self, value: int, mode: str="relative"📋 Files Needing Attention📉 Files with overall lowest coverage (click to expand)mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 59.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 76.3%
mssql_python.__init__.py: 77.3%
mssql_python.row.py: 77.6%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 81.4%
mssql_python.pybind.connection.connection.cpp: 83.7%
mssql_python.connection.py: 84.7%🔗 Quick Links
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Unraisable exceptions from Cursor.__del__ flow through sys.unraisablehook, not through the warnings module, so warnings.catch_warnings(record=True) never sees them — the previous test passed on unfixed cursor.py. Also drop the gc.collect() sync point in the __init__-failure test: Connection._cursors is a WeakSet, so a failed Cursor.__init__ never installs a strong ref anywhere and __del__ runs synchronously on the exception unwind out of conn.cursor(). Wrap the hook around that call only. Verified: on main (buggy cursor.py) the half-initialized test now fails with AttributeError at cursor.py:782; on this branch (fixed cursor.py) it passes clean.
Gaurav Sharma (bewithgaurav)
left a comment
There was a problem hiding this comment.
the core fix is correct, just need a minor correction on tests
Uh oh!
There was an error while loading. Please reload this page.
Binding the raised RuntimeError to an enclosing-frame local ('boom = ...')
keeps the exception's __traceback__ alive, which in turn keeps the
partial Cursor's frame alive past the sys.unraisablehook restore. __del__
then fires AFTER the hook is unwound, the hook window closes with no
unraisables captured, and the assertion silently becomes a no-op — same
dead-assertion pattern as the old catch_warnings, just reached via a
different mechanism. The test passed on unfixed cursor.py as a result.
Fix: construct the RuntimeError inline inside _raise so no long-lived
reference to it exists. Once pytest.raises exits, the exception (and its
traceback, and the partial cursor frame) become unreachable and __del__
fires synchronously inside the hook window. Verified: fails on main,
passes on this branch.
Docstring note added so nobody reintroduces the enclosing-frame local.Uh oh!
There was an error while loading. Please reload this page.
### Work Item / Issue Reference > [AB#47087](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/47087) ------------------------------------------------------------------- ### Summary Release mssql-python v1.13.0. Version bump to 1.13.0. Updates `mssql_python/__init__.py`, `setup.py`, `PyPI_Description.md`, and the README "Important Note" section. Bundled `mssql_py_core` bumped from 0.1.7 to 0.1.8 (no source/API changes — dev-nightly to stable pin). #### Enhancements - **ODBC driver ships exclusively via `mssql-python-odbc` (Phase 2)** — The `libs/` fallback introduced in v1.12.0 has been removed. `mssql-python` now hard-depends on `mssql-python-odbc==18.6.2.1`; `pip install mssql-python` still pulls the driver package transparently. Smaller wheels; driver binaries managed independently (#693). - **Apache Arrow bulk copy** — New `Cursor.bulkcopy_arrow(table_name, source)` method for high-performance bulk loading from `pyarrow.Table` / `RecordBatch` / Arrow C Data Interface sources; classic `bulkcopy()` now raises `TypeError` for Arrow inputs and steers users to the new method (#665). - **`token_provider=` parameter for Azure Identity credentials** — `connect()` accepts any credential with a `.get_token(scope)` method (`DefaultAzureCredential`, `AzureCliCredential`, `ManagedIdentityCredential`, …). Mutually exclusive with `Authentication=` in the connection string (#603, issue #577). - **Identity-aware connection pooling with token-expiry refresh** — Pool now keys on security context, preventing cross-identity connection leaks; token acquisition deferred to pool-misses; connections with near-expiry tokens refreshed automatically (#660, issues #651, #659). #### Bug Fixes - **Silent zero-row `executemany` batches on late NULLs** — Fixed numeric array parameter binding paths (`TINYINT`/`SMALLINT`/`INT`/`FLOAT`) that left indicator slots uninitialized when a NULL appeared partway through the batch (#702, issue #670). - **`SQL_WVARCHAR` output converter applied as catch-all to non-string columns** — Fallback now gated on `str`/`bytes` mapped types (#692, issue #691). - **Integer-keyed output converters silently never fired** — `add_output_converter(SQL_DECIMAL, ...)` and other integer SQL type code keys now dispatch correctly (pyodbc parity) (#690, issue #684). - **`RecordBatchReader.Close()` for Arrow result sets** — `Cursor.arrow_reader()` now returns a wrapper whose `.close()` releases server-side resources and leaves the parent cursor usable (#644, issue #643). - **`AttributeError` in `Cursor.__del__` on partially-initialized cursor** — `__init__` sets `closed`/`hstmt` before any raise; `__del__` uses correct `sys.is_finalizing()` guard (#646, issue #642). #### Version Bump - `mssql_python/__init__.py`: `__version__ = "1.13.0"` - `setup.py`: `version="1.13.0"` - `PyPI_Description.md`: `## What's new in v1.13.0` section refreshed - `README.md`: "Important Note" updated for Phase 2 (no more `libs/` fallback, `mssql-python-odbc==18.6.2.1`)
Work Item / Issue Reference
Summary
This pull request improves the robustness of the
Cursorclass initialization and cleanup logic, ensuring that partially-initialized or half-constructed cursor objects are handled safely and do not cause unraisable exceptions during garbage collection. It also adds regression tests to prevent recurrence of related bugs.Initialization and cleanup robustness:
__init__method incursor.pynow setsself.closed = Falseandself.hstmt = Noneas the very first statements, before any code that might raise an exception, ensuring that even partially-initializedCursorinstances have a consistent state for cleanup. [1][2]close()method now safely checks for the existence of theclosedattribute usinggetattr(self, "closed", True), preventingAttributeErrorif the attribute is missing (e.g., in half-initialized objects).__del__method now uses the correctsys.is_finalizing()function (instead of the incorrectsys._is_finalizing()) and guards logging calls to prevent unraisable exceptions during interpreter shutdown.Testing and regression prevention:
test_cursor_del_half_initialized_cursor_no_errorsandtest_cursor_init_failure_leaves_consistent_stateto ensure that half-initialized cursors do not raise unraisable exceptions during garbage collection and that failed initialization leaves the cursor and connection in a consistent, recoverable state.