Skip to content

Commit 9c6f195

Browse files
committed
Break PostgresMessage out into its own file
1 parent 5c9a91f commit 9c6f195

File tree

5 files changed

+232
-233
lines changed

5 files changed

+232
-233
lines changed

asyncpg/exceptions/_base.py

Lines changed: 41 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77

88
import asyncpg
9-
import sys
10-
import textwrap
9+
import typing
10+
11+
from ._postgres_message import PostgresMessage
1112

1213

1314
__all__ = ['PostgresError', 'FatalPostgresError', 'UnknownPostgresError',
@@ -16,155 +17,16 @@
1617
'UnsupportedClientFeatureError']
1718

1819

19-
def _is_asyncpg_class(cls):
20-
modname = cls.__module__
21-
return modname == 'asyncpg' or modname.startswith('asyncpg.')
22-
23-
24-
class PostgresMessageMeta(type):
25-
26-
_message_map = {}
27-
_field_map = {
28-
'S': 'severity',
29-
'V': 'severity_en',
30-
'C': 'sqlstate',
31-
'M': 'message',
32-
'D': 'detail',
33-
'H': 'hint',
34-
'P': 'position',
35-
'p': 'internal_position',
36-
'q': 'internal_query',
37-
'W': 'context',
38-
's': 'schema_name',
39-
't': 'table_name',
40-
'c': 'column_name',
41-
'd': 'data_type_name',
42-
'n': 'constraint_name',
43-
'F': 'server_source_filename',
44-
'L': 'server_source_line',
45-
'R': 'server_source_function'
46-
}
47-
48-
def __new__(mcls, name, bases, dct):
49-
cls = super().__new__(mcls, name, bases, dct)
50-
if cls.__module__ == mcls.__module__ and name == 'PostgresMessage':
51-
for f in mcls._field_map.values():
52-
setattr(cls, f, None)
53-
54-
if _is_asyncpg_class(cls):
55-
mod = sys.modules[cls.__module__]
56-
if hasattr(mod, name):
57-
raise RuntimeError('exception class redefinition: {}'.format(
58-
name))
59-
60-
code = dct.get('sqlstate')
61-
if code is not None:
62-
existing = mcls._message_map.get(code)
63-
if existing is not None:
64-
raise TypeError('{} has duplicate SQLSTATE code, which is'
65-
'already defined by {}'.format(
66-
name, existing.__name__))
67-
mcls._message_map[code] = cls
68-
69-
return cls
70-
71-
@classmethod
72-
def get_message_class_for_sqlstate(mcls, code):
73-
return mcls._message_map.get(code, UnknownPostgresError)
74-
75-
76-
class PostgresMessage(metaclass=PostgresMessageMeta):
77-
78-
@classmethod
79-
def _get_error_class(cls, fields):
80-
sqlstate = fields.get('C')
81-
return type(cls).get_message_class_for_sqlstate(sqlstate)
82-
83-
@classmethod
84-
def _get_error_dict(cls, fields, query):
85-
dct = {
86-
'query': query
87-
}
88-
89-
field_map = type(cls)._field_map
90-
for k, v in fields.items():
91-
field = field_map.get(k)
92-
if field:
93-
dct[field] = v
94-
95-
return dct
96-
97-
@classmethod
98-
def _make_constructor(cls, fields, query=None):
99-
dct = cls._get_error_dict(fields, query)
100-
101-
exccls = cls._get_error_class(fields)
102-
message = dct.get('message', '')
103-
104-
# PostgreSQL will raise an exception when it detects
105-
# that the result type of the query has changed from
106-
# when the statement was prepared.
107-
#
108-
# The original error is somewhat cryptic and unspecific,
109-
# so we raise a custom subclass that is easier to handle
110-
# and identify.
111-
#
112-
# Note that we specifically do not rely on the error
113-
# message, as it is localizable.
114-
is_icse = (
115-
exccls.__name__ == 'FeatureNotSupportedError' and
116-
_is_asyncpg_class(exccls) and
117-
dct.get('server_source_function') == 'RevalidateCachedQuery'
118-
)
119-
120-
if is_icse:
121-
exceptions = sys.modules[exccls.__module__]
122-
exccls = exceptions.InvalidCachedStatementError
123-
message = ('cached statement plan is invalid due to a database '
124-
'schema or configuration change')
125-
126-
is_prepared_stmt_error = (
127-
exccls.__name__ in ('DuplicatePreparedStatementError',
128-
'InvalidSQLStatementNameError') and
129-
_is_asyncpg_class(exccls)
130-
)
131-
132-
if is_prepared_stmt_error:
133-
hint = dct.get('hint', '')
134-
hint += textwrap.dedent("""\
135-
136-
NOTE: pgbouncer with pool_mode set to "transaction" or
137-
"statement" does not support prepared statements properly.
138-
You have two options:
139-
140-
* if you are using pgbouncer for connection pooling to a
141-
single server, switch to the connection pool functionality
142-
provided by asyncpg, it is a much better option for this
143-
purpose;
144-
145-
* if you have no option of avoiding the use of pgbouncer,
146-
then you can set statement_cache_size to 0 when creating
147-
the asyncpg connection object.
148-
""")
149-
150-
dct['hint'] = hint
151-
152-
return exccls, message, dct
153-
154-
def as_dict(self):
155-
dct = {}
156-
for f in type(self)._field_map.values():
157-
val = getattr(self, f)
158-
if val is not None:
159-
dct[f] = val
160-
return dct
20+
_PE = typing.TypeVar('_PE', bound='PostgresError')
21+
_IE = typing.TypeVar('_IE', bound='InterfaceError')
22+
_PM = typing.TypeVar('_PM', bound='PostgresMessage')
16123

16224

16325
class PostgresError(PostgresMessage, Exception):
16426
"""Base class for all Postgres errors."""
16527

166-
def __str__(self):
167-
msg = self.args[0]
28+
def __str__(self) -> str:
29+
msg: str = self.args[0]
16830
if self.detail:
16931
msg += '\nDETAIL: {}'.format(self.detail)
17032
if self.hint:
@@ -173,7 +35,11 @@ def __str__(self):
17335
return msg
17436

17537
@classmethod
176-
def new(cls, fields, query=None):
38+
def new(
39+
cls: typing.Type[_PE],
40+
fields: typing.Dict[str, str],
41+
query: typing.Optional[str] = None
42+
) -> _PE:
17743
exccls, message, dct = cls._make_constructor(fields, query)
17844
ex = exccls(message)
17945
ex.__dict__.update(dct)
@@ -189,12 +55,15 @@ class UnknownPostgresError(FatalPostgresError):
18955

19056

19157
class InterfaceMessage:
192-
def __init__(self, *, detail=None, hint=None):
193-
self.detail = detail
194-
self.hint = hint
58+
args: typing.Tuple[typing.Any, ...]
59+
60+
def __init__(self, *, detail: typing.Optional[str] = None,
61+
hint: typing.Optional[str] = None) -> None:
62+
self.detail: typing.Optional[str] = detail
63+
self.hint: typing.Optional[str] = hint
19564

196-
def __str__(self):
197-
msg = self.args[0]
65+
def __str__(self) -> str:
66+
msg: str = self.args[0]
19867
if self.detail:
19968
msg += '\nDETAIL: {}'.format(self.detail)
20069
if self.hint:
@@ -206,11 +75,12 @@ def __str__(self):
20675
class InterfaceError(InterfaceMessage, Exception):
20776
"""An error caused by improper use of asyncpg API."""
20877

209-
def __init__(self, msg, *, detail=None, hint=None):
78+
def __init__(self, msg: str, *, detail: typing.Optional[str] = None,
79+
hint: typing.Optional[str] = None) -> None:
21080
InterfaceMessage.__init__(self, detail=detail, hint=hint)
21181
Exception.__init__(self, msg)
21282

213-
def with_msg(self, msg):
83+
def with_msg(self: _IE, msg: str) -> _IE:
21484
return type(self)(
21585
msg,
21686
detail=self.detail,
@@ -231,7 +101,8 @@ class UnsupportedClientFeatureError(InterfaceError):
231101
class InterfaceWarning(InterfaceMessage, UserWarning):
232102
"""A warning caused by an improper use of asyncpg API."""
233103

234-
def __init__(self, msg, *, detail=None, hint=None):
104+
def __init__(self, msg: str, *, detail: typing.Optional[str] = None,
105+
hint: typing.Optional[str] = None) -> None:
235106
InterfaceMessage.__init__(self, detail=detail, hint=hint)
236107
UserWarning.__init__(self, msg)
237108

@@ -247,25 +118,32 @@ class ProtocolError(InternalClientError):
247118
class OutdatedSchemaCacheError(InternalClientError):
248119
"""A value decoding error caused by a schema change before row fetching."""
249120

250-
def __init__(self, msg, *, schema=None, data_type=None, position=None):
121+
def __init__(self, msg: str, *, schema: typing.Optional[str] = None,
122+
data_type: typing.Optional[str] = None,
123+
position: typing.Optional[str] = None) -> None:
251124
super().__init__(msg)
252-
self.schema_name = schema
253-
self.data_type_name = data_type
254-
self.position = position
125+
self.schema_name: typing.Optional[str] = schema
126+
self.data_type_name: typing.Optional[str] = data_type
127+
self.position: typing.Optional[str] = position
255128

256129

257130
class PostgresLogMessage(PostgresMessage):
258131
"""A base class for non-error server messages."""
259132

260-
def __str__(self):
133+
def __str__(self) -> str:
261134
return '{}: {}'.format(type(self).__name__, self.message)
262135

263-
def __setattr__(self, name, val):
136+
def __setattr__(self, name: str, val: typing.Any) -> None:
264137
raise TypeError('instances of {} are immutable'.format(
265138
type(self).__name__))
266139

267140
@classmethod
268-
def new(cls, fields, query=None):
141+
def new(
142+
cls: typing.Type[_PM],
143+
fields: typing.Dict[str, str],
144+
query: typing.Optional[str] = None
145+
) -> PostgresMessage:
146+
exccls: typing.Type[PostgresMessage]
269147
exccls, message_text, dct = cls._make_constructor(fields, query)
270148

271149
if exccls is UnknownPostgresError:
@@ -277,7 +155,7 @@ def new(cls, fields, query=None):
277155
exccls = asyncpg.PostgresWarning
278156

279157
if issubclass(exccls, (BaseException, Warning)):
280-
msg = exccls(message_text)
158+
msg: PostgresMessage = exccls(message_text)
281159
else:
282160
msg = exccls()
283161

asyncpg/exceptions/_base.pyi

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)