# Copyright (C) 2020 The Psycopg Team
+import re
+import string
import codecs
from typing import Any, Dict, Optional, TYPE_CHECKING
except KeyError:
sname = name.decode("utf8", "replace")
raise NotSupportedError(f"codec not available in Python: {sname!r}")
+
+
+def _as_python_identifier(s: str, prefix: str = "f") -> str:
+ """
+ Reduce a string to a valid Python identifier.
+
+ Replace all non-valid chars with '_' and prefix the value with *prefix* if
+ the first letter is an '_'.
+ """
+ s = _re_clean.sub("_", s)
+ # Python identifier cannot start with numbers, namedtuple fields
+ # cannot start with underscore. So...
+ if s[0] == "_" or "0" <= s[0] <= "9":
+ s = prefix + s
+ return s
+
+
+_re_clean = re.compile(
+ f"[^{string.ascii_lowercase}{string.ascii_uppercase}{string.digits}_]"
+)
# Copyright (C) 2021 The Psycopg Team
-import re
import functools
from typing import Any, Callable, Dict, NamedTuple, NoReturn, Sequence, Tuple
from typing import TYPE_CHECKING, Type, TypeVar
from . import errors as e
from ._compat import Protocol, TypeAlias
+from ._encodings import _as_python_identifier
if TYPE_CHECKING:
from .cursor import BaseCursor, Cursor
return nt._make
-# ascii except alnum and underscore
-_re_clean = re.compile("[" + re.escape(" !\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~") + "]")
-
-
@functools.lru_cache(512)
def _make_nt(*key: str) -> Type[NamedTuple]:
fields = []
for s in key:
- s = _re_clean.sub("_", s)
- # Python identifier cannot start with numbers, namedtuple fields
- # cannot start with underscore. So...
- if s[0] == "_" or "0" <= s[0] <= "9":
- s = "f" + s
- fields.append(s)
+ fields.append(_as_python_identifier(s))
return namedtuple("Row", fields) # type: ignore[return-value]