Skip to content

added support and test cases for psycopg3 #1737

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

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions debug_toolbar/panels/sql/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,59 @@

def get_isolation_level_display(vendor, level):
if vendor == "postgresql":
import psycopg2.extensions

choices = {
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: _("Autocommit"),
psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: _("Read uncommitted"),
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: _("Read committed"),
psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: _("Repeatable read"),
psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: _("Serializable"),
}
try:
import psycopg

choices = {
# AUTOCOMMIT level does not exists in psycopg3
psycopg.IsolationLevel.READ_UNCOMMITTED: _("Read uncommitted"),
psycopg.IsolationLevel.READ_COMMITTED: _("Read committed"),
psycopg.IsolationLevel.REPEATABLE_READ: _("Repeatable read"),
psycopg.IsolationLevel.SERIALIZABLE: _("Serializable"),
}
except ImportError:
import psycopg2.extensions

choices = {
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: _("Autocommit"),
psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: _(
"Read uncommitted"
),
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: _("Read committed"),
psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: _(
"Repeatable read"
),
psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: _("Serializable"),
}

else:
raise ValueError(vendor)
return choices.get(level)


def get_transaction_status_display(vendor, level):
if vendor == "postgresql":
import psycopg2.extensions

choices = {
psycopg2.extensions.TRANSACTION_STATUS_IDLE: _("Idle"),
psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: _("Active"),
psycopg2.extensions.TRANSACTION_STATUS_INTRANS: _("In transaction"),
psycopg2.extensions.TRANSACTION_STATUS_INERROR: _("In error"),
psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: _("Unknown"),
}
try:
import psycopg

choices = {
psycopg.pq.TransactionStatus.IDLE: _("Idle"),
psycopg.pq.TransactionStatus.ACTIVE: _("Active"),
psycopg.pq.TransactionStatus.INTRANS: _("In transaction"),
psycopg.pq.TransactionStatus.INERROR: _("In error"),
psycopg.pq.TransactionStatus.UNKNOWN: _("Unknown"),
}
except ImportError:
import psycopg2.extensions

choices = {
psycopg2.extensions.TRANSACTION_STATUS_IDLE: _("Idle"),
psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: _("Active"),
psycopg2.extensions.TRANSACTION_STATUS_INTRANS: _("In transaction"),
psycopg2.extensions.TRANSACTION_STATUS_INERROR: _("In error"),
psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: _("Unknown"),
}

else:
raise ValueError(vendor)
return choices.get(level)
Expand Down
33 changes: 24 additions & 9 deletions debug_toolbar/panels/sql/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
from debug_toolbar.utils import get_stack_trace, get_template_info

try:
from psycopg2._json import Json as PostgresJson
from psycopg2.extensions import STATUS_IN_TRANSACTION
import psycopg

PostgresJson = psycopg.types.json.Jsonb
STATUS_IN_TRANSACTION = psycopg.pq.TransactionStatus.INTRANS
except ImportError:
PostgresJson = None
STATUS_IN_TRANSACTION = None
try:
from psycopg2._json import Json as PostgresJson
from psycopg2.extensions import STATUS_IN_TRANSACTION
except ImportError:
PostgresJson = None
STATUS_IN_TRANSACTION = None

# Prevents SQL queries from being sent to the DB. It's used
# by the TemplatePanel to prevent the toolbar from issuing
Expand Down Expand Up @@ -126,7 +132,13 @@ def _quote_params(self, params):

def _decode(self, param):
if PostgresJson and isinstance(param, PostgresJson):
return param.dumps(param.adapted)
# psycopg3
if hasattr(param, "obj"):
return param.dumps(param.obj)
# psycopg2
if hasattr(param, "adapted"):
return param.dumps(param.adapted)

# If a sequence type, decode each element separately
if isinstance(param, (tuple, list)):
return [self._decode(element) for element in param]
Expand All @@ -149,7 +161,7 @@ def _record(self, method, sql, params):
if vendor == "postgresql":
# The underlying DB connection (as opposed to Django's wrapper)
conn = self.db.connection
initial_conn_status = conn.status
initial_conn_status = conn.info.transaction_status

start_time = time()
try:
Expand All @@ -166,7 +178,10 @@ def _record(self, method, sql, params):

# Sql might be an object (such as psycopg Composed).
# For logging purposes, make sure it's str.
sql = str(sql)
if vendor == "postgresql" and not isinstance(sql, str):
sql = sql.as_string(conn)
else:
sql = str(sql)

params = {
"vendor": vendor,
Expand Down Expand Up @@ -205,7 +220,7 @@ def _record(self, method, sql, params):
# case where Django can start a transaction before the first query
# executes, so in that case logger.current_transaction_id() will
# generate a new transaction ID since one does not already exist.
final_conn_status = conn.status
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like the old logic was based on connection status,
https://www.psycopg.org/docs/extensions.html#connection-status-constants

however according to the new api in psycopg3 connection.info.status we only have 2 states
https://www.psycopg.org/psycopg3/docs/api/pq.html#psycopg.pq.ConnStatus

Since STATUS_IN_TRANSACTION is never in any of the options of connection.info.status the test tests.panels.test_sql.SQLPanelMultiDBTestCase.test_transaction_status will always fail. How should we based the logic for the new states of psycopg3? should we rely on conn.info.transaction_status instead of connection.info.status

final_conn_status = conn.info.transaction_status
if final_conn_status == STATUS_IN_TRANSACTION:
if initial_conn_status == STATUS_IN_TRANSACTION:
trans_id = self.logger.current_transaction_id(alias)
Expand All @@ -217,7 +232,7 @@ def _record(self, method, sql, params):
params.update(
{
"trans_id": trans_id,
"trans_status": conn.get_transaction_status(),
"trans_status": conn.info.transaction_status,
"iso_level": iso_level,
}
)
Expand Down
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Pending
memory leaks and sometimes very verbose and hard to silence output in some
environments (but not others). The maintainers judged that time and effort is
better invested elsewhere.
* Added support for pyscorg3.
* Added pyscorg3 to the CI.

3.8.1 (2022-12-03)
------------------
Expand Down
22 changes: 18 additions & 4 deletions tests/panels/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,17 @@ def test_json_param_conversion(self):
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
)
def test_tuple_param_conversion(self):
"""
For psycopg3 You cannot use IN %s with a tuple
https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#you-cannot-use-in-s-with-a-tuple
"""
try:
import psycopg # noqa: F401

self.skipTest("test not supported for psycopg3")
except ImportError:
pass

self.assertEqual(len(self.panel._queries), 0)

list(
Expand Down Expand Up @@ -377,12 +388,15 @@ def test_erroneous_query(self):
@unittest.skipUnless(
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
)
def test_execute_with_psycopg2_composed_sql(self):
def test_execute_with_psycopg_composed_sql(self):
"""
Test command executed using a Composed psycopg2 object is logged.
Ref: http://initd.org/psycopg/docs/sql.html
Test command executed using a Composed psycopg object is logged.
Ref: https://www.psycopg.org/psycopg3/docs/api/sql.html
"""
from psycopg2 import sql
try:
from psycopg import sql
except ImportError:
from psycopg2 import sql

self.assertEqual(len(self.panel._queries), 0)

Expand Down
17 changes: 13 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ isolated_build = true
envlist =
docs
packaging
py{38,39,310}-dj{32,41,42}-{sqlite,postgresql,postgis,mysql}
py{38,39,310}-dj{32}-{sqlite,legacy_postgresql,postgis,mysql}
py{310}-dj{40}-{sqlite}
py{310,311}-dj{41,42,main}-{sqlite,postgresql,postgis,mysql}
py{310,311}-dj{41}-{sqlite,legacy_postgresql,postgis,mysql}
py{310,311}-dj{42,main}-{sqlite,legacy_postgresql,postgresql,postgis,mysql}

[testenv]
deps =
dj32: django~=3.2.9
dj40: django~=4.0.0
dj41: django~=4.1.3
dj42: django>=4.2a1,<5
postgresql: psycopg2-binary
legacy_postgresql: psycopg2-binary
postgresql: psycopg[binary]
postgis: psycopg2-binary
mysql: mysqlclient
djmain: https://github.com/django/django/archive/main.tar.gz
Expand Down Expand Up @@ -47,7 +49,13 @@ allowlist_externals = make
pip_pre = True
commands = python -b -W always -m coverage run -m django test -v2 {posargs:tests}

[testenv:py{38,39,310,311}-dj{32,40,41,42,main}-postgresql]
[testenv:py{38,39,310,311}-dj{32,40,41}-legacy_postgresql]
setenv =
{[testenv]setenv}
DB_BACKEND = postgresql
DB_PORT = {env:DB_PORT:5432}

[testenv:py{310,311}-dj{42,main}-postgresql]
setenv =
{[testenv]setenv}
DB_BACKEND = postgresql
Expand Down Expand Up @@ -97,5 +105,6 @@ python =
DB_BACKEND =
mysql: mysql
postgresql: postgresql
legacy_postgresql: postgresql
postgis: postgresql
sqlite3: sqlite