diff --git a/venv/Lib/site-packages/sqlalchemy/util/queue.py b/venv/Lib/site-packages/sqlalchemy/util/queue.py new file mode 100644 index 0000000..bd55357 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/util/queue.py @@ -0,0 +1,322 @@ +# util/queue.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: allow-untyped-defs, allow-untyped-calls + +"""An adaptation of Py2.3/2.4's Queue module which supports reentrant +behavior, using RLock instead of Lock for its mutex object. The +Queue object is used exclusively by the sqlalchemy.pool.QueuePool +class. + +This is to support the connection pool's usage of weakref callbacks to return +connections to the underlying Queue, which can in extremely +rare cases be invoked within the ``get()`` method of the Queue itself, +producing a ``put()`` inside the ``get()`` and therefore a reentrant +condition. + +""" + +from __future__ import annotations + +import asyncio +from collections import deque +import threading +from time import time as _time +import typing +from typing import Any +from typing import Awaitable +from typing import Deque +from typing import Generic +from typing import Optional +from typing import TypeVar + +from .concurrency import await_fallback +from .concurrency import await_only +from .langhelpers import memoized_property + +_T = TypeVar("_T", bound=Any) +__all__ = ["Empty", "Full", "Queue"] + + +class Empty(Exception): + "Exception raised by Queue.get(block=0)/get_nowait()." + + pass + + +class Full(Exception): + "Exception raised by Queue.put(block=0)/put_nowait()." + + pass + + +class QueueCommon(Generic[_T]): + maxsize: int + use_lifo: bool + + def __init__(self, maxsize: int = 0, use_lifo: bool = False): ... + + def empty(self) -> bool: + raise NotImplementedError() + + def full(self) -> bool: + raise NotImplementedError() + + def qsize(self) -> int: + raise NotImplementedError() + + def put_nowait(self, item: _T) -> None: + raise NotImplementedError() + + def put( + self, item: _T, block: bool = True, timeout: Optional[float] = None + ) -> None: + raise NotImplementedError() + + def get_nowait(self) -> _T: + raise NotImplementedError() + + def get(self, block: bool = True, timeout: Optional[float] = None) -> _T: + raise NotImplementedError() + + +class Queue(QueueCommon[_T]): + queue: Deque[_T] + + def __init__(self, maxsize: int = 0, use_lifo: bool = False): + """Initialize a queue object with a given maximum size. + + If `maxsize` is <= 0, the queue size is infinite. + + If `use_lifo` is True, this Queue acts like a Stack (LIFO). + """ + + self._init(maxsize) + # mutex must be held whenever the queue is mutating. All methods + # that acquire mutex must release it before returning. mutex + # is shared between the two conditions, so acquiring and + # releasing the conditions also acquires and releases mutex. + self.mutex = threading.RLock() + # Notify not_empty whenever an item is added to the queue; a + # thread waiting to get is notified then. + self.not_empty = threading.Condition(self.mutex) + # Notify not_full whenever an item is removed from the queue; + # a thread waiting to put is notified then. + self.not_full = threading.Condition(self.mutex) + # If this queue uses LIFO or FIFO + self.use_lifo = use_lifo + + def qsize(self) -> int: + """Return the approximate size of the queue (not reliable!).""" + + with self.mutex: + return self._qsize() + + def empty(self) -> bool: + """Return True if the queue is empty, False otherwise (not + reliable!).""" + + with self.mutex: + return self._empty() + + def full(self) -> bool: + """Return True if the queue is full, False otherwise (not + reliable!).""" + + with self.mutex: + return self._full() + + def put( + self, item: _T, block: bool = True, timeout: Optional[float] = None + ) -> None: + """Put an item into the queue. + + If optional args `block` is True and `timeout` is None (the + default), block if necessary until a free slot is + available. If `timeout` is a positive number, it blocks at + most `timeout` seconds and raises the ``Full`` exception if no + free slot was available within that time. Otherwise (`block` + is false), put an item on the queue if a free slot is + immediately available, else raise the ``Full`` exception + (`timeout` is ignored in that case). + """ + + with self.not_full: + if not block: + if self._full(): + raise Full + elif timeout is None: + while self._full(): + self.not_full.wait() + else: + if timeout < 0: + raise ValueError("'timeout' must be a positive number") + endtime = _time() + timeout + while self._full(): + remaining = endtime - _time() + if remaining <= 0.0: + raise Full + self.not_full.wait(remaining) + self._put(item) + self.not_empty.notify() + + def put_nowait(self, item: _T) -> None: + """Put an item into the queue without blocking. + + Only enqueue the item if a free slot is immediately available. + Otherwise raise the ``Full`` exception. + """ + return self.put(item, False) + + def get(self, block: bool = True, timeout: Optional[float] = None) -> _T: + """Remove and return an item from the queue. + + If optional args `block` is True and `timeout` is None (the + default), block if necessary until an item is available. If + `timeout` is a positive number, it blocks at most `timeout` + seconds and raises the ``Empty`` exception if no item was + available within that time. Otherwise (`block` is false), + return an item if one is immediately available, else raise the + ``Empty`` exception (`timeout` is ignored in that case). + + """ + with self.not_empty: + if not block: + if self._empty(): + raise Empty + elif timeout is None: + while self._empty(): + self.not_empty.wait() + else: + if timeout < 0: + raise ValueError("'timeout' must be a positive number") + endtime = _time() + timeout + while self._empty(): + remaining = endtime - _time() + if remaining <= 0.0: + raise Empty + self.not_empty.wait(remaining) + item = self._get() + self.not_full.notify() + return item + + def get_nowait(self) -> _T: + """Remove and return an item from the queue without blocking. + + Only get an item if one is immediately available. Otherwise + raise the ``Empty`` exception. + """ + + return self.get(False) + + def _init(self, maxsize: int) -> None: + self.maxsize = maxsize + self.queue = deque() + + def _qsize(self) -> int: + return len(self.queue) + + def _empty(self) -> bool: + return not self.queue + + def _full(self) -> bool: + return self.maxsize > 0 and len(self.queue) == self.maxsize + + def _put(self, item: _T) -> None: + self.queue.append(item) + + def _get(self) -> _T: + if self.use_lifo: + # LIFO + return self.queue.pop() + else: + # FIFO + return self.queue.popleft() + + +class AsyncAdaptedQueue(QueueCommon[_T]): + if typing.TYPE_CHECKING: + + @staticmethod + def await_(coroutine: Awaitable[Any]) -> _T: ... + + else: + await_ = staticmethod(await_only) + + def __init__(self, maxsize: int = 0, use_lifo: bool = False): + self.use_lifo = use_lifo + self.maxsize = maxsize + + def empty(self) -> bool: + return self._queue.empty() + + def full(self): + return self._queue.full() + + def qsize(self): + return self._queue.qsize() + + @memoized_property + def _queue(self) -> asyncio.Queue[_T]: + # Delay creation of the queue until it is first used, to avoid + # binding it to a possibly wrong event loop. + # By delaying the creation of the pool we accommodate the common + # usage pattern of instantiating the engine at module level, where a + # different event loop is in present compared to when the application + # is actually run. + + queue: asyncio.Queue[_T] + + if self.use_lifo: + queue = asyncio.LifoQueue(maxsize=self.maxsize) + else: + queue = asyncio.Queue(maxsize=self.maxsize) + return queue + + def put_nowait(self, item: _T) -> None: + try: + self._queue.put_nowait(item) + except asyncio.QueueFull as err: + raise Full() from err + + def put( + self, item: _T, block: bool = True, timeout: Optional[float] = None + ) -> None: + if not block: + return self.put_nowait(item) + + try: + if timeout is not None: + self.await_(asyncio.wait_for(self._queue.put(item), timeout)) + else: + self.await_(self._queue.put(item)) + except (asyncio.QueueFull, asyncio.TimeoutError) as err: + raise Full() from err + + def get_nowait(self) -> _T: + try: + return self._queue.get_nowait() + except asyncio.QueueEmpty as err: + raise Empty() from err + + def get(self, block: bool = True, timeout: Optional[float] = None) -> _T: + if not block: + return self.get_nowait() + + try: + if timeout is not None: + return self.await_( + asyncio.wait_for(self._queue.get(), timeout) + ) + else: + return self.await_(self._queue.get()) + except (asyncio.QueueEmpty, asyncio.TimeoutError) as err: + raise Empty() from err + + +class FallbackAsyncAdaptedQueue(AsyncAdaptedQueue[_T]): + if not typing.TYPE_CHECKING: + await_ = staticmethod(await_fallback) diff --git a/venv/Lib/site-packages/sqlalchemy/util/tool_support.py b/venv/Lib/site-packages/sqlalchemy/util/tool_support.py new file mode 100644 index 0000000..7f02f20 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/util/tool_support.py @@ -0,0 +1,202 @@ +# util/tool_support.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: allow-untyped-defs, allow-untyped-calls +"""support routines for the helpers in tools/. + +These aren't imported by the enclosing util package as the are not +needed for normal library use. + +""" + +from __future__ import annotations + +from argparse import ArgumentParser +from argparse import Namespace +import contextlib +import difflib +import os +from pathlib import Path +import shlex +import shutil +import subprocess +import sys +from typing import Any +from typing import Dict +from typing import Iterator +from typing import Optional +from typing import Union + +from . import compat + + +class code_writer_cmd: + parser: ArgumentParser + args: Namespace + suppress_output: bool + diffs_detected: bool + source_root: Path + pyproject_toml_path: Path + + def __init__(self, tool_script: str): + self.source_root = Path(tool_script).parent.parent + self.pyproject_toml_path = self.source_root / Path("pyproject.toml") + assert self.pyproject_toml_path.exists() + + self.parser = ArgumentParser() + self.parser.add_argument( + "--stdout", + action="store_true", + help="Write to stdout instead of saving to file", + ) + self.parser.add_argument( + "-c", + "--check", + help="Don't write the files back, just return the " + "status. Return code 0 means nothing would change. " + "Return code 1 means some files would be reformatted", + action="store_true", + ) + + def run_zimports(self, tempfile: str) -> None: + self._run_console_script( + str(tempfile), + { + "entrypoint": "zimports", + "options": f"--toml-config {self.pyproject_toml_path}", + }, + ) + + def run_black(self, tempfile: str) -> None: + self._run_console_script( + str(tempfile), + { + "entrypoint": "black", + "options": f"--config {self.pyproject_toml_path}", + }, + ) + + def _run_console_script(self, path: str, options: Dict[str, Any]) -> None: + """Run a Python console application from within the process. + + Used for black, zimports + + """ + + is_posix = os.name == "posix" + + entrypoint_name = options["entrypoint"] + + for entry in compat.importlib_metadata_get("console_scripts"): + if entry.name == entrypoint_name: + impl = entry + break + else: + raise Exception( + f"Could not find entrypoint console_scripts.{entrypoint_name}" + ) + cmdline_options_str = options.get("options", "") + cmdline_options_list = shlex.split( + cmdline_options_str, posix=is_posix + ) + [path] + + kw: Dict[str, Any] = {} + if self.suppress_output: + kw["stdout"] = kw["stderr"] = subprocess.DEVNULL + + subprocess.run( + [ + sys.executable, + "-c", + "import %s; %s.%s()" % (impl.module, impl.module, impl.attr), + ] + + cmdline_options_list, + cwd=str(self.source_root), + **kw, + ) + + def write_status(self, *text: str) -> None: + if not self.suppress_output: + sys.stderr.write(" ".join(text)) + + def write_output_file_from_text( + self, text: str, destination_path: Union[str, Path] + ) -> None: + if self.args.check: + self._run_diff(destination_path, source=text) + elif self.args.stdout: + print(text) + else: + self.write_status(f"Writing {destination_path}...") + Path(destination_path).write_text( + text, encoding="utf-8", newline="\n" + ) + self.write_status("done\n") + + def write_output_file_from_tempfile( + self, tempfile: str, destination_path: str + ) -> None: + if self.args.check: + self._run_diff(destination_path, source_file=tempfile) + os.unlink(tempfile) + elif self.args.stdout: + with open(tempfile) as tf: + print(tf.read()) + os.unlink(tempfile) + else: + self.write_status(f"Writing {destination_path}...") + shutil.move(tempfile, destination_path) + self.write_status("done\n") + + def _run_diff( + self, + destination_path: Union[str, Path], + *, + source: Optional[str] = None, + source_file: Optional[str] = None, + ) -> None: + if source_file: + with open(source_file, encoding="utf-8") as tf: + source_lines = list(tf) + elif source is not None: + source_lines = source.splitlines(keepends=True) + else: + assert False, "source or source_file is required" + + with open(destination_path, encoding="utf-8") as dp: + d = difflib.unified_diff( + list(dp), + source_lines, + fromfile=Path(destination_path).as_posix(), + tofile="", + n=3, + lineterm="\n", + ) + d_as_list = list(d) + if d_as_list: + self.diffs_detected = True + print("".join(d_as_list)) + + @contextlib.contextmanager + def add_arguments(self) -> Iterator[ArgumentParser]: + yield self.parser + + @contextlib.contextmanager + def run_program(self) -> Iterator[None]: + self.args = self.parser.parse_args() + if self.args.check: + self.diffs_detected = False + self.suppress_output = True + elif self.args.stdout: + self.suppress_output = True + else: + self.suppress_output = False + yield + + if self.args.check and self.diffs_detected: + sys.exit(1) + else: + sys.exit(0) diff --git a/venv/Lib/site-packages/sqlalchemy/util/topological.py b/venv/Lib/site-packages/sqlalchemy/util/topological.py new file mode 100644 index 0000000..b051ca7 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/util/topological.py @@ -0,0 +1,120 @@ +# util/topological.py +# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php + +"""Topological sorting algorithms.""" + +from __future__ import annotations + +from typing import Any +from typing import Collection +from typing import DefaultDict +from typing import Iterable +from typing import Iterator +from typing import Sequence +from typing import Set +from typing import Tuple +from typing import TypeVar + +from .. import util +from ..exc import CircularDependencyError + +_T = TypeVar("_T", bound=Any) + +__all__ = ["sort", "sort_as_subsets", "find_cycles"] + + +def sort_as_subsets( + tuples: Collection[Tuple[_T, _T]], allitems: Collection[_T] +) -> Iterator[Sequence[_T]]: + edges: DefaultDict[_T, Set[_T]] = util.defaultdict(set) + for parent, child in tuples: + edges[child].add(parent) + + todo = list(allitems) + todo_set = set(allitems) + + while todo_set: + output = [] + for node in todo: + if todo_set.isdisjoint(edges[node]): + output.append(node) + + if not output: + raise CircularDependencyError( + "Circular dependency detected.", + find_cycles(tuples, allitems), + _gen_edges(edges), + ) + + todo_set.difference_update(output) + todo = [t for t in todo if t in todo_set] + yield output + + +def sort( + tuples: Collection[Tuple[_T, _T]], + allitems: Collection[_T], + deterministic_order: bool = True, +) -> Iterator[_T]: + """sort the given list of items by dependency. + + 'tuples' is a list of tuples representing a partial ordering. + + deterministic_order is no longer used, the order is now always + deterministic given the order of "allitems". the flag is there + for backwards compatibility with Alembic. + + """ + + for set_ in sort_as_subsets(tuples, allitems): + yield from set_ + + +def find_cycles( + tuples: Iterable[Tuple[_T, _T]], allitems: Iterable[_T] +) -> Set[_T]: + # adapted from: + # https://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html + + edges: DefaultDict[_T, Set[_T]] = util.defaultdict(set) + for parent, child in tuples: + edges[parent].add(child) + nodes_to_test = set(edges) + + output = set() + + # we'd like to find all nodes that are + # involved in cycles, so we do the full + # pass through the whole thing for each + # node in the original list. + + # we can go just through parent edge nodes. + # if a node is only a child and never a parent, + # by definition it can't be part of a cycle. same + # if it's not in the edges at all. + for node in nodes_to_test: + stack = [node] + todo = nodes_to_test.difference(stack) + while stack: + top = stack[-1] + for node in edges[top]: + if node in stack: + cyc = stack[stack.index(node) :] + todo.difference_update(cyc) + output.update(cyc) + + if node in todo: + stack.append(node) + todo.remove(node) + break + else: + stack.pop() + return output + + +def _gen_edges(edges: DefaultDict[_T, Set[_T]]) -> Set[Tuple[_T, _T]]: + return {(right, left) for left in edges for right in edges[left]} diff --git a/venv/Lib/site-packages/sqlalchemy/util/typing.py b/venv/Lib/site-packages/sqlalchemy/util/typing.py new file mode 100644 index 0000000..2ee78d6 --- /dev/null +++ b/venv/Lib/site-packages/sqlalchemy/util/typing.py @@ -0,0 +1,734 @@ +# util/typing.py +# Copyright (C) 2022-2026 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: allow-untyped-defs, allow-untyped-calls + +from __future__ import annotations + +import builtins +from collections import deque +import collections.abc as collections_abc +import re +import sys +import typing +from typing import Any +from typing import Callable +from typing import Dict +from typing import ForwardRef +from typing import Generic +from typing import Iterable +from typing import Mapping +from typing import NewType +from typing import NoReturn +from typing import Optional +from typing import overload +from typing import Set +from typing import Tuple +from typing import Type +from typing import TYPE_CHECKING +from typing import TypeVar +from typing import Union + +import typing_extensions + +from . import compat + +if True: # zimports removes the tailing comments + from typing_extensions import Annotated as Annotated # 3.8 + from typing_extensions import Concatenate as Concatenate # 3.10 + from typing_extensions import ( + dataclass_transform as dataclass_transform, # 3.11, + ) + from typing_extensions import Final as Final # 3.8 + from typing_extensions import final as final # 3.8 + from typing_extensions import get_args as get_args # 3.10 + from typing_extensions import get_origin as get_origin # 3.10 + from typing_extensions import Literal as Literal # 3.8 + from typing_extensions import NotRequired as NotRequired # 3.11 + from typing_extensions import ParamSpec as ParamSpec # 3.10 + from typing_extensions import Protocol as Protocol # 3.8 + from typing_extensions import SupportsIndex as SupportsIndex # 3.8 + from typing_extensions import TypeAlias as TypeAlias # 3.10 + from typing_extensions import TypedDict as TypedDict # 3.8 + from typing_extensions import TypeGuard as TypeGuard # 3.10 + from typing_extensions import Self as Self # 3.11 + from typing_extensions import TypeAliasType as TypeAliasType # 3.12 + from typing_extensions import Never as Never # 3.11 + from typing_extensions import LiteralString as LiteralString # 3.11 + +_T = TypeVar("_T", bound=Any) +_KT = TypeVar("_KT") +_KT_co = TypeVar("_KT_co", covariant=True) +_KT_contra = TypeVar("_KT_contra", contravariant=True) +_VT = TypeVar("_VT") +_VT_co = TypeVar("_VT_co", covariant=True) + +if compat.py310: + # why they took until py310 to put this in stdlib is beyond me, + # I've been wanting it since py27 + from types import NoneType as NoneType +else: + NoneType = type(None) # type: ignore + + +def is_fwd_none(typ: Any) -> bool: + return isinstance(typ, ForwardRef) and typ.__forward_arg__ == "None" + + +_AnnotationScanType = Union[ + Type[Any], str, ForwardRef, NewType, TypeAliasType, "GenericProtocol[Any]" +] + + +class ArgsTypeProtocol(Protocol): + """protocol for types that have ``__args__`` + + there's no public interface for this AFAIK + + """ + + __args__: Tuple[_AnnotationScanType, ...] + + +class GenericProtocol(Protocol[_T]): + """protocol for generic types. + + this since Python.typing _GenericAlias is private + + """ + + __args__: Tuple[_AnnotationScanType, ...] + __origin__: Type[_T] + + # Python's builtin _GenericAlias has this method, however builtins like + # list, dict, etc. do not, even though they have ``__origin__`` and + # ``__args__`` + # + # def copy_with(self, params: Tuple[_AnnotationScanType, ...]) -> Type[_T]: + # ... + + +# copied from TypeShed, required in order to implement +# MutableMapping.update() +class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): + def keys(self) -> Iterable[_KT]: ... + + def __getitem__(self, __k: _KT) -> _VT_co: ... + + +# work around https://github.com/microsoft/pyright/issues/3025 +_LiteralStar = Literal["*"] + + +def de_stringify_annotation( + cls: Type[Any], + annotation: _AnnotationScanType, + originating_module: str, + locals_: Mapping[str, Any], + *, + str_cleanup_fn: Optional[Callable[[str, str], str]] = None, + include_generic: bool = False, + _already_seen: Optional[Set[Any]] = None, +) -> Type[Any]: + """Resolve annotations that may be string based into real objects. + + This is particularly important if a module defines "from __future__ import + annotations", as everything inside of __annotations__ is a string. We want + to at least have generic containers like ``Mapped``, ``Union``, ``List``, + etc. + + """ + # looked at typing.get_type_hints(), looked at pydantic. We need much + # less here, and we here try to not use any private typing internals + # or construct ForwardRef objects which is documented as something + # that should be avoided. + + original_annotation = annotation + + if is_fwd_ref(annotation): + annotation = annotation.__forward_arg__ + + if isinstance(annotation, str): + if str_cleanup_fn: + annotation = str_cleanup_fn(annotation, originating_module) + + annotation = eval_expression( + annotation, originating_module, locals_=locals_, in_class=cls + ) + + if ( + include_generic + and is_generic(annotation) + and not is_literal(annotation) + ): + if _already_seen is None: + _already_seen = set() + + if annotation in _already_seen: + # only occurs recursively. outermost return type + # will always be Type. + # the element here will be either ForwardRef or + # Optional[ForwardRef] + return original_annotation # type: ignore + else: + _already_seen.add(annotation) + + elements = tuple( + de_stringify_annotation( + cls, + elem, + originating_module, + locals_, + str_cleanup_fn=str_cleanup_fn, + include_generic=include_generic, + _already_seen=_already_seen, + ) + for elem in annotation.__args__ + ) + + return _copy_generic_annotation_with(annotation, elements) + + return annotation # type: ignore + + +def fixup_container_fwd_refs( + type_: _AnnotationScanType, +) -> _AnnotationScanType: + """Correct dict['x', 'y'] into dict[ForwardRef('x'), ForwardRef('y')] + and similar for list, set + + """ + + if ( + is_generic(type_) + and get_origin(type_) + in ( + dict, + set, + list, + collections_abc.MutableSet, + collections_abc.MutableMapping, + collections_abc.MutableSequence, + collections_abc.Mapping, + collections_abc.Sequence, + ) + # fight, kick and scream to struggle to tell the difference between + # dict[] and typing.Dict[] which DO NOT compare the same and DO NOT + # behave the same yet there is NO WAY to distinguish between which type + # it is using public attributes + and not re.match( + "typing.(?:Dict|List|Set|.*Mapping|.*Sequence|.*Set)", repr(type_) + ) + ): + # compat with py3.10 and earlier + return get_origin(type_).__class_getitem__( # type: ignore + tuple( + [ + ForwardRef(elem) if isinstance(elem, str) else elem + for elem in get_args(type_) + ] + ) + ) + return type_ + + +def _copy_generic_annotation_with( + annotation: GenericProtocol[_T], elements: Tuple[_AnnotationScanType, ...] +) -> Type[_T]: + if hasattr(annotation, "copy_with"): + # List, Dict, etc. real generics + return annotation.copy_with(elements) # type: ignore + else: + # Python builtins list, dict, etc. + return annotation.__origin__[elements] # type: ignore + + +def eval_expression( + expression: str, + module_name: str, + *, + locals_: Optional[Mapping[str, Any]] = None, + in_class: Optional[Type[Any]] = None, +) -> Any: + try: + base_globals: Dict[str, Any] = sys.modules[module_name].__dict__ + except KeyError as ke: + raise NameError( + f"Module {module_name} isn't present in sys.modules; can't " + f"evaluate expression {expression}" + ) from ke + + try: + if in_class is not None: + cls_namespace = dict(in_class.__dict__) + cls_namespace.setdefault(in_class.__name__, in_class) + + # see #10899. We want the locals/globals to take precedence + # over the class namespace in this context, even though this + # is not the usual way variables would resolve. + cls_namespace.update(base_globals) + + annotation = eval(expression, cls_namespace, locals_) + else: + annotation = eval(expression, base_globals, locals_) + except Exception as err: + raise NameError( + f"Could not de-stringify annotation {expression!r}" + ) from err + else: + return annotation + + +def eval_name_only( + name: str, + module_name: str, + *, + locals_: Optional[Mapping[str, Any]] = None, +) -> Any: + if "." in name: + return eval_expression(name, module_name, locals_=locals_) + + try: + base_globals: Dict[str, Any] = sys.modules[module_name].__dict__ + except KeyError as ke: + raise NameError( + f"Module {module_name} isn't present in sys.modules; can't " + f"resolve name {name}" + ) from ke + + # name only, just look in globals. eval() works perfectly fine here, + # however we are seeking to have this be faster, as this occurs for + # every Mapper[] keyword, etc. depending on configuration + try: + return base_globals[name] + except KeyError as ke: + # check in builtins as well to handle `list`, `set` or `dict`, etc. + try: + return builtins.__dict__[name] + except KeyError: + pass + + raise NameError( + f"Could not locate name {name} in module {module_name}" + ) from ke + + +def resolve_name_to_real_class_name(name: str, module_name: str) -> str: + try: + obj = eval_name_only(name, module_name) + except NameError: + return name + else: + return getattr(obj, "__name__", name) + + +def is_pep593(type_: Optional[Any]) -> bool: + return type_ is not None and get_origin(type_) in _type_tuples.Annotated + + +def is_non_string_iterable(obj: Any) -> TypeGuard[Iterable[Any]]: + return isinstance(obj, collections_abc.Iterable) and not isinstance( + obj, (str, bytes) + ) + + +def is_literal(type_: Any) -> bool: + return get_origin(type_) in _type_tuples.Literal + + +def is_newtype(type_: Optional[_AnnotationScanType]) -> TypeGuard[NewType]: + return hasattr(type_, "__supertype__") + + # doesn't work in 3.8, 3.7 as it passes a closure, not an + # object instance + # isinstance(type, type_instances.NewType) + + +def is_generic(type_: _AnnotationScanType) -> TypeGuard[GenericProtocol[Any]]: + return hasattr(type_, "__args__") and hasattr(type_, "__origin__") + + +def is_pep695(type_: _AnnotationScanType) -> TypeGuard[TypeAliasType]: + # NOTE: a generic TAT does not instance check as TypeAliasType outside of + # python 3.10. For sqlalchemy use cases it's fine to consider it a TAT + # though. + # NOTE: things seems to work also without this additional check + if is_generic(type_): + return is_pep695(type_.__origin__) + return isinstance(type_, _type_instances.TypeAliasType) + + +def flatten_newtype(type_: NewType) -> Type[Any]: + super_type = type_.__supertype__ + while is_newtype(super_type): + super_type = super_type.__supertype__ + return super_type # type: ignore[return-value] + + +def pep695_values(type_: _AnnotationScanType) -> Set[Any]: + """Extracts the value from a TypeAliasType, recursively exploring unions + and inner TypeAliasType to flatten them into a single set. + + Forward references are not evaluated, so no recursive exploration happens + into them. + """ + _seen = set() + + def recursive_value(inner_type): + if inner_type in _seen: + # recursion are not supported (at least it's flagged as + # an error by pyright). Just avoid infinite loop + return inner_type + _seen.add(inner_type) + if not is_pep695(inner_type): + return inner_type + value = inner_type.__value__ + if not is_union(value): + return value + return [recursive_value(t) for t in value.__args__] + + res = recursive_value(type_) + if isinstance(res, list): + types = set() + stack = deque(res) + while stack: + t = stack.popleft() + if isinstance(t, list): + stack.extend(t) + else: + types.add(None if t is NoneType or is_fwd_none(t) else t) + return types + else: + return {res} + + +def is_fwd_ref( + type_: _AnnotationScanType, + check_generic: bool = False, + check_for_plain_string: bool = False, +) -> TypeGuard[ForwardRef]: + if check_for_plain_string and isinstance(type_, str): + return True + elif isinstance(type_, _type_instances.ForwardRef): + return True + elif check_generic and is_generic(type_): + return any( + is_fwd_ref( + arg, True, check_for_plain_string=check_for_plain_string + ) + for arg in type_.__args__ + ) + else: + return False + + +@overload +def de_optionalize_union_types(type_: str) -> str: ... + + +@overload +def de_optionalize_union_types(type_: Type[Any]) -> Type[Any]: ... + + +@overload +def de_optionalize_union_types( + type_: _AnnotationScanType, +) -> _AnnotationScanType: ... + + +def de_optionalize_union_types( + type_: _AnnotationScanType, +) -> _AnnotationScanType: + """Given a type, filter out ``Union`` types that include ``NoneType`` + to not include the ``NoneType``. + + Contains extra logic to work on non-flattened unions, unions that contain + ``None`` (seen in py38, 37) + + """ + + if is_fwd_ref(type_): + return _de_optionalize_fwd_ref_union_types(type_, False) + + elif is_union(type_) and includes_none(type_): + if compat.py39: + typ = set(type_.__args__) + else: + # py38, 37 - unions are not automatically flattened, can contain + # None rather than NoneType + stack_of_unions = deque([type_]) + typ = set() + while stack_of_unions: + u_typ = stack_of_unions.popleft() + for elem in u_typ.__args__: + if is_union(elem): + stack_of_unions.append(elem) + else: + typ.add(elem) + + typ.discard(None) # type: ignore[arg-type,unused-ignore] + + typ = {t for t in typ if t is not NoneType and not is_fwd_none(t)} + + return make_union_type(*typ) + + else: + return type_ + + +@overload +def _de_optionalize_fwd_ref_union_types( + type_: ForwardRef, return_has_none: Literal[True] +) -> bool: ... + + +@overload +def _de_optionalize_fwd_ref_union_types( + type_: ForwardRef, return_has_none: Literal[False] +) -> _AnnotationScanType: ... + + +def _de_optionalize_fwd_ref_union_types( + type_: ForwardRef, return_has_none: bool +) -> Union[_AnnotationScanType, bool]: + """return the non-optional type for Optional[], Union[None, ...], x|None, + etc. without de-stringifying forward refs. + + unfortunately this seems to require lots of hardcoded heuristics + + """ + + annotation = type_.__forward_arg__ + + mm = re.match(r"^(.+?)\[(.+)\]$", annotation) + if mm: + g1 = mm.group(1).split(".")[-1] + if g1 == "Optional": + return True if return_has_none else ForwardRef(mm.group(2)) + elif g1 == "Union": + if "[" in mm.group(2): + # cases like "Union[Dict[str, int], int, None]" + elements: list[str] = [] + current: list[str] = [] + ignore_comma = 0 + for char in mm.group(2): + if char == "[": + ignore_comma += 1 + elif char == "]": + ignore_comma -= 1 + elif ignore_comma == 0 and char == ",": + elements.append("".join(current).strip()) + current.clear() + continue + current.append(char) + else: + elements = re.split(r",\s*", mm.group(2)) + parts = [ForwardRef(elem) for elem in elements if elem != "None"] + if return_has_none: + return len(elements) != len(parts) + else: + return make_union_type(*parts) if parts else Never # type: ignore[return-value] # noqa: E501 + else: + return False if return_has_none else type_ + + pipe_tokens = re.split(r"\s*\|\s*", annotation) + has_none = "None" in pipe_tokens + if return_has_none: + return has_none + if has_none: + anno_str = "|".join(p for p in pipe_tokens if p != "None") + return ForwardRef(anno_str) if anno_str else Never # type: ignore[return-value] # noqa: E501 + + return type_ + + +def make_union_type(*types: _AnnotationScanType) -> Type[Any]: + """Make a Union type.""" + + return Union[types] # type: ignore + + +def includes_none(type_: Any) -> bool: + """Returns if the type annotation ``type_`` allows ``None``. + + This function supports: + * forward refs + * unions + * pep593 - Annotated + * pep695 - TypeAliasType (does not support looking into + fw reference of other pep695) + * NewType + * plain types like ``int``, ``None``, etc + """ + if is_fwd_ref(type_): + return _de_optionalize_fwd_ref_union_types(type_, True) + if is_union(type_): + return any(includes_none(t) for t in get_args(type_)) + if is_pep593(type_): + return includes_none(get_args(type_)[0]) + if is_pep695(type_): + return any(includes_none(t) for t in pep695_values(type_)) + if is_newtype(type_): + return includes_none(type_.__supertype__) + try: + return type_ in (NoneType, None) or is_fwd_none(type_) + except TypeError: + # if type_ is Column, mapped_column(), etc. the use of "in" + # resolves to ``__eq__()`` which then gives us an expression object + # that can't resolve to boolean. just catch it all via exception + return False + + +def is_a_type(type_: Any) -> bool: + return ( + isinstance(type_, type) + or get_origin(type_) is not None + or getattr(type_, "__module__", None) + in ("typing", "typing_extensions") + or type(type_).__mro__[0].__module__ in ("typing", "typing_extensions") + ) + + +def is_union(type_: Any) -> TypeGuard[ArgsTypeProtocol]: + return is_origin_of(type_, "Union", "UnionType") + + +def is_origin_of_cls( + type_: Any, class_obj: Union[Tuple[Type[Any], ...], Type[Any]] +) -> bool: + """return True if the given type has an __origin__ that shares a base + with the given class""" + + origin = get_origin(type_) + if origin is None: + return False + + return isinstance(origin, type) and issubclass(origin, class_obj) + + +def is_origin_of( + type_: Any, *names: str, module: Optional[str] = None +) -> bool: + """return True if the given type has an __origin__ with the given name + and optional module.""" + + origin = get_origin(type_) + if origin is None: + return False + + return _get_type_name(origin) in names and ( + module is None or origin.__module__.startswith(module) + ) + + +def _get_type_name(type_: Type[Any]) -> str: + if compat.py310: + return type_.__name__ + else: + typ_name = getattr(type_, "__name__", None) + if typ_name is None: + typ_name = getattr(type_, "_name", None) + + return typ_name # type: ignore + + +class DescriptorProto(Protocol): + def __get__(self, instance: object, owner: Any) -> Any: ... + + def __set__(self, instance: Any, value: Any) -> None: ... + + def __delete__(self, instance: Any) -> None: ... + + +_DESC = TypeVar("_DESC", bound=DescriptorProto) + + +class DescriptorReference(Generic[_DESC]): + """a descriptor that refers to a descriptor. + + used for cases where we need to have an instance variable referring to an + object that is itself a descriptor, which typically confuses typing tools + as they don't know when they should use ``__get__`` or not when referring + to the descriptor assignment as an instance variable. See + sqlalchemy.orm.interfaces.PropComparator.prop + + """ + + if TYPE_CHECKING: + + def __get__(self, instance: object, owner: Any) -> _DESC: ... + + def __set__(self, instance: Any, value: _DESC) -> None: ... + + def __delete__(self, instance: Any) -> None: ... + + +_DESC_co = TypeVar("_DESC_co", bound=DescriptorProto, covariant=True) + + +class RODescriptorReference(Generic[_DESC_co]): + """a descriptor that refers to a descriptor. + + same as :class:`.DescriptorReference` but is read-only, so that subclasses + can define a subtype as the generically contained element + + """ + + if TYPE_CHECKING: + + def __get__(self, instance: object, owner: Any) -> _DESC_co: ... + + def __set__(self, instance: Any, value: Any) -> NoReturn: ... + + def __delete__(self, instance: Any) -> NoReturn: ... + + +_FN = TypeVar("_FN", bound=Optional[Callable[..., Any]]) + + +class CallableReference(Generic[_FN]): + """a descriptor that refers to a callable. + + works around mypy's limitation of not allowing callables assigned + as instance variables + + + """ + + if TYPE_CHECKING: + + def __get__(self, instance: object, owner: Any) -> _FN: ... + + def __set__(self, instance: Any, value: _FN) -> None: ... + + def __delete__(self, instance: Any) -> None: ... + + +class _TypingInstances: + def __getattr__(self, key: str) -> tuple[type, ...]: + types = tuple( + { + t + for t in [ + getattr(typing, key, None), + getattr(typing_extensions, key, None), + ] + if t is not None + } + ) + if not types: + raise AttributeError(key) + self.__dict__[key] = types + return types + + +_type_tuples = _TypingInstances() +if TYPE_CHECKING: + _type_instances = typing_extensions +else: + _type_instances = _type_tuples + +LITERAL_TYPES = _type_tuples.Literal