Skip to content

Commit 23a6d44

Browse files
authored
FEAT: Adding Conn.Error (#164)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#34889](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/34889) ------------------------------------------------------------------- ### Summary This pull request enhances DB-API 2.0 compliance for exception handling in the `mssql_python` package. The main changes are the addition of all standard DB-API 2.0 exception classes as attributes on the `Connection` class, refactoring error handling in the `Cursor` class to use these exceptions, and introducing comprehensive tests to verify correct behavior, inheritance, and consistency of these exception attributes. **DB-API 2.0 Exception Support** * Added all DB-API 2.0 exception classes (`Warning`, `Error`, `InterfaceError`, `DatabaseError`, `DataError`, `OperationalError`, `IntegrityError`, `InternalError`, `ProgrammingError`, `NotSupportedError`) as attributes on the `Connection` class, making it possible to catch exceptions using `connection.Error`, `connection.ProgrammingError`, etc. (`mssql_python/connection.py`) **Error Handling Improvements** * Updated the `Cursor` class to raise `InterfaceError` (instead of generic `Exception`) when operations are attempted on a closed cursor, ensuring proper use of DB-API exceptions. (`mssql_python/cursor.py`) **Testing Enhancements** * Added extensive tests to verify: - Presence and correctness of exception attributes on `Connection` instances and the class itself. - Proper inheritance hierarchy of exception classes. - Instantiation and catching of exceptions via connection attributes. - Consistency of exception attributes across multiple connections. - Real-world error handling scenarios using these attributes. - Correct error raising and handling when operating on closed cursors. (`tests/test_003_connection.py`) --------- Co-authored-by: Jahnvi Thakkar <jathakkar@microsoft.com>
1 parent 3201752 commit 23a6d44

File tree

3 files changed

+238
-6
lines changed

3 files changed

+238
-6
lines changed

mssql_python/connection.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,22 @@
1616
from mssql_python.helpers import add_driver_to_connection_str, sanitize_connection_string, log
1717
from mssql_python import ddbc_bindings
1818
from mssql_python.pooling import PoolingManager
19-
from mssql_python.exceptions import InterfaceError
2019
from mssql_python.auth import process_connection_string
2120

21+
# Import all DB-API 2.0 exception classes for Connection attributes
22+
from mssql_python.exceptions import (
23+
Warning,
24+
Error,
25+
InterfaceError,
26+
DatabaseError,
27+
DataError,
28+
OperationalError,
29+
IntegrityError,
30+
InternalError,
31+
ProgrammingError,
32+
NotSupportedError,
33+
)
34+
2235

2336
class Connection:
2437
"""
@@ -38,6 +51,19 @@ class Connection:
3851
close() -> None:
3952
"""
4053

54+
# DB-API 2.0 Exception attributes
55+
# These allow users to catch exceptions using connection.Error, connection.ProgrammingError, etc.
56+
Warning = Warning
57+
Error = Error
58+
InterfaceError = InterfaceError
59+
DatabaseError = DatabaseError
60+
DataError = DataError
61+
OperationalError = OperationalError
62+
IntegrityError = IntegrityError
63+
InternalError = InternalError
64+
ProgrammingError = ProgrammingError
65+
NotSupportedError = NotSupportedError
66+
4167
def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_before: dict = None, **kwargs) -> None:
4268
"""
4369
Initialize the connection object with the specified connection string and parameters.

mssql_python/cursor.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -437,11 +437,15 @@ def close(self) -> None:
437437
"""
438438
Close the cursor now (rather than whenever __del__ is called).
439439
440-
Raises:
441-
Error: If any operation is attempted with the cursor after it is closed.
440+
The cursor will be unusable from this point forward; an InterfaceError
441+
will be raised if any operation is attempted with the cursor.
442+
443+
Note:
444+
Unlike the current behavior, this method can be called multiple times safely.
445+
Subsequent calls to close() on an already closed cursor will have no effect.
442446
"""
443447
if self.closed:
444-
raise Exception("Cursor is already closed.")
448+
return
445449

446450
if self.hstmt:
447451
self.hstmt.free()
@@ -454,10 +458,13 @@ def _check_closed(self):
454458
Check if the cursor is closed and raise an exception if it is.
455459
456460
Raises:
457-
Error: If the cursor is closed.
461+
InterfaceError: If the cursor is closed.
458462
"""
459463
if self.closed:
460-
raise Exception("Operation cannot be performed: the cursor is closed.")
464+
raise InterfaceError(
465+
driver_error="Operation cannot be performed: the cursor is closed.",
466+
ddbc_error="Operation cannot be performed: the cursor is closed."
467+
)
461468

462469
def _create_parameter_types_list(self, parameter, param_info, parameters_list, i):
463470
"""

tests/test_003_connection.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@
2424
from mssql_python import Connection, connect, pooling
2525
import threading
2626

27+
# Import all exception classes for testing
28+
from mssql_python.exceptions import (
29+
Warning,
30+
Error,
31+
InterfaceError,
32+
DatabaseError,
33+
DataError,
34+
OperationalError,
35+
IntegrityError,
36+
InternalError,
37+
ProgrammingError,
38+
NotSupportedError,
39+
)
40+
2741
def drop_table_if_exists(cursor, table_name):
2842
"""Drop the table if it exists"""
2943
try:
@@ -485,3 +499,188 @@ def test_connection_pooling_basic(conn_str):
485499

486500
conn1.close()
487501
conn2.close()
502+
503+
# DB-API 2.0 Exception Attribute Tests
504+
def test_connection_exception_attributes_exist(db_connection):
505+
"""Test that all DB-API 2.0 exception classes are available as Connection attributes"""
506+
# Test that all required exception attributes exist
507+
assert hasattr(db_connection, 'Warning'), "Connection should have Warning attribute"
508+
assert hasattr(db_connection, 'Error'), "Connection should have Error attribute"
509+
assert hasattr(db_connection, 'InterfaceError'), "Connection should have InterfaceError attribute"
510+
assert hasattr(db_connection, 'DatabaseError'), "Connection should have DatabaseError attribute"
511+
assert hasattr(db_connection, 'DataError'), "Connection should have DataError attribute"
512+
assert hasattr(db_connection, 'OperationalError'), "Connection should have OperationalError attribute"
513+
assert hasattr(db_connection, 'IntegrityError'), "Connection should have IntegrityError attribute"
514+
assert hasattr(db_connection, 'InternalError'), "Connection should have InternalError attribute"
515+
assert hasattr(db_connection, 'ProgrammingError'), "Connection should have ProgrammingError attribute"
516+
assert hasattr(db_connection, 'NotSupportedError'), "Connection should have NotSupportedError attribute"
517+
518+
def test_connection_exception_attributes_are_classes(db_connection):
519+
"""Test that all exception attributes are actually exception classes"""
520+
# Test that the attributes are the correct exception classes
521+
assert db_connection.Warning is Warning, "Connection.Warning should be the Warning class"
522+
assert db_connection.Error is Error, "Connection.Error should be the Error class"
523+
assert db_connection.InterfaceError is InterfaceError, "Connection.InterfaceError should be the InterfaceError class"
524+
assert db_connection.DatabaseError is DatabaseError, "Connection.DatabaseError should be the DatabaseError class"
525+
assert db_connection.DataError is DataError, "Connection.DataError should be the DataError class"
526+
assert db_connection.OperationalError is OperationalError, "Connection.OperationalError should be the OperationalError class"
527+
assert db_connection.IntegrityError is IntegrityError, "Connection.IntegrityError should be the IntegrityError class"
528+
assert db_connection.InternalError is InternalError, "Connection.InternalError should be the InternalError class"
529+
assert db_connection.ProgrammingError is ProgrammingError, "Connection.ProgrammingError should be the ProgrammingError class"
530+
assert db_connection.NotSupportedError is NotSupportedError, "Connection.NotSupportedError should be the NotSupportedError class"
531+
532+
def test_connection_exception_inheritance(db_connection):
533+
"""Test that exception classes have correct inheritance hierarchy"""
534+
# Test inheritance hierarchy according to DB-API 2.0
535+
536+
# All exceptions inherit from Error (except Warning)
537+
assert issubclass(db_connection.InterfaceError, db_connection.Error), "InterfaceError should inherit from Error"
538+
assert issubclass(db_connection.DatabaseError, db_connection.Error), "DatabaseError should inherit from Error"
539+
540+
# Database exceptions inherit from DatabaseError
541+
assert issubclass(db_connection.DataError, db_connection.DatabaseError), "DataError should inherit from DatabaseError"
542+
assert issubclass(db_connection.OperationalError, db_connection.DatabaseError), "OperationalError should inherit from DatabaseError"
543+
assert issubclass(db_connection.IntegrityError, db_connection.DatabaseError), "IntegrityError should inherit from DatabaseError"
544+
assert issubclass(db_connection.InternalError, db_connection.DatabaseError), "InternalError should inherit from DatabaseError"
545+
assert issubclass(db_connection.ProgrammingError, db_connection.DatabaseError), "ProgrammingError should inherit from DatabaseError"
546+
assert issubclass(db_connection.NotSupportedError, db_connection.DatabaseError), "NotSupportedError should inherit from DatabaseError"
547+
548+
def test_connection_exception_instantiation(db_connection):
549+
"""Test that exception classes can be instantiated from Connection attributes"""
550+
# Test that we can create instances of exceptions using connection attributes
551+
warning = db_connection.Warning("Test warning", "DDBC warning")
552+
assert isinstance(warning, db_connection.Warning), "Should be able to create Warning instance"
553+
assert "Test warning" in str(warning), "Warning should contain driver error message"
554+
555+
error = db_connection.Error("Test error", "DDBC error")
556+
assert isinstance(error, db_connection.Error), "Should be able to create Error instance"
557+
assert "Test error" in str(error), "Error should contain driver error message"
558+
559+
interface_error = db_connection.InterfaceError("Interface error", "DDBC interface error")
560+
assert isinstance(interface_error, db_connection.InterfaceError), "Should be able to create InterfaceError instance"
561+
assert "Interface error" in str(interface_error), "InterfaceError should contain driver error message"
562+
563+
db_error = db_connection.DatabaseError("Database error", "DDBC database error")
564+
assert isinstance(db_error, db_connection.DatabaseError), "Should be able to create DatabaseError instance"
565+
assert "Database error" in str(db_error), "DatabaseError should contain driver error message"
566+
567+
def test_connection_exception_catching_with_connection_attributes(db_connection):
568+
"""Test that we can catch exceptions using Connection attributes in multi-connection scenarios"""
569+
cursor = db_connection.cursor()
570+
571+
try:
572+
# Test catching InterfaceError using connection attribute
573+
cursor.close()
574+
cursor.execute("SELECT 1") # Should raise InterfaceError on closed cursor
575+
pytest.fail("Should have raised an exception")
576+
except db_connection.InterfaceError as e:
577+
assert "closed" in str(e).lower(), "Error message should mention closed cursor"
578+
except Exception as e:
579+
pytest.fail(f"Should have caught InterfaceError, but got {type(e).__name__}: {e}")
580+
581+
def test_connection_exception_error_handling_example(db_connection):
582+
"""Test real-world error handling example using Connection exception attributes"""
583+
cursor = db_connection.cursor()
584+
585+
try:
586+
# Try to create a table with invalid syntax (should raise ProgrammingError)
587+
cursor.execute("CREATE INVALID TABLE syntax_error")
588+
pytest.fail("Should have raised ProgrammingError")
589+
except db_connection.ProgrammingError as e:
590+
# This is the expected exception for syntax errors
591+
assert "syntax" in str(e).lower() or "incorrect" in str(e).lower() or "near" in str(e).lower(), "Should be a syntax-related error"
592+
except db_connection.DatabaseError as e:
593+
# ProgrammingError inherits from DatabaseError, so this might catch it too
594+
# This is acceptable according to DB-API 2.0
595+
pass
596+
except Exception as e:
597+
pytest.fail(f"Expected ProgrammingError or DatabaseError, got {type(e).__name__}: {e}")
598+
599+
def test_connection_exception_multi_connection_scenario(conn_str):
600+
"""Test exception handling in multi-connection environment"""
601+
# Create two separate connections
602+
conn1 = connect(conn_str)
603+
conn2 = connect(conn_str)
604+
605+
try:
606+
cursor1 = conn1.cursor()
607+
cursor2 = conn2.cursor()
608+
609+
# Close first connection but try to use its cursor
610+
conn1.close()
611+
612+
try:
613+
cursor1.execute("SELECT 1")
614+
pytest.fail("Should have raised an exception")
615+
except conn1.InterfaceError as e:
616+
# Using conn1.InterfaceError even though conn1 is closed
617+
# The exception class attribute should still be accessible
618+
assert "closed" in str(e).lower(), "Should mention closed cursor"
619+
except Exception as e:
620+
pytest.fail(f"Expected InterfaceError from conn1 attributes, got {type(e).__name__}: {e}")
621+
622+
# Second connection should still work
623+
cursor2.execute("SELECT 1")
624+
result = cursor2.fetchone()
625+
assert result[0] == 1, "Second connection should still work"
626+
627+
# Test using conn2 exception attributes
628+
try:
629+
cursor2.execute("SELECT * FROM nonexistent_table_12345")
630+
pytest.fail("Should have raised an exception")
631+
except conn2.ProgrammingError as e:
632+
# Using conn2.ProgrammingError for table not found
633+
assert "nonexistent_table_12345" in str(e) or "object" in str(e).lower() or "not" in str(e).lower(), "Should mention the missing table"
634+
except conn2.DatabaseError as e:
635+
# Acceptable since ProgrammingError inherits from DatabaseError
636+
pass
637+
except Exception as e:
638+
pytest.fail(f"Expected ProgrammingError or DatabaseError from conn2, got {type(e).__name__}: {e}")
639+
640+
finally:
641+
try:
642+
if not conn1._closed:
643+
conn1.close()
644+
except:
645+
pass
646+
try:
647+
if not conn2._closed:
648+
conn2.close()
649+
except:
650+
pass
651+
652+
def test_connection_exception_attributes_consistency(conn_str):
653+
"""Test that exception attributes are consistent across multiple Connection instances"""
654+
conn1 = connect(conn_str)
655+
conn2 = connect(conn_str)
656+
657+
try:
658+
# Test that the same exception classes are referenced by different connections
659+
assert conn1.Error is conn2.Error, "All connections should reference the same Error class"
660+
assert conn1.InterfaceError is conn2.InterfaceError, "All connections should reference the same InterfaceError class"
661+
assert conn1.DatabaseError is conn2.DatabaseError, "All connections should reference the same DatabaseError class"
662+
assert conn1.ProgrammingError is conn2.ProgrammingError, "All connections should reference the same ProgrammingError class"
663+
664+
# Test that the classes are the same as module-level imports
665+
assert conn1.Error is Error, "Connection.Error should be the same as module-level Error"
666+
assert conn1.InterfaceError is InterfaceError, "Connection.InterfaceError should be the same as module-level InterfaceError"
667+
assert conn1.DatabaseError is DatabaseError, "Connection.DatabaseError should be the same as module-level DatabaseError"
668+
669+
finally:
670+
conn1.close()
671+
conn2.close()
672+
673+
def test_connection_exception_attributes_comprehensive_list():
674+
"""Test that all DB-API 2.0 required exception attributes are present on Connection class"""
675+
# Test at the class level (before instantiation)
676+
required_exceptions = [
677+
'Warning', 'Error', 'InterfaceError', 'DatabaseError',
678+
'DataError', 'OperationalError', 'IntegrityError',
679+
'InternalError', 'ProgrammingError', 'NotSupportedError'
680+
]
681+
682+
for exc_name in required_exceptions:
683+
assert hasattr(Connection, exc_name), f"Connection class should have {exc_name} attribute"
684+
exc_class = getattr(Connection, exc_name)
685+
assert isinstance(exc_class, type), f"Connection.{exc_name} should be a class"
686+
assert issubclass(exc_class, Exception), f"Connection.{exc_name} should be an Exception subclass"

0 commit comments

Comments
 (0)