33import psycopg2
44
55
6+ def jsonify_cursor (django_cursor ):
7+ """
8+ Adjust an already existing cursor to ensure it will return structured types (list or dict)
9+ from jsonb columns instead of strings. Django 3.1.1+ returns strings for raw queries.
10+ https://code.djangoproject.com/ticket/31956
11+ https://code.djangoproject.com/ticket/31973
12+ https://www.psycopg.org/docs/extras.html#psycopg2.extras.register_default_jsonb
13+ """
14+
15+ # The thing that is returned by connection.cursor() is (normally) a Django object
16+ # of type CursorWrapper that itself has the "real" cursor as a property called cursor.
17+ # However, it could be a CursorDebugWrapper instead, or it could be an outer wrapper
18+ # wrapping one of those. For example django-debug-toolbar wraps CursorDebugWrapper in
19+ # a NormalCursorWrapper. The django-db-readonly package wraps the Django CursorWrapper
20+ # in a ReadOnlyCursorWrapper. I'm not sure if they ever nest multiple levels. I tried
21+ # looping with `while isinstance(inner_cursor, CursorWrapper)`, but it seems that the
22+ # outer wrapper is not necessarily a subclass of the Django wrapper. My next best option
23+ # is to make the assumption that we need to get to the last property called `cursor`,
24+ # basically assuming that any wrapper is going to have a property called `cursor`
25+ # that is the real cursor or the next-level wrapper.
26+ # Another option might be to check the class of inner_cursor to see if it is the real
27+ # database cursor. That would require importing more django libraries, and probably
28+ # having to handle some changes in those libraries over different versions.
29+
30+ # We expect that there is always at least one wrapper, but we might as well handle
31+ # the possibility that we get passed the inner cursor.
32+ inner_cursor = django_cursor
33+
34+ while hasattr (inner_cursor , 'cursor' ):
35+ inner_cursor = inner_cursor .cursor
36+
37+ # Hopefully we have the right thing now, but try/catch so we can get a little better info
38+ # if it is not. Another option might be an isinstance, or another function that tests the cursor?
39+ try :
40+ psycopg2 .extras .register_default_jsonb (conn_or_curs = inner_cursor , loads = json .loads )
41+ except TypeError as e :
42+ raise Exception (f'jsonify_cursor: conn_or_curs was actually a { type (inner_cursor )} : { e } ' )
43+
44+
645@contextlib .contextmanager
746def json_cursor (django_database_connection ):
847 """
@@ -12,5 +51,5 @@ def json_cursor(django_database_connection):
1251 https://www.psycopg.org/docs/extras.html#psycopg2.extras.register_default_jsonb
1352 """
1453 with django_database_connection .cursor () as cursor :
15- psycopg2 . extras . register_default_jsonb ( conn_or_curs = cursor . cursor , loads = json . loads )
54+ jsonify_cursor ( cursor )
1655 yield cursor
0 commit comments