1# util/compat.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: allow-untyped-defs, allow-untyped-calls
8
9"""Handle Python version/platform incompatibilities."""
10
11from __future__ import annotations
12
13import base64
14import dataclasses
15import hashlib
16from importlib import metadata as importlib_metadata
17import inspect
18import operator
19import platform
20import sys
21import typing
22from typing import Any
23from typing import Callable
24from typing import Dict
25from typing import Iterable
26from typing import List
27from typing import Mapping
28from typing import Optional
29from typing import Sequence
30from typing import Set
31from typing import Tuple
32from typing import Type
33
34py314b1 = sys.version_info >= (3, 14, 0, "beta", 1)
35py314 = sys.version_info >= (3, 14)
36py313 = sys.version_info >= (3, 13)
37py312 = sys.version_info >= (3, 12)
38py311 = sys.version_info >= (3, 11)
39pypy = platform.python_implementation() == "PyPy"
40cpython = platform.python_implementation() == "CPython"
41
42win32 = sys.platform.startswith("win")
43osx = sys.platform.startswith("darwin")
44arm = "aarch" in platform.machine().lower()
45is64bit = sys.maxsize > 2**32
46
47has_refcount_gc = bool(cpython)
48
49dottedgetter = operator.attrgetter
50
51
52class FullArgSpec(typing.NamedTuple):
53 args: List[str]
54 varargs: Optional[str]
55 varkw: Optional[str]
56 defaults: Optional[Tuple[Any, ...]]
57 kwonlyargs: List[str]
58 kwonlydefaults: Optional[Dict[str, Any]]
59 annotations: Dict[str, Any]
60
61
62def inspect_getfullargspec(func: Callable[..., Any]) -> FullArgSpec:
63 """Fully vendored version of getfullargspec from Python 3.3."""
64
65 if inspect.ismethod(func):
66 func = func.__func__
67 if not inspect.isfunction(func):
68 raise TypeError(f"{func!r} is not a Python function")
69
70 co = func.__code__
71 if not inspect.iscode(co):
72 raise TypeError(f"{co!r} is not a code object")
73
74 nargs = co.co_argcount
75 names = co.co_varnames
76 nkwargs = co.co_kwonlyargcount
77 args = list(names[:nargs])
78 kwonlyargs = list(names[nargs : nargs + nkwargs])
79
80 nargs += nkwargs
81 varargs = None
82 if co.co_flags & inspect.CO_VARARGS:
83 varargs = co.co_varnames[nargs]
84 nargs = nargs + 1
85 varkw = None
86 if co.co_flags & inspect.CO_VARKEYWORDS:
87 varkw = co.co_varnames[nargs]
88
89 return FullArgSpec(
90 args,
91 varargs,
92 varkw,
93 func.__defaults__,
94 kwonlyargs,
95 func.__kwdefaults__,
96 func.__annotations__,
97 )
98
99
100# python stubs don't have a public type for this. not worth
101# making a protocol
102def md5_not_for_security() -> Any:
103 return hashlib.md5(usedforsecurity=False)
104
105
106def importlib_metadata_get(group):
107 ep = importlib_metadata.entry_points()
108 if typing.TYPE_CHECKING or hasattr(ep, "select"):
109 return ep.select(group=group)
110 else:
111 return ep.get(group, ())
112
113
114def b(s):
115 return s.encode("latin-1")
116
117
118def b64decode(x: str) -> bytes:
119 return base64.b64decode(x.encode("ascii"))
120
121
122def b64encode(x: bytes) -> str:
123 return base64.b64encode(x).decode("ascii")
124
125
126def decode_backslashreplace(text: bytes, encoding: str) -> str:
127 return text.decode(encoding, errors="backslashreplace")
128
129
130def cmp(a, b):
131 return (a > b) - (a < b)
132
133
134def _formatannotation(annotation, base_module=None):
135 """vendored from python 3.7"""
136
137 if isinstance(annotation, str):
138 return annotation
139
140 if getattr(annotation, "__module__", None) == "typing":
141 return repr(annotation).replace("typing.", "").replace("~", "")
142 if isinstance(annotation, type):
143 if annotation.__module__ in ("builtins", base_module):
144 return repr(annotation.__qualname__)
145 return annotation.__module__ + "." + annotation.__qualname__
146 elif isinstance(annotation, typing.TypeVar):
147 return repr(annotation).replace("~", "")
148 return repr(annotation).replace("~", "")
149
150
151def inspect_formatargspec(
152 args: List[str],
153 varargs: Optional[str] = None,
154 varkw: Optional[str] = None,
155 defaults: Optional[Sequence[Any]] = None,
156 kwonlyargs: Optional[Sequence[str]] = (),
157 kwonlydefaults: Optional[Mapping[str, Any]] = {},
158 annotations: Mapping[str, Any] = {},
159 formatarg: Callable[[str], str] = str,
160 formatvarargs: Callable[[str], str] = lambda name: "*" + name,
161 formatvarkw: Callable[[str], str] = lambda name: "**" + name,
162 formatvalue: Callable[[Any], str] = lambda value: "=" + repr(value),
163 formatreturns: Callable[[Any], str] = lambda text: " -> " + str(text),
164 formatannotation: Callable[[Any], str] = _formatannotation,
165) -> str:
166 """Copy formatargspec from python 3.7 standard library.
167
168 Python 3 has deprecated formatargspec and requested that Signature
169 be used instead, however this requires a full reimplementation
170 of formatargspec() in terms of creating Parameter objects and such.
171 Instead of introducing all the object-creation overhead and having
172 to reinvent from scratch, just copy their compatibility routine.
173
174 Ultimately we would need to rewrite our "decorator" routine completely
175 which is not really worth it right now, until all Python 2.x support
176 is dropped.
177
178 """
179
180 kwonlydefaults = kwonlydefaults or {}
181 annotations = annotations or {}
182
183 def formatargandannotation(arg):
184 result = formatarg(arg)
185 if arg in annotations:
186 result += ": " + formatannotation(annotations[arg])
187 return result
188
189 specs = []
190 if defaults:
191 firstdefault = len(args) - len(defaults)
192 else:
193 firstdefault = -1
194
195 for i, arg in enumerate(args):
196 spec = formatargandannotation(arg)
197 if defaults and i >= firstdefault:
198 spec = spec + formatvalue(defaults[i - firstdefault])
199 specs.append(spec)
200
201 if varargs is not None:
202 specs.append(formatvarargs(formatargandannotation(varargs)))
203 else:
204 if kwonlyargs:
205 specs.append("*")
206
207 if kwonlyargs:
208 for kwonlyarg in kwonlyargs:
209 spec = formatargandannotation(kwonlyarg)
210 if kwonlydefaults and kwonlyarg in kwonlydefaults:
211 spec += formatvalue(kwonlydefaults[kwonlyarg])
212 specs.append(spec)
213
214 if varkw is not None:
215 specs.append(formatvarkw(formatargandannotation(varkw)))
216
217 result = "(" + ", ".join(specs) + ")"
218 if "return" in annotations:
219 result += formatreturns(formatannotation(annotations["return"]))
220 return result
221
222
223def dataclass_fields(cls: Type[Any]) -> Iterable[dataclasses.Field[Any]]:
224 """Return a sequence of all dataclasses.Field objects associated
225 with a class as an already processed dataclass.
226
227 The class must **already be a dataclass** for Field objects to be returned.
228
229 """
230
231 if dataclasses.is_dataclass(cls):
232 return dataclasses.fields(cls)
233 else:
234 return []
235
236
237def local_dataclass_fields(cls: Type[Any]) -> Iterable[dataclasses.Field[Any]]:
238 """Return a sequence of all dataclasses.Field objects associated with
239 an already processed dataclass, excluding those that originate from a
240 superclass.
241
242 The class must **already be a dataclass** for Field objects to be returned.
243
244 """
245
246 if dataclasses.is_dataclass(cls):
247 super_fields: Set[dataclasses.Field[Any]] = set()
248 for sup in cls.__bases__:
249 super_fields.update(dataclass_fields(sup))
250 return [f for f in dataclasses.fields(cls) if f not in super_fields]
251 else:
252 return []