Package: pymysqlite
pymysqlitepymysqliteacts as a simple compatibility layer that translatespymysqlcalls tosqlite3calls, allowing you to test MySQL code without having a running MySQL server on hand. Hopefully.Come again?Ever had the bright idea to add tests to your database-adjacent Python scripts only to find out that, despite having superficially similar API, you can't just use Sqlite as a drop-in replacement fixture in your tests? No? Well, pretend with me for a while.Should I use this in my project?Probably not. This only makes sense for simple projects that don't use an abstraction layer like SQLAlchemy already.Key differences inpymysqlandsqlite3The first and most glaring difference is in how each library treats its SQL placeholders - wherepymysqluses%sand%(name)sfor SQL arguments,sqlite3uses?and:nameinstead. Youdouse placeholders,right?sqlite_cursor.execute("SELECT * FROM users where id=?",(1,))# sqlite3pymysql_cursor.execute("SELECT * FROM users where id=%d",(1,))# pymysqlAnother difference is that whilepymysqlallows instantiating a cursor via a context manager,sqlite3does not:# pymysqlconn=pymysql.connect(**config)withconn.cursor()ascursor:cursor.execute("SELECT * FROM users")# sqlite3conn=sqlite3.connect(":memory:")withconn.cursor()ascursor:# fails...cursor=conn.cursor()# works okcursor.execute("SELECT * FROM users")How many rows?A common usage pattern inpymysqlis as follows:# pymysqlnum_rows=cursor.execute("SELECT * FROM users WHERE id=%s",(id,))ifnum_rows:user=cursor.fetchone()Unfortunately,sqlite3has a... difficult relationship when it comes to knowing how many rows a cursor returns. For one, the.execute()method does not return the number of rows affected, instead returning the cursor instance:# sqlite3res=cursor.execute("SELECT * FROM users")resiscursor# Trueres.rowcount==-1# TrueFurthermore, the.rowcountattribute only returns correct values forINSERT,UPDATE,DELETE, andREPLACEstatements (docs). This means thatif num_rows: ...in the example above would not work as expected.One possible workaround would be tofetchall()results a variable and return its__len__, however this would require us to run the SELECT query twice, and modify the behavior offetch*methods. This is a TODO. For now, try to avoid this usage pattern.# sqlite3cursor.execute("SELECT * FROM users WHERE id=1")ifrow:=cursor.fetchone():# this works fine...Example use withpytest# examples.mymoduleimportpymysqldefconnect():conn=pymysql.connect(host=...,port=...,user=...,db=...)returnconndefget_user_id(user:str)->int|None:connection=connect()return_get_user_id(connection,user)def_get_user_id(conn,user)->int|None:withconn.cursor()ascur:cur.execute("SELECT id FROM users WHERE name=%s",(user,))ifrow:=cur.fetchone():returnrow[0]returnNone# examples.test_mymoduleimportpytestimportpymysqlite# import tested module as a pytest fixture@pytest.fixturedefmymodule():from.importmymodulereturnmymodule@pytest.fixturedeffake_conn():connection=pymysqlite.connect(":memory:")returnconnectionTEST_SQL="""CREATE TABLE users (id INTEGER, name TEXT);INSERT INTO users VALUES (1, "foo"), (2, "bar");"""@pytest.fixturedefload_test_data(fake_conn):fake_conn.executescript(TEST_SQL)fake_conn.commit()returnfake_conn# test a method by passing in fake_conn (dependency injection)@pytest.mark.usefixtures("load_test_data")deftest_my_method(mymodule,fake_conn):assertmymodule._get_user_id(fake_conn,"foo")==1assertmymodule._get_user_id(fake_conn,"bar")==2assertmymodule._get_user_id(fake_conn,"nonexist")isNone# test a method by monkeypatching mymodule.connect()@pytest.fixturedefpatch_conn(monkeypatch,mymodule,fake_conn):monkeypatch.setattr(mymodule,"connect",lambda:fake_conn)@pytest.mark.usefixtures("load_test_data","patch_conn")deftest_monkeypatched_method(mymodule):assertmymodule.get_user_id("foo")==1assertmymodule.get_user_id("bar")==2assertmymodule.get_user_id("nonexist")isNone