mkleehammer / pyodbc

Python ODBC bridge
https://github.com/mkleehammer/pyodbc/wiki
MIT No Attribution
2.92k stars 561 forks source link

AttributeError: 'pyodbc.Connection' object attribute 'cursor' is read-only #1177

Closed hb2638 closed 1 year ago

hb2638 commented 1 year ago

Environment

Issue

Is there anyway you can make pyodbc.Connection and pyodbc.Cursor patch friendly. We're running some integration tests and want to stub the "commit" functions of cursors and connections so that it's a no-op. I like this approach because everything will be in a transaction that will get rolled back after the test.

Unfortunately I get the eror AttributeError: 'pyodbc.Connection' object attribute 'cursor' is read-only when I use unittest.mock.patch.object

E.x.:

def create_no_commit_cursor(*args, **kwargs):
    return None

conn = pyodbc.connect("Driver={ODBC Driver 18 for SQL Server};TrustServerCertificate=yes;Server=.;Encrypt=yes;Trusted_Connection=yes")
self.exit_stack.enter_context(unittest.mock.patch.object(conn, "cursor", side_effect=create_no_commit_cursor))
cursor = conn.cursor()

The work around we're doing for now is creating wrapper classes for connection and cursors... not ideal

v-makouz commented 1 year ago

Is there a complete repro script? I want to see if I get that error and where is it coming from.

hb2638 commented 1 year ago

Is there a complete repro script? I want to see if I get that error and where is it coming from.

import contextlib
import typing
import unittest
import unittest.mock

import pyodbc

class MyCursor:
    def commit(self) -> typing.NoReturn:
        raise NotImplementedError()
    def rollback(self) -> typing.NoReturn:
        raise NotImplementedError()
    def close(self) -> None:
        return

class MyConnection:
    def cursor(self) -> None:
        return MyCursor()
    def commit(self) -> typing.NoReturn:
        raise NotImplementedError()
    def rollback(self) -> typing.NoReturn:
        raise NotImplementedError()
    def close(self) -> typing.NoReturn:
        raise NotImplementedError()

class MyTestCase(unittest.TestCase):
    def test_mocking(self):
        exit_stack = contextlib.ExitStack()
        conn = pyodbc.connect("Driver={ODBC Driver 18 for SQL Server};TrustServerCertificate=yes;Server=.;Trusted_Connection=yes;MARS_Connection=yes")
        #uncomment below line to see that it works if we create a wrapper class
        #conn = MyConnection()
        create_cursor = conn.cursor
        def create_no_commit_cursor() -> pyodbc.Cursor:
            cursor = create_cursor()
            exit_stack.enter_context(unittest.mock.patch.object(cursor, "commit", new=lambda: None))
            exit_stack.enter_context(unittest.mock.patch.object(cursor, "rollback", new=lambda: None))
            return cursor

        exit_stack.enter_context(unittest.mock.patch.object(conn, "cursor", new=create_no_commit_cursor))
        exit_stack.enter_context(unittest.mock.patch.object(conn, "commit", new=lambda: None))
        exit_stack.enter_context(unittest.mock.patch.object(conn, "rollback", new=lambda: None))
        exit_stack.enter_context(unittest.mock.patch.object(conn, "close", new=lambda: None))
        cursor = conn.cursor()
        cursor.commit()
        cursor.rollback()
        cursor.close()

        conn.close()
        conn.commit()
        conn.rollback()

if __name__ == '__main__':
    unittest.main()

Error Traceback (most recent call last): File "C:\Program Files\Python311\Lib\unittest\mock.py", line 1546, in enter setattr(self.target, self.attribute, new_attr) AttributeError: 'pyodbc.Connection' object attribute 'cursor' is read-only

During handling of the above exception, another exception occurred:

Traceback (most recent call last): File "test_stuff.py", line 40, in test_mocking exit_stack.enter_context(unittest.mock.patch.object(conn, "cursor", new=create_no_commit_cursor)) File "C:\Program Files\Python311\Lib\contextlib.py", line 502, in enter_context result = _enter(cm) ^^^^^^^^^^ File "C:\Program Files\Python311\Lib\unittest\mock.py", line 1559, in enter if not self.exit(*sys.exc_info()): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Program Files\Python311\Lib\unittest\mock.py", line 1567, in exit delattr(self.target, self.attribute) AttributeError: 'pyodbc.Connection' object attribute 'cursor' is read-only

mkleehammer commented 1 year ago

It's a C extension, so I don't think it will be possible. And, I think you will find that the wrappers are actually pretty useful. Implement them with __getattr__ instead of trying to rewrite each method and it will literally be just a few lines of code.

I always have a central function in each app to get a connection and I sometimes have debug configuration I can even wrap the connection with multiple wrappers. Some to log every SQL and parameter, or to measure timing and log out the longest queries, etc.

I'm going to close this because it isn't something we can do right now. We are considering reorganizing the code to use more Python code with C code implementing some core functions in in future versions, but it will depend on performance.