diff --git a/lib/python3.12/site-packages/annotated_types/__init__.py b/lib/python3.12/site-packages/annotated_types/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..74e0deeab3f5904260ac2d36d64fbdec7e0ee0bf --- /dev/null +++ b/lib/python3.12/site-packages/annotated_types/__init__.py @@ -0,0 +1,432 @@ +import math +import sys +import types +from dataclasses import dataclass +from datetime import tzinfo +from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union + +if sys.version_info < (3, 8): + from typing_extensions import Protocol, runtime_checkable +else: + from typing import Protocol, runtime_checkable + +if sys.version_info < (3, 9): + from typing_extensions import Annotated, Literal +else: + from typing import Annotated, Literal + +if sys.version_info < (3, 10): + EllipsisType = type(Ellipsis) + KW_ONLY = {} + SLOTS = {} +else: + from types import EllipsisType + + KW_ONLY = {"kw_only": True} + SLOTS = {"slots": True} + + +__all__ = ( + 'BaseMetadata', + 'GroupedMetadata', + 'Gt', + 'Ge', + 'Lt', + 'Le', + 'Interval', + 'MultipleOf', + 'MinLen', + 'MaxLen', + 'Len', + 'Timezone', + 'Predicate', + 'LowerCase', + 'UpperCase', + 'IsDigits', + 'IsFinite', + 'IsNotFinite', + 'IsNan', + 'IsNotNan', + 'IsInfinite', + 'IsNotInfinite', + 'doc', + 'DocInfo', + '__version__', +) + +__version__ = '0.7.0' + + +T = TypeVar('T') + + +# arguments that start with __ are considered +# positional only +# see https://peps.python.org/pep-0484/#positional-only-arguments + + +class SupportsGt(Protocol): + def __gt__(self: T, __other: T) -> bool: + ... + + +class SupportsGe(Protocol): + def __ge__(self: T, __other: T) -> bool: + ... + + +class SupportsLt(Protocol): + def __lt__(self: T, __other: T) -> bool: + ... + + +class SupportsLe(Protocol): + def __le__(self: T, __other: T) -> bool: + ... + + +class SupportsMod(Protocol): + def __mod__(self: T, __other: T) -> T: + ... + + +class SupportsDiv(Protocol): + def __div__(self: T, __other: T) -> T: + ... + + +class BaseMetadata: + """Base class for all metadata. + + This exists mainly so that implementers + can do `isinstance(..., BaseMetadata)` while traversing field annotations. + """ + + __slots__ = () + + +@dataclass(frozen=True, **SLOTS) +class Gt(BaseMetadata): + """Gt(gt=x) implies that the value must be greater than x. + + It can be used with any type that supports the ``>`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + gt: SupportsGt + + +@dataclass(frozen=True, **SLOTS) +class Ge(BaseMetadata): + """Ge(ge=x) implies that the value must be greater than or equal to x. + + It can be used with any type that supports the ``>=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + ge: SupportsGe + + +@dataclass(frozen=True, **SLOTS) +class Lt(BaseMetadata): + """Lt(lt=x) implies that the value must be less than x. + + It can be used with any type that supports the ``<`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + lt: SupportsLt + + +@dataclass(frozen=True, **SLOTS) +class Le(BaseMetadata): + """Le(le=x) implies that the value must be less than or equal to x. + + It can be used with any type that supports the ``<=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + le: SupportsLe + + +@runtime_checkable +class GroupedMetadata(Protocol): + """A grouping of multiple objects, like typing.Unpack. + + `GroupedMetadata` on its own is not metadata and has no meaning. + All of the constraints and metadata should be fully expressable + in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`. + + Concrete implementations should override `GroupedMetadata.__iter__()` + to add their own metadata. + For example: + + >>> @dataclass + >>> class Field(GroupedMetadata): + >>> gt: float | None = None + >>> description: str | None = None + ... + >>> def __iter__(self) -> Iterable[object]: + >>> if self.gt is not None: + >>> yield Gt(self.gt) + >>> if self.description is not None: + >>> yield Description(self.gt) + + Also see the implementation of `Interval` below for an example. + + Parsers should recognize this and unpack it so that it can be used + both with and without unpacking: + + - `Annotated[int, Field(...)]` (parser must unpack Field) + - `Annotated[int, *Field(...)]` (PEP-646) + """ # noqa: trailing-whitespace + + @property + def __is_annotated_types_grouped_metadata__(self) -> Literal[True]: + return True + + def __iter__(self) -> Iterator[object]: + ... + + if not TYPE_CHECKING: + __slots__ = () # allow subclasses to use slots + + def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: + # Basic ABC like functionality without the complexity of an ABC + super().__init_subclass__(*args, **kwargs) + if cls.__iter__ is GroupedMetadata.__iter__: + raise TypeError("Can't subclass GroupedMetadata without implementing __iter__") + + def __iter__(self) -> Iterator[object]: # noqa: F811 + raise NotImplementedError # more helpful than "None has no attribute..." type errors + + +@dataclass(frozen=True, **KW_ONLY, **SLOTS) +class Interval(GroupedMetadata): + """Interval can express inclusive or exclusive bounds with a single object. + + It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which + are interpreted the same way as the single-bound constraints. + """ + + gt: Union[SupportsGt, None] = None + ge: Union[SupportsGe, None] = None + lt: Union[SupportsLt, None] = None + le: Union[SupportsLe, None] = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack an Interval into zero or more single-bounds.""" + if self.gt is not None: + yield Gt(self.gt) + if self.ge is not None: + yield Ge(self.ge) + if self.lt is not None: + yield Lt(self.lt) + if self.le is not None: + yield Le(self.le) + + +@dataclass(frozen=True, **SLOTS) +class MultipleOf(BaseMetadata): + """MultipleOf(multiple_of=x) might be interpreted in two ways: + + 1. Python semantics, implying ``value % multiple_of == 0``, or + 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of`` + + We encourage users to be aware of these two common interpretations, + and libraries to carefully document which they implement. + """ + + multiple_of: Union[SupportsDiv, SupportsMod] + + +@dataclass(frozen=True, **SLOTS) +class MinLen(BaseMetadata): + """ + MinLen() implies minimum inclusive length, + e.g. ``len(value) >= min_length``. + """ + + min_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, **SLOTS) +class MaxLen(BaseMetadata): + """ + MaxLen() implies maximum inclusive length, + e.g. ``len(value) <= max_length``. + """ + + max_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, **SLOTS) +class Len(GroupedMetadata): + """ + Len() implies that ``min_length <= len(value) <= max_length``. + + Upper bound may be omitted or ``None`` to indicate no upper length bound. + """ + + min_length: Annotated[int, Ge(0)] = 0 + max_length: Optional[Annotated[int, Ge(0)]] = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack a Len into zone or more single-bounds.""" + if self.min_length > 0: + yield MinLen(self.min_length) + if self.max_length is not None: + yield MaxLen(self.max_length) + + +@dataclass(frozen=True, **SLOTS) +class Timezone(BaseMetadata): + """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive). + + ``Annotated[datetime, Timezone(None)]`` must be a naive datetime. + ``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be + tz-aware but any timezone is allowed. + + You may also pass a specific timezone string or tzinfo object such as + ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that + you only allow a specific timezone, though we note that this is often + a symptom of poor design. + """ + + tz: Union[str, tzinfo, EllipsisType, None] + + +@dataclass(frozen=True, **SLOTS) +class Unit(BaseMetadata): + """Indicates that the value is a physical quantity with the specified unit. + + It is intended for usage with numeric types, where the value represents the + magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]`` + or ``speed: Annotated[float, Unit('m/s')]``. + + Interpretation of the unit string is left to the discretion of the consumer. + It is suggested to follow conventions established by python libraries that work + with physical quantities, such as + + - ``pint`` : + - ``astropy.units``: + + For indicating a quantity with a certain dimensionality but without a specific unit + it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`. + Note, however, ``annotated_types`` itself makes no use of the unit string. + """ + + unit: str + + +@dataclass(frozen=True, **SLOTS) +class Predicate(BaseMetadata): + """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values. + + Users should prefer statically inspectable metadata, but if you need the full + power and flexibility of arbitrary runtime predicates... here it is. + + We provide a few predefined predicates for common string constraints: + ``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and + ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which + can be given special handling, and avoid indirection like ``lambda s: s.lower()``. + + Some libraries might have special logic to handle certain predicates, e.g. by + checking for `str.isdigit` and using its presence to both call custom logic to + enforce digit-only strings, and customise some generated external schema. + + We do not specify what behaviour should be expected for predicates that raise + an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently + skip invalid constraints, or statically raise an error; or it might try calling it + and then propagate or discard the resulting exception. + """ + + func: Callable[[Any], bool] + + def __repr__(self) -> str: + if getattr(self.func, "__name__", "") == "": + return f"{self.__class__.__name__}({self.func!r})" + if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and ( + namespace := getattr(self.func.__self__, "__name__", None) + ): + return f"{self.__class__.__name__}({namespace}.{self.func.__name__})" + if isinstance(self.func, type(str.isascii)): # method descriptor + return f"{self.__class__.__name__}({self.func.__qualname__})" + return f"{self.__class__.__name__}({self.func.__name__})" + + +@dataclass +class Not: + func: Callable[[Any], bool] + + def __call__(self, __v: Any) -> bool: + return not self.func(__v) + + +_StrType = TypeVar("_StrType", bound=str) + +LowerCase = Annotated[_StrType, Predicate(str.islower)] +""" +Return True if the string is a lowercase string, False otherwise. + +A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. +""" # noqa: E501 +UpperCase = Annotated[_StrType, Predicate(str.isupper)] +""" +Return True if the string is an uppercase string, False otherwise. + +A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. +""" # noqa: E501 +IsDigit = Annotated[_StrType, Predicate(str.isdigit)] +IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63 +""" +Return True if the string is a digit string, False otherwise. + +A string is a digit string if all characters in the string are digits and there is at least one character in the string. +""" # noqa: E501 +IsAscii = Annotated[_StrType, Predicate(str.isascii)] +""" +Return True if all characters in the string are ASCII, False otherwise. + +ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. +""" + +_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex]) +IsFinite = Annotated[_NumericType, Predicate(math.isfinite)] +"""Return True if x is neither an infinity nor a NaN, and False otherwise.""" +IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))] +"""Return True if x is one of infinity or NaN, and False otherwise""" +IsNan = Annotated[_NumericType, Predicate(math.isnan)] +"""Return True if x is a NaN (not a number), and False otherwise.""" +IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))] +"""Return True if x is anything but NaN (not a number), and False otherwise.""" +IsInfinite = Annotated[_NumericType, Predicate(math.isinf)] +"""Return True if x is a positive or negative infinity, and False otherwise.""" +IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))] +"""Return True if x is neither a positive or negative infinity, and False otherwise.""" + +try: + from typing_extensions import DocInfo, doc # type: ignore [attr-defined] +except ImportError: + + @dataclass(frozen=True, **SLOTS) + class DocInfo: # type: ignore [no-redef] + """ " + The return value of doc(), mainly to be used by tools that want to extract the + Annotated documentation at runtime. + """ + + documentation: str + """The documentation string passed to doc().""" + + def doc( + documentation: str, + ) -> DocInfo: + """ + Add documentation to a type annotation inside of Annotated. + + For example: + + >>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ... + """ + return DocInfo(documentation) diff --git a/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dc219da11d2aa628fc628503a13afc70751acb9 Binary files /dev/null and b/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc b/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b163a66db810d7ed9a4a20ee021e94e02b84343 Binary files /dev/null and b/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/annotated_types/py.typed b/lib/python3.12/site-packages/annotated_types/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/annotated_types/test_cases.py b/lib/python3.12/site-packages/annotated_types/test_cases.py new file mode 100644 index 0000000000000000000000000000000000000000..d9164d6883d2dd47cb766b483592ca3730f6f09d --- /dev/null +++ b/lib/python3.12/site-packages/annotated_types/test_cases.py @@ -0,0 +1,151 @@ +import math +import sys +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal +from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple + +if sys.version_info < (3, 9): + from typing_extensions import Annotated +else: + from typing import Annotated + +import annotated_types as at + + +class Case(NamedTuple): + """ + A test case for `annotated_types`. + """ + + annotation: Any + valid_cases: Iterable[Any] + invalid_cases: Iterable[Any] + + +def cases() -> Iterable[Case]: + # Gt, Ge, Lt, Le + yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Gt(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(date(2000, 1, 1))], + [date(2000, 1, 2), date(2000, 1, 3)], + [date(2000, 1, 1), date(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(Decimal('1.123'))], + [Decimal('1.1231'), Decimal('123')], + [Decimal('1.123'), Decimal('0')], + ) + + yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1)) + yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Ge(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(1998, 1, 1), datetime(1999, 12, 31)], + ) + + yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4)) + yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Lt(datetime(2000, 1, 1))], + [datetime(1999, 12, 31), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000)) + yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Le(datetime(2000, 1, 1))], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + # Interval + yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1)) + yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1)) + yield Case( + Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(2000, 1, 4)], + ) + + yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4)) + yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1)) + + # lengths + + yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + + yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + + yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10)) + yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234')) + + yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}]) + yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4})) + yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4))) + + # Timezone + + yield Case( + Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)] + ) + yield Case( + Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)] + ) + yield Case( + Annotated[datetime, at.Timezone(timezone.utc)], + [datetime(2000, 1, 1, tzinfo=timezone.utc)], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + yield Case( + Annotated[datetime, at.Timezone('Europe/London')], + [datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + + # Quantity + + yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m')) + + # predicate types + + yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom']) + yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC']) + yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2']) + yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀']) + + yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5]) + + yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf]) + yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23]) + yield Case(at.IsNan[float], [math.nan], [1.23, math.inf]) + yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan]) + yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23]) + yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf]) + + # check stacked predicates + yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan]) + + # doc + yield Case(Annotated[int, at.doc("A number")], [1, 2], []) + + # custom GroupedMetadata + class MyCustomGroupedMetadata(at.GroupedMetadata): + def __iter__(self) -> Iterator[at.Predicate]: + yield at.Predicate(lambda x: float(x).is_integer()) + + yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5]) diff --git a/lib/python3.12/site-packages/antlr4/BufferedTokenStream.py b/lib/python3.12/site-packages/antlr4/BufferedTokenStream.py new file mode 100644 index 0000000000000000000000000000000000000000..aaa58c63dc02b8b0bf43398b751f1d14fef89ad9 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/BufferedTokenStream.py @@ -0,0 +1,302 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. + +# This implementation of {@link TokenStream} loads tokens from a +# {@link TokenSource} on-demand, and places the tokens in a buffer to provide +# access to any previous token by index. +# +#

+# This token stream ignores the value of {@link Token#getChannel}. If your +# parser requires the token stream filter tokens to only those on a particular +# channel, such as {@link Token#DEFAULT_CHANNEL} or +# {@link Token#HIDDEN_CHANNEL}, use a filtering token stream such a +# {@link CommonTokenStream}.

+from io import StringIO +from antlr4.Token import Token +from antlr4.error.Errors import IllegalStateException + +# need forward declaration +Lexer = None + +# this is just to keep meaningful parameter types to Parser +class TokenStream(object): + + pass + + +class BufferedTokenStream(TokenStream): + __slots__ = ('tokenSource', 'tokens', 'index', 'fetchedEOF') + + def __init__(self, tokenSource:Lexer): + # The {@link TokenSource} from which tokens for this stream are fetched. + self.tokenSource = tokenSource + + # A collection of all tokens fetched from the token source. The list is + # considered a complete view of the input once {@link #fetchedEOF} is set + # to {@code true}. + self.tokens = [] + + # The index into {@link #tokens} of the current token (next token to + # {@link #consume}). {@link #tokens}{@code [}{@link #p}{@code ]} should be + # {@link #LT LT(1)}. + # + #

This field is set to -1 when the stream is first constructed or when + # {@link #setTokenSource} is called, indicating that the first token has + # not yet been fetched from the token source. For additional information, + # see the documentation of {@link IntStream} for a description of + # Initializing Methods.

+ self.index = -1 + + # Indicates whether the {@link Token#EOF} token has been fetched from + # {@link #tokenSource} and added to {@link #tokens}. This field improves + # performance for the following cases: + # + #
    + #
  • {@link #consume}: The lookahead check in {@link #consume} to prevent + # consuming the EOF symbol is optimized by checking the values of + # {@link #fetchedEOF} and {@link #p} instead of calling {@link #LA}.
  • + #
  • {@link #fetch}: The check to prevent adding multiple EOF symbols into + # {@link #tokens} is trivial with this field.
  • + #
      + self.fetchedEOF = False + + def mark(self): + return 0 + + def release(self, marker:int): + # no resources to release + pass + + def reset(self): + self.seek(0) + + def seek(self, index:int): + self.lazyInit() + self.index = self.adjustSeekIndex(index) + + def get(self, index:int): + self.lazyInit() + return self.tokens[index] + + def consume(self): + skipEofCheck = False + if self.index >= 0: + if self.fetchedEOF: + # the last token in tokens is EOF. skip check if p indexes any + # fetched token except the last. + skipEofCheck = self.index < len(self.tokens) - 1 + else: + # no EOF token in tokens. skip check if p indexes a fetched token. + skipEofCheck = self.index < len(self.tokens) + else: + # not yet initialized + skipEofCheck = False + + if not skipEofCheck and self.LA(1) == Token.EOF: + raise IllegalStateException("cannot consume EOF") + + if self.sync(self.index + 1): + self.index = self.adjustSeekIndex(self.index + 1) + + # Make sure index {@code i} in tokens has a token. + # + # @return {@code true} if a token is located at index {@code i}, otherwise + # {@code false}. + # @see #get(int i) + #/ + def sync(self, i:int): + n = i - len(self.tokens) + 1 # how many more elements we need? + if n > 0 : + fetched = self.fetch(n) + return fetched >= n + return True + + # Add {@code n} elements to buffer. + # + # @return The actual number of elements added to the buffer. + #/ + def fetch(self, n:int): + if self.fetchedEOF: + return 0 + for i in range(0, n): + t = self.tokenSource.nextToken() + t.tokenIndex = len(self.tokens) + self.tokens.append(t) + if t.type==Token.EOF: + self.fetchedEOF = True + return i + 1 + return n + + + # Get all tokens from start..stop inclusively#/ + def getTokens(self, start:int, stop:int, types:set=None): + if start<0 or stop<0: + return None + self.lazyInit() + subset = [] + if stop >= len(self.tokens): + stop = len(self.tokens)-1 + for i in range(start, stop): + t = self.tokens[i] + if t.type==Token.EOF: + break + if types is None or t.type in types: + subset.append(t) + return subset + + def LA(self, i:int): + return self.LT(i).type + + def LB(self, k:int): + if (self.index-k) < 0: + return None + return self.tokens[self.index-k] + + def LT(self, k:int): + self.lazyInit() + if k==0: + return None + if k < 0: + return self.LB(-k) + i = self.index + k - 1 + self.sync(i) + if i >= len(self.tokens): # return EOF token + # EOF must be last token + return self.tokens[len(self.tokens)-1] + return self.tokens[i] + + # Allowed derived classes to modify the behavior of operations which change + # the current stream position by adjusting the target token index of a seek + # operation. The default implementation simply returns {@code i}. If an + # exception is thrown in this method, the current stream index should not be + # changed. + # + #

      For example, {@link CommonTokenStream} overrides this method to ensure that + # the seek target is always an on-channel token.

      + # + # @param i The target token index. + # @return The adjusted target token index. + + def adjustSeekIndex(self, i:int): + return i + + def lazyInit(self): + if self.index == -1: + self.setup() + + def setup(self): + self.sync(0) + self.index = self.adjustSeekIndex(0) + + # Reset this token stream by setting its token source.#/ + def setTokenSource(self, tokenSource:Lexer): + self.tokenSource = tokenSource + self.tokens = [] + self.index = -1 + self.fetchedEOF = False + + + # Given a starting index, return the index of the next token on channel. + # Return i if tokens[i] is on channel. Return the index of the EOF token + # if there are no tokens on channel between i and EOF. + #/ + def nextTokenOnChannel(self, i:int, channel:int): + self.sync(i) + if i>=len(self.tokens): + return len(self.tokens) - 1 + token = self.tokens[i] + while token.channel!=channel: + if token.type==Token.EOF: + return i + i += 1 + self.sync(i) + token = self.tokens[i] + return i + + # Given a starting index, return the index of the previous token on channel. + # Return i if tokens[i] is on channel. Return -1 if there are no tokens + # on channel between i and 0. + def previousTokenOnChannel(self, i:int, channel:int): + while i>=0 and self.tokens[i].channel!=channel: + i -= 1 + return i + + # Collect all tokens on specified channel to the right of + # the current token up until we see a token on DEFAULT_TOKEN_CHANNEL or + # EOF. If channel is -1, find any non default channel token. + def getHiddenTokensToRight(self, tokenIndex:int, channel:int=-1): + self.lazyInit() + if tokenIndex<0 or tokenIndex>=len(self.tokens): + raise Exception(str(tokenIndex) + " not in 0.." + str(len(self.tokens)-1)) + from antlr4.Lexer import Lexer + nextOnChannel = self.nextTokenOnChannel(tokenIndex + 1, Lexer.DEFAULT_TOKEN_CHANNEL) + from_ = tokenIndex+1 + # if none onchannel to right, nextOnChannel=-1 so set to = last token + to = (len(self.tokens)-1) if nextOnChannel==-1 else nextOnChannel + return self.filterForChannel(from_, to, channel) + + + # Collect all tokens on specified channel to the left of + # the current token up until we see a token on DEFAULT_TOKEN_CHANNEL. + # If channel is -1, find any non default channel token. + def getHiddenTokensToLeft(self, tokenIndex:int, channel:int=-1): + self.lazyInit() + if tokenIndex<0 or tokenIndex>=len(self.tokens): + raise Exception(str(tokenIndex) + " not in 0.." + str(len(self.tokens)-1)) + from antlr4.Lexer import Lexer + prevOnChannel = self.previousTokenOnChannel(tokenIndex - 1, Lexer.DEFAULT_TOKEN_CHANNEL) + if prevOnChannel == tokenIndex - 1: + return None + # if none on channel to left, prevOnChannel=-1 then from=0 + from_ = prevOnChannel+1 + to = tokenIndex-1 + return self.filterForChannel(from_, to, channel) + + + def filterForChannel(self, left:int, right:int, channel:int): + hidden = [] + for i in range(left, right+1): + t = self.tokens[i] + if channel==-1: + from antlr4.Lexer import Lexer + if t.channel!= Lexer.DEFAULT_TOKEN_CHANNEL: + hidden.append(t) + elif t.channel==channel: + hidden.append(t) + if len(hidden)==0: + return None + return hidden + + def getSourceName(self): + return self.tokenSource.getSourceName() + + # Get the text of all tokens in this buffer.#/ + def getText(self, start:int=None, stop:int=None): + self.lazyInit() + self.fill() + if isinstance(start, Token): + start = start.tokenIndex + elif start is None: + start = 0 + if isinstance(stop, Token): + stop = stop.tokenIndex + elif stop is None or stop >= len(self.tokens): + stop = len(self.tokens) - 1 + if start < 0 or stop < 0 or stop < start: + return "" + with StringIO() as buf: + for i in range(start, stop+1): + t = self.tokens[i] + if t.type==Token.EOF: + break + buf.write(t.text) + return buf.getvalue() + + + # Get all tokens from lexer until EOF#/ + def fill(self): + self.lazyInit() + while self.fetch(1000)==1000: + pass diff --git a/lib/python3.12/site-packages/antlr4/CommonTokenFactory.py b/lib/python3.12/site-packages/antlr4/CommonTokenFactory.py new file mode 100644 index 0000000000000000000000000000000000000000..0d09ad78598de14f7a32d813c667e877ef1c0f6c --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/CommonTokenFactory.py @@ -0,0 +1,61 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# This default implementation of {@link TokenFactory} creates +# {@link CommonToken} objects. +# +from antlr4.Token import CommonToken + +class TokenFactory(object): + + pass + +class CommonTokenFactory(TokenFactory): + __slots__ = 'copyText' + + # + # The default {@link CommonTokenFactory} instance. + # + #

      + # This token factory does not explicitly copy token text when constructing + # tokens.

      + # + DEFAULT = None + + def __init__(self, copyText:bool=False): + # Indicates whether {@link CommonToken#setText} should be called after + # constructing tokens to explicitly set the text. This is useful for cases + # where the input stream might not be able to provide arbitrary substrings + # of text from the input after the lexer creates a token (e.g. the + # implementation of {@link CharStream#getText} in + # {@link UnbufferedCharStream} throws an + # {@link UnsupportedOperationException}). Explicitly setting the token text + # allows {@link Token#getText} to be called at any time regardless of the + # input stream implementation. + # + #

      + # The default value is {@code false} to avoid the performance and memory + # overhead of copying text for every token unless explicitly requested.

      + # + self.copyText = copyText + + def create(self, source, type:int, text:str, channel:int, start:int, stop:int, line:int, column:int): + t = CommonToken(source, type, channel, start, stop) + t.line = line + t.column = column + if text is not None: + t.text = text + elif self.copyText and source[1] is not None: + t.text = source[1].getText(start,stop) + return t + + def createThin(self, type:int, text:str): + t = CommonToken(type=type) + t.text = text + return t + +CommonTokenFactory.DEFAULT = CommonTokenFactory() diff --git a/lib/python3.12/site-packages/antlr4/CommonTokenStream.py b/lib/python3.12/site-packages/antlr4/CommonTokenStream.py new file mode 100644 index 0000000000000000000000000000000000000000..dd0a984e33fe42b3499844705b698953e7f04af9 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/CommonTokenStream.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# +# This class extends {@link BufferedTokenStream} with functionality to filter +# token streams to tokens on a particular channel (tokens where +# {@link Token#getChannel} returns a particular value). +# +#

      +# This token stream provides access to all tokens by index or when calling +# methods like {@link #getText}. The channel filtering is only used for code +# accessing tokens via the lookahead methods {@link #LA}, {@link #LT}, and +# {@link #LB}.

      +# +#

      +# By default, tokens are placed on the default channel +# ({@link Token#DEFAULT_CHANNEL}), but may be reassigned by using the +# {@code ->channel(HIDDEN)} lexer command, or by using an embedded action to +# call {@link Lexer#setChannel}. +#

      +# +#

      +# Note: lexer rules which use the {@code ->skip} lexer command or call +# {@link Lexer#skip} do not produce tokens at all, so input text matched by +# such a rule will not be available as part of the token stream, regardless of +# channel.

      +#/ + +from antlr4.BufferedTokenStream import BufferedTokenStream +from antlr4.Lexer import Lexer +from antlr4.Token import Token + + +class CommonTokenStream(BufferedTokenStream): + __slots__ = 'channel' + + def __init__(self, lexer:Lexer, channel:int=Token.DEFAULT_CHANNEL): + super().__init__(lexer) + self.channel = channel + + def adjustSeekIndex(self, i:int): + return self.nextTokenOnChannel(i, self.channel) + + def LB(self, k:int): + if k==0 or (self.index-k)<0: + return None + i = self.index + n = 1 + # find k good tokens looking backwards + while n <= k: + # skip off-channel tokens + i = self.previousTokenOnChannel(i - 1, self.channel) + n += 1 + if i < 0: + return None + return self.tokens[i] + + def LT(self, k:int): + self.lazyInit() + if k == 0: + return None + if k < 0: + return self.LB(-k) + i = self.index + n = 1 # we know tokens[pos] is a good one + # find k good tokens + while n < k: + # skip off-channel tokens, but make sure to not look past EOF + if self.sync(i + 1): + i = self.nextTokenOnChannel(i + 1, self.channel) + n += 1 + return self.tokens[i] + + # Count EOF just once.#/ + def getNumberOfOnChannelTokens(self): + n = 0 + self.fill() + for i in range(0, len(self.tokens)): + t = self.tokens[i] + if t.channel==self.channel: + n += 1 + if t.type==Token.EOF: + break + return n diff --git a/lib/python3.12/site-packages/antlr4/FileStream.py b/lib/python3.12/site-packages/antlr4/FileStream.py new file mode 100644 index 0000000000000000000000000000000000000000..1c6ce9fbc02a6f42965f44d5df0a55787626d7eb --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/FileStream.py @@ -0,0 +1,27 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# This is an InputStream that is loaded from a file all at once +# when you construct the object. +# + +import codecs +from antlr4.InputStream import InputStream + + +class FileStream(InputStream): + __slots__ = 'fileName' + + def __init__(self, fileName:str, encoding:str='ascii', errors:str='strict'): + super().__init__(self.readDataFrom(fileName, encoding, errors)) + self.fileName = fileName + + def readDataFrom(self, fileName:str, encoding:str, errors:str='strict'): + # read binary to avoid line ending conversion + with open(fileName, 'rb') as file: + bytes = file.read() + return codecs.decode(bytes, encoding, errors) diff --git a/lib/python3.12/site-packages/antlr4/InputStream.py b/lib/python3.12/site-packages/antlr4/InputStream.py new file mode 100644 index 0000000000000000000000000000000000000000..5d077d13576d2fd5f91153608f91e907092b87d5 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/InputStream.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + + +# +# Vacuum all input from a string and then treat it like a buffer. +# +from antlr4.Token import Token + + +class InputStream (object): + __slots__ = ('name', 'strdata', '_index', 'data', '_size') + + def __init__(self, data: str): + self.name = "" + self.strdata = data + self._loadString() + + def _loadString(self): + self._index = 0 + self.data = [ord(c) for c in self.strdata] + self._size = len(self.data) + + @property + def index(self): + return self._index + + @property + def size(self): + return self._size + + # Reset the stream so that it's in the same state it was + # when the object was created *except* the data array is not + # touched. + # + def reset(self): + self._index = 0 + + def consume(self): + if self._index >= self._size: + assert self.LA(1) == Token.EOF + raise Exception("cannot consume EOF") + self._index += 1 + + def LA(self, offset: int): + if offset==0: + return 0 # undefined + if offset<0: + offset += 1 # e.g., translate LA(-1) to use offset=0 + pos = self._index + offset - 1 + if pos < 0 or pos >= self._size: # invalid + return Token.EOF + return self.data[pos] + + def LT(self, offset: int): + return self.LA(offset) + + # mark/release do nothing; we have entire buffer + def mark(self): + return -1 + + def release(self, marker: int): + pass + + # consume() ahead until p==_index; can't just set p=_index as we must + # update line and column. If we seek backwards, just set p + # + def seek(self, _index: int): + if _index<=self._index: + self._index = _index # just jump; don't update stream state (line, ...) + return + # seek forward + self._index = min(_index, self._size) + + def getText(self, start :int, stop: int): + if stop >= self._size: + stop = self._size-1 + if start >= self._size: + return "" + else: + return self.strdata[start:stop+1] + + def __str__(self): + return self.strdata diff --git a/lib/python3.12/site-packages/antlr4/IntervalSet.py b/lib/python3.12/site-packages/antlr4/IntervalSet.py new file mode 100644 index 0000000000000000000000000000000000000000..fda8e6da3f69ba12147fdcb61268f7000d55d635 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/IntervalSet.py @@ -0,0 +1,180 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +from io import StringIO +from antlr4.Token import Token + +# need forward declarations +IntervalSet = None + +class IntervalSet(object): + __slots__ = ('intervals', 'readonly') + + def __init__(self): + self.intervals = None + self.readonly = False + + def __iter__(self): + if self.intervals is not None: + for i in self.intervals: + for c in i: + yield c + + def __getitem__(self, item): + i = 0 + for k in self: + if i==item: + return k + else: + i += 1 + return Token.INVALID_TYPE + + def addOne(self, v:int): + self.addRange(range(v, v+1)) + + def addRange(self, v:range): + if self.intervals is None: + self.intervals = list() + self.intervals.append(v) + else: + # find insert pos + k = 0 + for i in self.intervals: + # distinct range -> insert + if v.stop adjust + elif v.stop==i.start: + self.intervals[k] = range(v.start, i.stop) + return + # overlapping range -> adjust and reduce + elif v.start<=i.stop: + self.intervals[k] = range(min(i.start,v.start), max(i.stop,v.stop)) + self.reduce(k) + return + k += 1 + # greater than any existing + self.intervals.append(v) + + def addSet(self, other:IntervalSet): + if other.intervals is not None: + for i in other.intervals: + self.addRange(i) + return self + + def reduce(self, k:int): + # only need to reduce if k is not the last + if k= r.stop: + self.intervals.pop(k+1) + self.reduce(k) + elif l.stop >= r.start: + self.intervals[k] = range(l.start, r.stop) + self.intervals.pop(k+1) + + def complement(self, start, stop): + result = IntervalSet() + result.addRange(range(start,stop+1)) + for i in self.intervals: + result.removeRange(i) + return result + + def __contains__(self, item): + if self.intervals is None: + return False + else: + return any(item in i for i in self.intervals) + + def __len__(self): + return sum(len(i) for i in self.intervals) + + def removeRange(self, v): + if v.start==v.stop-1: + self.removeOne(v.start) + elif self.intervals is not None: + k = 0 + for i in self.intervals: + # intervals are ordered + if v.stop<=i.start: + return + # check for including range, split it + elif v.start>i.start and v.stop=i.stop: + self.intervals.pop(k) + k -= 1 # need another pass + # check for lower boundary + elif v.start1: + buf.write("{") + first = True + for i in self.intervals: + for j in i: + if not first: + buf.write(", ") + buf.write(self.elementName(literalNames, symbolicNames, j)) + first = False + if len(self)>1: + buf.write("}") + return buf.getvalue() + + def elementName(self, literalNames:list, symbolicNames:list, a:int): + if a==Token.EOF: + return "" + elif a==Token.EPSILON: + return "" + else: + if a" diff --git a/lib/python3.12/site-packages/antlr4/LL1Analyzer.py b/lib/python3.12/site-packages/antlr4/LL1Analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..ac149fa2f1d23903ef60593521291c4d9f1945a4 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/LL1Analyzer.py @@ -0,0 +1,173 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ +from antlr4.IntervalSet import IntervalSet +from antlr4.Token import Token +from antlr4.PredictionContext import PredictionContext, SingletonPredictionContext, PredictionContextFromRuleContext +from antlr4.RuleContext import RuleContext +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNConfig import ATNConfig +from antlr4.atn.ATNState import ATNState, RuleStopState +from antlr4.atn.Transition import WildcardTransition, NotSetTransition, AbstractPredicateTransition, RuleTransition + + +class LL1Analyzer (object): + __slots__ = 'atn' + + #* Special value added to the lookahead sets to indicate that we hit + # a predicate during analysis if {@code seeThruPreds==false}. + #/ + HIT_PRED = Token.INVALID_TYPE + + def __init__(self, atn:ATN): + self.atn = atn + + #* + # Calculates the SLL(1) expected lookahead set for each outgoing transition + # of an {@link ATNState}. The returned array has one element for each + # outgoing transition in {@code s}. If the closure from transition + # i leads to a semantic predicate before matching a symbol, the + # element at index i of the result will be {@code null}. + # + # @param s the ATN state + # @return the expected symbols for each outgoing transition of {@code s}. + #/ + def getDecisionLookahead(self, s:ATNState): + if s is None: + return None + + count = len(s.transitions) + look = [] * count + for alt in range(0, count): + look[alt] = set() + lookBusy = set() + seeThruPreds = False # fail to get lookahead upon pred + self._LOOK(s.transition(alt).target, None, PredictionContext.EMPTY, + look[alt], lookBusy, set(), seeThruPreds, False) + # Wipe out lookahead for this alternative if we found nothing + # or we had a predicate when we !seeThruPreds + if len(look[alt])==0 or self.HIT_PRED in look[alt]: + look[alt] = None + return look + + #* + # Compute set of tokens that can follow {@code s} in the ATN in the + # specified {@code ctx}. + # + #

      If {@code ctx} is {@code null} and the end of the rule containing + # {@code s} is reached, {@link Token#EPSILON} is added to the result set. + # If {@code ctx} is not {@code null} and the end of the outermost rule is + # reached, {@link Token#EOF} is added to the result set.

      + # + # @param s the ATN state + # @param stopState the ATN state to stop at. This can be a + # {@link BlockEndState} to detect epsilon paths through a closure. + # @param ctx the complete parser context, or {@code null} if the context + # should be ignored + # + # @return The set of tokens that can follow {@code s} in the ATN in the + # specified {@code ctx}. + #/ + def LOOK(self, s:ATNState, stopState:ATNState=None, ctx:RuleContext=None): + r = IntervalSet() + seeThruPreds = True # ignore preds; get all lookahead + lookContext = PredictionContextFromRuleContext(s.atn, ctx) if ctx is not None else None + self._LOOK(s, stopState, lookContext, r, set(), set(), seeThruPreds, True) + return r + + #* + # Compute set of tokens that can follow {@code s} in the ATN in the + # specified {@code ctx}. + # + #

      If {@code ctx} is {@code null} and {@code stopState} or the end of the + # rule containing {@code s} is reached, {@link Token#EPSILON} is added to + # the result set. If {@code ctx} is not {@code null} and {@code addEOF} is + # {@code true} and {@code stopState} or the end of the outermost rule is + # reached, {@link Token#EOF} is added to the result set.

      + # + # @param s the ATN state. + # @param stopState the ATN state to stop at. This can be a + # {@link BlockEndState} to detect epsilon paths through a closure. + # @param ctx The outer context, or {@code null} if the outer context should + # not be used. + # @param look The result lookahead set. + # @param lookBusy A set used for preventing epsilon closures in the ATN + # from causing a stack overflow. Outside code should pass + # {@code new HashSet} for this argument. + # @param calledRuleStack A set used for preventing left recursion in the + # ATN from causing a stack overflow. Outside code should pass + # {@code new BitSet()} for this argument. + # @param seeThruPreds {@code true} to true semantic predicates as + # implicitly {@code true} and "see through them", otherwise {@code false} + # to treat semantic predicates as opaque and add {@link #HIT_PRED} to the + # result if one is encountered. + # @param addEOF Add {@link Token#EOF} to the result if the end of the + # outermost context is reached. This parameter has no effect if {@code ctx} + # is {@code null}. + #/ + def _LOOK(self, s:ATNState, stopState:ATNState , ctx:PredictionContext, look:IntervalSet, lookBusy:set, + calledRuleStack:set, seeThruPreds:bool, addEOF:bool): + c = ATNConfig(s, 0, ctx) + + if c in lookBusy: + return + lookBusy.add(c) + + if s == stopState: + if ctx is None: + look.addOne(Token.EPSILON) + return + elif ctx.isEmpty() and addEOF: + look.addOne(Token.EOF) + return + + if isinstance(s, RuleStopState ): + if ctx is None: + look.addOne(Token.EPSILON) + return + elif ctx.isEmpty() and addEOF: + look.addOne(Token.EOF) + return + + if ctx != PredictionContext.EMPTY: + removed = s.ruleIndex in calledRuleStack + try: + calledRuleStack.discard(s.ruleIndex) + # run thru all possible stack tops in ctx + for i in range(0, len(ctx)): + returnState = self.atn.states[ctx.getReturnState(i)] + self._LOOK(returnState, stopState, ctx.getParent(i), look, lookBusy, calledRuleStack, seeThruPreds, addEOF) + finally: + if removed: + calledRuleStack.add(s.ruleIndex) + return + + for t in s.transitions: + if type(t) == RuleTransition: + if t.target.ruleIndex in calledRuleStack: + continue + + newContext = SingletonPredictionContext.create(ctx, t.followState.stateNumber) + + try: + calledRuleStack.add(t.target.ruleIndex) + self._LOOK(t.target, stopState, newContext, look, lookBusy, calledRuleStack, seeThruPreds, addEOF) + finally: + calledRuleStack.remove(t.target.ruleIndex) + elif isinstance(t, AbstractPredicateTransition ): + if seeThruPreds: + self._LOOK(t.target, stopState, ctx, look, lookBusy, calledRuleStack, seeThruPreds, addEOF) + else: + look.addOne(self.HIT_PRED) + elif t.isEpsilon: + self._LOOK(t.target, stopState, ctx, look, lookBusy, calledRuleStack, seeThruPreds, addEOF) + elif type(t) == WildcardTransition: + look.addRange( range(Token.MIN_USER_TOKEN_TYPE, self.atn.maxTokenType + 1) ) + else: + set_ = t.label + if set_ is not None: + if isinstance(t, NotSetTransition): + set_ = set_.complement(Token.MIN_USER_TOKEN_TYPE, self.atn.maxTokenType) + look.addSet(set_) diff --git a/lib/python3.12/site-packages/antlr4/Lexer.py b/lib/python3.12/site-packages/antlr4/Lexer.py new file mode 100644 index 0000000000000000000000000000000000000000..82accadcbca680b48f4c0206731259d32adc299e --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/Lexer.py @@ -0,0 +1,329 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# A lexer is recognizer that draws input symbols from a character stream. +# lexer grammars result in a subclass of self object. A Lexer object +# uses simplified match() and error recovery mechanisms in the interest +# of speed. +#/ +from io import StringIO + +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO +from antlr4.CommonTokenFactory import CommonTokenFactory +from antlr4.atn.LexerATNSimulator import LexerATNSimulator +from antlr4.InputStream import InputStream +from antlr4.Recognizer import Recognizer +from antlr4.Token import Token +from antlr4.error.Errors import IllegalStateException, LexerNoViableAltException, RecognitionException + +class TokenSource(object): + + pass + + +class Lexer(Recognizer, TokenSource): + __slots__ = ( + '_input', '_output', '_factory', '_tokenFactorySourcePair', '_token', + '_tokenStartCharIndex', '_tokenStartLine', '_tokenStartColumn', + '_hitEOF', '_channel', '_type', '_modeStack', '_mode', '_text' + ) + + DEFAULT_MODE = 0 + MORE = -2 + SKIP = -3 + + DEFAULT_TOKEN_CHANNEL = Token.DEFAULT_CHANNEL + HIDDEN = Token.HIDDEN_CHANNEL + MIN_CHAR_VALUE = 0x0000 + MAX_CHAR_VALUE = 0x10FFFF + + def __init__(self, input:InputStream, output:TextIO = sys.stdout): + super().__init__() + self._input = input + self._output = output + self._factory = CommonTokenFactory.DEFAULT + self._tokenFactorySourcePair = (self, input) + + self._interp = None # child classes must populate this + + # The goal of all lexer rules/methods is to create a token object. + # self is an instance variable as multiple rules may collaborate to + # create a single token. nextToken will return self object after + # matching lexer rule(s). If you subclass to allow multiple token + # emissions, then set self to the last token to be matched or + # something nonnull so that the auto token emit mechanism will not + # emit another token. + self._token = None + + # What character index in the stream did the current token start at? + # Needed, for example, to get the text for current token. Set at + # the start of nextToken. + self._tokenStartCharIndex = -1 + + # The line on which the first character of the token resides#/ + self._tokenStartLine = -1 + + # The character position of first character within the line#/ + self._tokenStartColumn = -1 + + # Once we see EOF on char stream, next token will be EOF. + # If you have DONE : EOF ; then you see DONE EOF. + self._hitEOF = False + + # The channel number for the current token#/ + self._channel = Token.DEFAULT_CHANNEL + + # The token type for the current token#/ + self._type = Token.INVALID_TYPE + + self._modeStack = [] + self._mode = self.DEFAULT_MODE + + # You can set the text for the current token to override what is in + # the input char buffer. Use setText() or can set self instance var. + #/ + self._text = None + + + def reset(self): + # wack Lexer state variables + if self._input is not None: + self._input.seek(0) # rewind the input + self._token = None + self._type = Token.INVALID_TYPE + self._channel = Token.DEFAULT_CHANNEL + self._tokenStartCharIndex = -1 + self._tokenStartColumn = -1 + self._tokenStartLine = -1 + self._text = None + + self._hitEOF = False + self._mode = Lexer.DEFAULT_MODE + self._modeStack = [] + + self._interp.reset() + + # Return a token from self source; i.e., match a token on the char + # stream. + def nextToken(self): + if self._input is None: + raise IllegalStateException("nextToken requires a non-null input stream.") + + # Mark start location in char stream so unbuffered streams are + # guaranteed at least have text of current token + tokenStartMarker = self._input.mark() + try: + while True: + if self._hitEOF: + self.emitEOF() + return self._token + self._token = None + self._channel = Token.DEFAULT_CHANNEL + self._tokenStartCharIndex = self._input.index + self._tokenStartColumn = self._interp.column + self._tokenStartLine = self._interp.line + self._text = None + continueOuter = False + while True: + self._type = Token.INVALID_TYPE + ttype = self.SKIP + try: + ttype = self._interp.match(self._input, self._mode) + except LexerNoViableAltException as e: + self.notifyListeners(e) # report error + self.recover(e) + if self._input.LA(1)==Token.EOF: + self._hitEOF = True + if self._type == Token.INVALID_TYPE: + self._type = ttype + if self._type == self.SKIP: + continueOuter = True + break + if self._type!=self.MORE: + break + if continueOuter: + continue + if self._token is None: + self.emit() + return self._token + finally: + # make sure we release marker after match or + # unbuffered char stream will keep buffering + self._input.release(tokenStartMarker) + + # Instruct the lexer to skip creating a token for current lexer rule + # and look for another token. nextToken() knows to keep looking when + # a lexer rule finishes with token set to SKIP_TOKEN. Recall that + # if token==null at end of any token rule, it creates one for you + # and emits it. + #/ + def skip(self): + self._type = self.SKIP + + def more(self): + self._type = self.MORE + + def mode(self, m:int): + self._mode = m + + def pushMode(self, m:int): + if self._interp.debug: + print("pushMode " + str(m), file=self._output) + self._modeStack.append(self._mode) + self.mode(m) + + def popMode(self): + if len(self._modeStack)==0: + raise Exception("Empty Stack") + if self._interp.debug: + print("popMode back to "+ self._modeStack[:-1], file=self._output) + self.mode( self._modeStack.pop() ) + return self._mode + + # Set the char stream and reset the lexer#/ + @property + def inputStream(self): + return self._input + + @inputStream.setter + def inputStream(self, input:InputStream): + self._input = None + self._tokenFactorySourcePair = (self, self._input) + self.reset() + self._input = input + self._tokenFactorySourcePair = (self, self._input) + + @property + def sourceName(self): + return self._input.sourceName + + # By default does not support multiple emits per nextToken invocation + # for efficiency reasons. Subclass and override self method, nextToken, + # and getToken (to push tokens into a list and pull from that list + # rather than a single variable as self implementation does). + #/ + def emitToken(self, token:Token): + self._token = token + + # The standard method called to automatically emit a token at the + # outermost lexical rule. The token object should point into the + # char buffer start..stop. If there is a text override in 'text', + # use that to set the token's text. Override self method to emit + # custom Token objects or provide a new factory. + #/ + def emit(self): + t = self._factory.create(self._tokenFactorySourcePair, self._type, self._text, self._channel, self._tokenStartCharIndex, + self.getCharIndex()-1, self._tokenStartLine, self._tokenStartColumn) + self.emitToken(t) + return t + + def emitEOF(self): + cpos = self.column + lpos = self.line + eof = self._factory.create(self._tokenFactorySourcePair, Token.EOF, None, Token.DEFAULT_CHANNEL, self._input.index, + self._input.index-1, lpos, cpos) + self.emitToken(eof) + return eof + + @property + def type(self): + return self._type + + @type.setter + def type(self, type:int): + self._type = type + + @property + def line(self): + return self._interp.line + + @line.setter + def line(self, line:int): + self._interp.line = line + + @property + def column(self): + return self._interp.column + + @column.setter + def column(self, column:int): + self._interp.column = column + + # What is the index of the current character of lookahead?#/ + def getCharIndex(self): + return self._input.index + + # Return the text matched so far for the current token or any + # text override. + @property + def text(self): + if self._text is not None: + return self._text + else: + return self._interp.getText(self._input) + + # Set the complete text of self token; it wipes any previous + # changes to the text. + @text.setter + def text(self, txt:str): + self._text = txt + + # Return a list of all Token objects in input char stream. + # Forces load of all tokens. Does not include EOF token. + #/ + def getAllTokens(self): + tokens = [] + t = self.nextToken() + while t.type!=Token.EOF: + tokens.append(t) + t = self.nextToken() + return tokens + + def notifyListeners(self, e:LexerNoViableAltException): + start = self._tokenStartCharIndex + stop = self._input.index + text = self._input.getText(start, stop) + msg = "token recognition error at: '" + self.getErrorDisplay(text) + "'" + listener = self.getErrorListenerDispatch() + listener.syntaxError(self, None, self._tokenStartLine, self._tokenStartColumn, msg, e) + + def getErrorDisplay(self, s:str): + with StringIO() as buf: + for c in s: + buf.write(self.getErrorDisplayForChar(c)) + return buf.getvalue() + + def getErrorDisplayForChar(self, c:str): + if ord(c[0])==Token.EOF: + return "" + elif c=='\n': + return "\\n" + elif c=='\t': + return "\\t" + elif c=='\r': + return "\\r" + else: + return c + + def getCharErrorDisplay(self, c:str): + return "'" + self.getErrorDisplayForChar(c) + "'" + + # Lexers can normally match any char in it's vocabulary after matching + # a token, so do the easy thing and just kill a character and hope + # it all works out. You can instead use the rule invocation stack + # to do sophisticated error recovery if you are in a fragment rule. + #/ + def recover(self, re:RecognitionException): + if self._input.LA(1) != Token.EOF: + if isinstance(re, LexerNoViableAltException): + # skip a char and try again + self._interp.consume(self._input) + else: + # TODO: Do we lose character or line position information? + self._input.consume() diff --git a/lib/python3.12/site-packages/antlr4/ListTokenSource.py b/lib/python3.12/site-packages/antlr4/ListTokenSource.py new file mode 100644 index 0000000000000000000000000000000000000000..40bcaf94a243e7bd7d2c9b9bfa7f522706a46788 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/ListTokenSource.py @@ -0,0 +1,144 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# Provides an implementation of {@link TokenSource} as a wrapper around a list +# of {@link Token} objects. +# +#

      If the final token in the list is an {@link Token#EOF} token, it will be used +# as the EOF token for every call to {@link #nextToken} after the end of the +# list is reached. Otherwise, an EOF token will be created.

      +# +from antlr4.CommonTokenFactory import CommonTokenFactory +from antlr4.Lexer import TokenSource +from antlr4.Token import Token + + +class ListTokenSource(TokenSource): + __slots__ = ('tokens', 'sourceName', 'pos', 'eofToken', '_factory') + + # Constructs a new {@link ListTokenSource} instance from the specified + # collection of {@link Token} objects and source name. + # + # @param tokens The collection of {@link Token} objects to provide as a + # {@link TokenSource}. + # @param sourceName The name of the {@link TokenSource}. If this value is + # {@code null}, {@link #getSourceName} will attempt to infer the name from + # the next {@link Token} (or the previous token if the end of the input has + # been reached). + # + # @exception NullPointerException if {@code tokens} is {@code null} + # + def __init__(self, tokens:list, sourceName:str=None): + if tokens is None: + raise ReferenceError("tokens cannot be null") + self.tokens = tokens + self.sourceName = sourceName + # The index into {@link #tokens} of token to return by the next call to + # {@link #nextToken}. The end of the input is indicated by this value + # being greater than or equal to the number of items in {@link #tokens}. + self.pos = 0 + # This field caches the EOF token for the token source. + self.eofToken = None + # This is the backing field for {@link #getTokenFactory} and + self._factory = CommonTokenFactory.DEFAULT + + + # + # {@inheritDoc} + # + @property + def column(self): + if self.pos < len(self.tokens): + return self.tokens[self.pos].column + elif self.eofToken is not None: + return self.eofToken.column + elif len(self.tokens) > 0: + # have to calculate the result from the line/column of the previous + # token, along with the text of the token. + lastToken = self.tokens[len(self.tokens) - 1] + tokenText = lastToken.text + if tokenText is not None: + lastNewLine = tokenText.rfind('\n') + if lastNewLine >= 0: + return len(tokenText) - lastNewLine - 1 + return lastToken.column + lastToken.stop - lastToken.start + 1 + + # only reach this if tokens is empty, meaning EOF occurs at the first + # position in the input + return 0 + + # + # {@inheritDoc} + # + def nextToken(self): + if self.pos >= len(self.tokens): + if self.eofToken is None: + start = -1 + if len(self.tokens) > 0: + previousStop = self.tokens[len(self.tokens) - 1].stop + if previousStop != -1: + start = previousStop + 1 + stop = max(-1, start - 1) + self.eofToken = self._factory.create((self, self.getInputStream()), + Token.EOF, "EOF", Token.DEFAULT_CHANNEL, start, stop, self.line, self.column) + return self.eofToken + t = self.tokens[self.pos] + if self.pos == len(self.tokens) - 1 and t.type == Token.EOF: + self.eofToken = t + self.pos += 1 + return t + + # + # {@inheritDoc} + # + @property + def line(self): + if self.pos < len(self.tokens): + return self.tokens[self.pos].line + elif self.eofToken is not None: + return self.eofToken.line + elif len(self.tokens) > 0: + # have to calculate the result from the line/column of the previous + # token, along with the text of the token. + lastToken = self.tokens[len(self.tokens) - 1] + line = lastToken.line + tokenText = lastToken.text + if tokenText is not None: + line += tokenText.count('\n') + + # if no text is available, assume the token did not contain any newline characters. + return line + + # only reach this if tokens is empty, meaning EOF occurs at the first + # position in the input + return 1 + + # + # {@inheritDoc} + # + def getInputStream(self): + if self.pos < len(self.tokens): + return self.tokens[self.pos].getInputStream() + elif self.eofToken is not None: + return self.eofToken.getInputStream() + elif len(self.tokens) > 0: + return self.tokens[len(self.tokens) - 1].getInputStream() + else: + # no input stream information is available + return None + + # + # {@inheritDoc} + # + def getSourceName(self): + if self.sourceName is not None: + return self.sourceName + inputStream = self.getInputStream() + if inputStream is not None: + return inputStream.getSourceName() + else: + return "List" diff --git a/lib/python3.12/site-packages/antlr4/Parser.py b/lib/python3.12/site-packages/antlr4/Parser.py new file mode 100644 index 0000000000000000000000000000000000000000..ae7f48cce07bebc77f03a577278b95a45832fda0 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/Parser.py @@ -0,0 +1,580 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +import sys +if sys.version_info[1] > 5: + from typing import TextIO +else: + from typing.io import TextIO +from antlr4.BufferedTokenStream import TokenStream +from antlr4.CommonTokenFactory import TokenFactory +from antlr4.error.ErrorStrategy import DefaultErrorStrategy +from antlr4.InputStream import InputStream +from antlr4.Recognizer import Recognizer +from antlr4.RuleContext import RuleContext +from antlr4.ParserRuleContext import ParserRuleContext +from antlr4.Token import Token +from antlr4.Lexer import Lexer +from antlr4.atn.ATNDeserializer import ATNDeserializer +from antlr4.atn.ATNDeserializationOptions import ATNDeserializationOptions +from antlr4.error.Errors import UnsupportedOperationException, RecognitionException +from antlr4.tree.ParseTreePatternMatcher import ParseTreePatternMatcher +from antlr4.tree.Tree import ParseTreeListener, TerminalNode, ErrorNode + +class TraceListener(ParseTreeListener): + __slots__ = '_parser' + + def __init__(self, parser): + self._parser = parser + + def enterEveryRule(self, ctx): + print("enter " + self._parser.ruleNames[ctx.getRuleIndex()] + ", LT(1)=" + self._parser._input.LT(1).text, file=self._parser._output) + + def visitTerminal(self, node): + + print("consume " + str(node.symbol) + " rule " + self._parser.ruleNames[self._parser._ctx.getRuleIndex()], file=self._parser._output) + + def visitErrorNode(self, node): + pass + + + def exitEveryRule(self, ctx): + print("exit " + self._parser.ruleNames[ctx.getRuleIndex()] + ", LT(1)=" + self._parser._input.LT(1).text, file=self._parser._output) + + +# self is all the parsing support code essentially; most of it is error recovery stuff.# +class Parser (Recognizer): + __slots__ = ( + '_input', '_output', '_errHandler', '_precedenceStack', '_ctx', + 'buildParseTrees', '_tracer', '_parseListeners', '_syntaxErrors' + + ) + # self field maps from the serialized ATN string to the deserialized {@link ATN} with + # bypass alternatives. + # + # @see ATNDeserializationOptions#isGenerateRuleBypassTransitions() + # + bypassAltsAtnCache = dict() + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__() + # The input stream. + self._input = None + self._output = output + # The error handling strategy for the parser. The default value is a new + # instance of {@link DefaultErrorStrategy}. + self._errHandler = DefaultErrorStrategy() + self._precedenceStack = list() + self._precedenceStack.append(0) + # The {@link ParserRuleContext} object for the currently executing rule. + # self is always non-null during the parsing process. + self._ctx = None + # Specifies whether or not the parser should construct a parse tree during + # the parsing process. The default value is {@code true}. + self.buildParseTrees = True + # When {@link #setTrace}{@code (true)} is called, a reference to the + # {@link TraceListener} is stored here so it can be easily removed in a + # later call to {@link #setTrace}{@code (false)}. The listener itself is + # implemented as a parser listener so self field is not directly used by + # other parser methods. + self._tracer = None + # The list of {@link ParseTreeListener} listeners registered to receive + # events during the parse. + self._parseListeners = None + # The number of syntax errors reported during parsing. self value is + # incremented each time {@link #notifyErrorListeners} is called. + self._syntaxErrors = 0 + self.setInputStream(input) + + # reset the parser's state# + def reset(self): + if self._input is not None: + self._input.seek(0) + self._errHandler.reset(self) + self._ctx = None + self._syntaxErrors = 0 + self.setTrace(False) + self._precedenceStack = list() + self._precedenceStack.append(0) + if self._interp is not None: + self._interp.reset() + + # Match current input symbol against {@code ttype}. If the symbol type + # matches, {@link ANTLRErrorStrategy#reportMatch} and {@link #consume} are + # called to complete the match process. + # + #

      If the symbol type does not match, + # {@link ANTLRErrorStrategy#recoverInline} is called on the current error + # strategy to attempt recovery. If {@link #getBuildParseTree} is + # {@code true} and the token index of the symbol returned by + # {@link ANTLRErrorStrategy#recoverInline} is -1, the symbol is added to + # the parse tree by calling {@link ParserRuleContext#addErrorNode}.

      + # + # @param ttype the token type to match + # @return the matched symbol + # @throws RecognitionException if the current input symbol did not match + # {@code ttype} and the error strategy could not recover from the + # mismatched symbol + + def match(self, ttype:int): + t = self.getCurrentToken() + if t.type==ttype: + self._errHandler.reportMatch(self) + self.consume() + else: + t = self._errHandler.recoverInline(self) + if self.buildParseTrees and t.tokenIndex==-1: + # we must have conjured up a new token during single token insertion + # if it's not the current symbol + self._ctx.addErrorNode(t) + return t + + # Match current input symbol as a wildcard. If the symbol type matches + # (i.e. has a value greater than 0), {@link ANTLRErrorStrategy#reportMatch} + # and {@link #consume} are called to complete the match process. + # + #

      If the symbol type does not match, + # {@link ANTLRErrorStrategy#recoverInline} is called on the current error + # strategy to attempt recovery. If {@link #getBuildParseTree} is + # {@code true} and the token index of the symbol returned by + # {@link ANTLRErrorStrategy#recoverInline} is -1, the symbol is added to + # the parse tree by calling {@link ParserRuleContext#addErrorNode}.

      + # + # @return the matched symbol + # @throws RecognitionException if the current input symbol did not match + # a wildcard and the error strategy could not recover from the mismatched + # symbol + + def matchWildcard(self): + t = self.getCurrentToken() + if t.type > 0: + self._errHandler.reportMatch(self) + self.consume() + else: + t = self._errHandler.recoverInline(self) + if self.buildParseTrees and t.tokenIndex == -1: + # we must have conjured up a new token during single token insertion + # if it's not the current symbol + self._ctx.addErrorNode(t) + + return t + + def getParseListeners(self): + return list() if self._parseListeners is None else self._parseListeners + + # Registers {@code listener} to receive events during the parsing process. + # + #

      To support output-preserving grammar transformations (including but not + # limited to left-recursion removal, automated left-factoring, and + # optimized code generation), calls to listener methods during the parse + # may differ substantially from calls made by + # {@link ParseTreeWalker#DEFAULT} used after the parse is complete. In + # particular, rule entry and exit events may occur in a different order + # during the parse than after the parser. In addition, calls to certain + # rule entry methods may be omitted.

      + # + #

      With the following specific exceptions, calls to listener events are + # deterministic, i.e. for identical input the calls to listener + # methods will be the same.

      + # + #
        + #
      • Alterations to the grammar used to generate code may change the + # behavior of the listener calls.
      • + #
      • Alterations to the command line options passed to ANTLR 4 when + # generating the parser may change the behavior of the listener calls.
      • + #
      • Changing the version of the ANTLR Tool used to generate the parser + # may change the behavior of the listener calls.
      • + #
      + # + # @param listener the listener to add + # + # @throws NullPointerException if {@code} listener is {@code null} + # + def addParseListener(self, listener:ParseTreeListener): + if listener is None: + raise ReferenceError("listener") + if self._parseListeners is None: + self._parseListeners = [] + self._parseListeners.append(listener) + + # + # Remove {@code listener} from the list of parse listeners. + # + #

      If {@code listener} is {@code null} or has not been added as a parse + # listener, self method does nothing.

      + # @param listener the listener to remove + # + def removeParseListener(self, listener:ParseTreeListener): + if self._parseListeners is not None: + self._parseListeners.remove(listener) + if len(self._parseListeners)==0: + self._parseListeners = None + + # Remove all parse listeners. + def removeParseListeners(self): + self._parseListeners = None + + # Notify any parse listeners of an enter rule event. + def triggerEnterRuleEvent(self): + if self._parseListeners is not None: + for listener in self._parseListeners: + listener.enterEveryRule(self._ctx) + self._ctx.enterRule(listener) + + # + # Notify any parse listeners of an exit rule event. + # + # @see #addParseListener + # + def triggerExitRuleEvent(self): + if self._parseListeners is not None: + # reverse order walk of listeners + for listener in reversed(self._parseListeners): + self._ctx.exitRule(listener) + listener.exitEveryRule(self._ctx) + + + # Gets the number of syntax errors reported during parsing. This value is + # incremented each time {@link #notifyErrorListeners} is called. + # + # @see #notifyErrorListeners + # + def getNumberOfSyntaxErrors(self): + return self._syntaxErrors + + def getTokenFactory(self): + return self._input.tokenSource._factory + + # Tell our token source and error strategy about a new way to create tokens.# + def setTokenFactory(self, factory:TokenFactory): + self._input.tokenSource._factory = factory + + # The ATN with bypass alternatives is expensive to create so we create it + # lazily. + # + # @throws UnsupportedOperationException if the current parser does not + # implement the {@link #getSerializedATN()} method. + # + def getATNWithBypassAlts(self): + serializedAtn = self.getSerializedATN() + if serializedAtn is None: + raise UnsupportedOperationException("The current parser does not support an ATN with bypass alternatives.") + result = self.bypassAltsAtnCache.get(serializedAtn, None) + if result is None: + deserializationOptions = ATNDeserializationOptions() + deserializationOptions.generateRuleBypassTransitions = True + result = ATNDeserializer(deserializationOptions).deserialize(serializedAtn) + self.bypassAltsAtnCache[serializedAtn] = result + return result + + # The preferred method of getting a tree pattern. For example, here's a + # sample use: + # + #
      +    # ParseTree t = parser.expr();
      +    # ParseTreePattern p = parser.compileParseTreePattern("<ID>+0", MyParser.RULE_expr);
      +    # ParseTreeMatch m = p.match(t);
      +    # String id = m.get("ID");
      +    # 
      + # + def compileParseTreePattern(self, pattern:str, patternRuleIndex:int, lexer:Lexer = None): + if lexer is None: + if self.getTokenStream() is not None: + tokenSource = self.getTokenStream().tokenSource + if isinstance( tokenSource, Lexer ): + lexer = tokenSource + if lexer is None: + raise UnsupportedOperationException("Parser can't discover a lexer to use") + + m = ParseTreePatternMatcher(lexer, self) + return m.compile(pattern, patternRuleIndex) + + + def getInputStream(self): + return self.getTokenStream() + + def setInputStream(self, input:InputStream): + self.setTokenStream(input) + + def getTokenStream(self): + return self._input + + # Set the token stream and reset the parser.# + def setTokenStream(self, input:TokenStream): + self._input = None + self.reset() + self._input = input + + # Match needs to return the current input symbol, which gets put + # into the label for the associated token ref; e.g., x=ID. + # + def getCurrentToken(self): + return self._input.LT(1) + + def notifyErrorListeners(self, msg:str, offendingToken:Token = None, e:RecognitionException = None): + if offendingToken is None: + offendingToken = self.getCurrentToken() + self._syntaxErrors += 1 + line = offendingToken.line + column = offendingToken.column + listener = self.getErrorListenerDispatch() + listener.syntaxError(self, offendingToken, line, column, msg, e) + + # + # Consume and return the {@linkplain #getCurrentToken current symbol}. + # + #

      E.g., given the following input with {@code A} being the current + # lookahead symbol, self function moves the cursor to {@code B} and returns + # {@code A}.

      + # + #
      +    #  A B
      +    #  ^
      +    # 
      + # + # If the parser is not in error recovery mode, the consumed symbol is added + # to the parse tree using {@link ParserRuleContext#addChild(Token)}, and + # {@link ParseTreeListener#visitTerminal} is called on any parse listeners. + # If the parser is in error recovery mode, the consumed symbol is + # added to the parse tree using + # {@link ParserRuleContext#addErrorNode(Token)}, and + # {@link ParseTreeListener#visitErrorNode} is called on any parse + # listeners. + # + def consume(self): + o = self.getCurrentToken() + if o.type != Token.EOF: + self.getInputStream().consume() + hasListener = self._parseListeners is not None and len(self._parseListeners)>0 + if self.buildParseTrees or hasListener: + if self._errHandler.inErrorRecoveryMode(self): + node = self._ctx.addErrorNode(o) + else: + node = self._ctx.addTokenNode(o) + if hasListener: + for listener in self._parseListeners: + if isinstance(node, ErrorNode): + listener.visitErrorNode(node) + elif isinstance(node, TerminalNode): + listener.visitTerminal(node) + return o + + def addContextToParseTree(self): + # add current context to parent if we have a parent + if self._ctx.parentCtx is not None: + self._ctx.parentCtx.addChild(self._ctx) + + # Always called by generated parsers upon entry to a rule. Access field + # {@link #_ctx} get the current context. + # + def enterRule(self, localctx:ParserRuleContext , state:int , ruleIndex:int): + self.state = state + self._ctx = localctx + self._ctx.start = self._input.LT(1) + if self.buildParseTrees: + self.addContextToParseTree() + if self._parseListeners is not None: + self.triggerEnterRuleEvent() + + def exitRule(self): + self._ctx.stop = self._input.LT(-1) + # trigger event on _ctx, before it reverts to parent + if self._parseListeners is not None: + self.triggerExitRuleEvent() + self.state = self._ctx.invokingState + self._ctx = self._ctx.parentCtx + + def enterOuterAlt(self, localctx:ParserRuleContext, altNum:int): + localctx.setAltNumber(altNum) + # if we have new localctx, make sure we replace existing ctx + # that is previous child of parse tree + if self.buildParseTrees and self._ctx != localctx: + if self._ctx.parentCtx is not None: + self._ctx.parentCtx.removeLastChild() + self._ctx.parentCtx.addChild(localctx) + self._ctx = localctx + + # Get the precedence level for the top-most precedence rule. + # + # @return The precedence level for the top-most precedence rule, or -1 if + # the parser context is not nested within a precedence rule. + # + def getPrecedence(self): + if len(self._precedenceStack)==0: + return -1 + else: + return self._precedenceStack[-1] + + def enterRecursionRule(self, localctx:ParserRuleContext, state:int, ruleIndex:int, precedence:int): + self.state = state + self._precedenceStack.append(precedence) + self._ctx = localctx + self._ctx.start = self._input.LT(1) + if self._parseListeners is not None: + self.triggerEnterRuleEvent() # simulates rule entry for left-recursive rules + + # + # Like {@link #enterRule} but for recursive rules. + # + def pushNewRecursionContext(self, localctx:ParserRuleContext, state:int, ruleIndex:int): + previous = self._ctx + previous.parentCtx = localctx + previous.invokingState = state + previous.stop = self._input.LT(-1) + + self._ctx = localctx + self._ctx.start = previous.start + if self.buildParseTrees: + self._ctx.addChild(previous) + + if self._parseListeners is not None: + self.triggerEnterRuleEvent() # simulates rule entry for left-recursive rules + + def unrollRecursionContexts(self, parentCtx:ParserRuleContext): + self._precedenceStack.pop() + self._ctx.stop = self._input.LT(-1) + retCtx = self._ctx # save current ctx (return value) + # unroll so _ctx is as it was before call to recursive method + if self._parseListeners is not None: + while self._ctx is not parentCtx: + self.triggerExitRuleEvent() + self._ctx = self._ctx.parentCtx + else: + self._ctx = parentCtx + + # hook into tree + retCtx.parentCtx = parentCtx + + if self.buildParseTrees and parentCtx is not None: + # add return ctx into invoking rule's tree + parentCtx.addChild(retCtx) + + def getInvokingContext(self, ruleIndex:int): + ctx = self._ctx + while ctx is not None: + if ctx.getRuleIndex() == ruleIndex: + return ctx + ctx = ctx.parentCtx + return None + + + def precpred(self, localctx:RuleContext , precedence:int): + return precedence >= self._precedenceStack[-1] + + def inContext(self, context:str): + # TODO: useful in parser? + return False + + # + # Checks whether or not {@code symbol} can follow the current state in the + # ATN. The behavior of self method is equivalent to the following, but is + # implemented such that the complete context-sensitive follow set does not + # need to be explicitly constructed. + # + #
      +    # return getExpectedTokens().contains(symbol);
      +    # 
      + # + # @param symbol the symbol type to check + # @return {@code true} if {@code symbol} can follow the current state in + # the ATN, otherwise {@code false}. + # + def isExpectedToken(self, symbol:int): + atn = self._interp.atn + ctx = self._ctx + s = atn.states[self.state] + following = atn.nextTokens(s) + if symbol in following: + return True + if not Token.EPSILON in following: + return False + + while ctx is not None and ctx.invokingState>=0 and Token.EPSILON in following: + invokingState = atn.states[ctx.invokingState] + rt = invokingState.transitions[0] + following = atn.nextTokens(rt.followState) + if symbol in following: + return True + ctx = ctx.parentCtx + + if Token.EPSILON in following and symbol == Token.EOF: + return True + else: + return False + + # Computes the set of input symbols which could follow the current parser + # state and context, as given by {@link #getState} and {@link #getContext}, + # respectively. + # + # @see ATN#getExpectedTokens(int, RuleContext) + # + def getExpectedTokens(self): + return self._interp.atn.getExpectedTokens(self.state, self._ctx) + + def getExpectedTokensWithinCurrentRule(self): + atn = self._interp.atn + s = atn.states[self.state] + return atn.nextTokens(s) + + # Get a rule's index (i.e., {@code RULE_ruleName} field) or -1 if not found.# + def getRuleIndex(self, ruleName:str): + ruleIndex = self.getRuleIndexMap().get(ruleName, None) + if ruleIndex is not None: + return ruleIndex + else: + return -1 + + # Return List<String> of the rule names in your parser instance + # leading up to a call to the current rule. You could override if + # you want more details such as the file/line info of where + # in the ATN a rule is invoked. + # + # this is very useful for error messages. + # + def getRuleInvocationStack(self, p:RuleContext=None): + if p is None: + p = self._ctx + stack = list() + while p is not None: + # compute what follows who invoked us + ruleIndex = p.getRuleIndex() + if ruleIndex<0: + stack.append("n/a") + else: + stack.append(self.ruleNames[ruleIndex]) + p = p.parentCtx + return stack + + # For debugging and other purposes.# + def getDFAStrings(self): + return [ str(dfa) for dfa in self._interp.decisionToDFA] + + # For debugging and other purposes.# + def dumpDFA(self): + seenOne = False + for i in range(0, len(self._interp.decisionToDFA)): + dfa = self._interp.decisionToDFA[i] + if len(dfa.states)>0: + if seenOne: + print(file=self._output) + print("Decision " + str(dfa.decision) + ":", file=self._output) + print(dfa.toString(self.literalNames, self.symbolicNames), end='', file=self._output) + seenOne = True + + + def getSourceName(self): + return self._input.sourceName + + # During a parse is sometimes useful to listen in on the rule entry and exit + # events as well as token matches. self is for quick and dirty debugging. + # + def setTrace(self, trace:bool): + if not trace: + self.removeParseListener(self._tracer) + self._tracer = None + else: + if self._tracer is not None: + self.removeParseListener(self._tracer) + self._tracer = TraceListener(self) + self.addParseListener(self._tracer) diff --git a/lib/python3.12/site-packages/antlr4/ParserInterpreter.py b/lib/python3.12/site-packages/antlr4/ParserInterpreter.py new file mode 100644 index 0000000000000000000000000000000000000000..4de43623cf7d308e841c9484470a93fec510d616 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/ParserInterpreter.py @@ -0,0 +1,170 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# A parser simulator that mimics what ANTLR's generated +# parser code does. A ParserATNSimulator is used to make +# predictions via adaptivePredict but this class moves a pointer through the +# ATN to simulate parsing. ParserATNSimulator just +# makes us efficient rather than having to backtrack, for example. +# +# This properly creates parse trees even for left recursive rules. +# +# We rely on the left recursive rule invocation and special predicate +# transitions to make left recursive rules work. +# +# See TestParserInterpreter for examples. +# +from antlr4.dfa.DFA import DFA +from antlr4.BufferedTokenStream import TokenStream +from antlr4.Lexer import Lexer +from antlr4.Parser import Parser +from antlr4.ParserRuleContext import InterpreterRuleContext, ParserRuleContext +from antlr4.Token import Token +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNState import StarLoopEntryState, ATNState, LoopEndState +from antlr4.atn.ParserATNSimulator import ParserATNSimulator +from antlr4.PredictionContext import PredictionContextCache +from antlr4.atn.Transition import Transition +from antlr4.error.Errors import RecognitionException, UnsupportedOperationException, FailedPredicateException + + +class ParserInterpreter(Parser): + __slots__ = ( + 'grammarFileName', 'atn', 'tokenNames', 'ruleNames', 'decisionToDFA', + 'sharedContextCache', '_parentContextStack', + 'pushRecursionContextStates' + ) + + def __init__(self, grammarFileName:str, tokenNames:list, ruleNames:list, atn:ATN, input:TokenStream): + super().__init__(input) + self.grammarFileName = grammarFileName + self.atn = atn + self.tokenNames = tokenNames + self.ruleNames = ruleNames + self.decisionToDFA = [ DFA(state) for state in atn.decisionToState ] + self.sharedContextCache = PredictionContextCache() + self._parentContextStack = list() + # identify the ATN states where pushNewRecursionContext must be called + self.pushRecursionContextStates = set() + for state in atn.states: + if not isinstance(state, StarLoopEntryState): + continue + if state.isPrecedenceDecision: + self.pushRecursionContextStates.add(state.stateNumber) + # get atn simulator that knows how to do predictions + self._interp = ParserATNSimulator(self, atn, self.decisionToDFA, self.sharedContextCache) + + # Begin parsing at startRuleIndex# + def parse(self, startRuleIndex:int): + startRuleStartState = self.atn.ruleToStartState[startRuleIndex] + rootContext = InterpreterRuleContext(None, ATNState.INVALID_STATE_NUMBER, startRuleIndex) + if startRuleStartState.isPrecedenceRule: + self.enterRecursionRule(rootContext, startRuleStartState.stateNumber, startRuleIndex, 0) + else: + self.enterRule(rootContext, startRuleStartState.stateNumber, startRuleIndex) + while True: + p = self.getATNState() + if p.stateType==ATNState.RULE_STOP : + # pop; return from rule + if len(self._ctx)==0: + if startRuleStartState.isPrecedenceRule: + result = self._ctx + parentContext = self._parentContextStack.pop() + self.unrollRecursionContexts(parentContext.a) + return result + else: + self.exitRule() + return rootContext + self.visitRuleStopState(p) + + else: + try: + self.visitState(p) + except RecognitionException as e: + self.state = self.atn.ruleToStopState[p.ruleIndex].stateNumber + self._ctx.exception = e + self._errHandler.reportError(self, e) + self._errHandler.recover(self, e) + + def enterRecursionRule(self, localctx:ParserRuleContext, state:int, ruleIndex:int, precedence:int): + self._parentContextStack.append((self._ctx, localctx.invokingState)) + super().enterRecursionRule(localctx, state, ruleIndex, precedence) + + def getATNState(self): + return self.atn.states[self.state] + + def visitState(self, p:ATNState): + edge = 0 + if len(p.transitions) > 1: + self._errHandler.sync(self) + edge = self._interp.adaptivePredict(self._input, p.decision, self._ctx) + else: + edge = 1 + + transition = p.transitions[edge - 1] + tt = transition.serializationType + if tt==Transition.EPSILON: + + if self.pushRecursionContextStates[p.stateNumber] and not isinstance(transition.target, LoopEndState): + t = self._parentContextStack[-1] + ctx = InterpreterRuleContext(t[0], t[1], self._ctx.ruleIndex) + self.pushNewRecursionContext(ctx, self.atn.ruleToStartState[p.ruleIndex].stateNumber, self._ctx.ruleIndex) + + elif tt==Transition.ATOM: + + self.match(transition.label) + + elif tt in [ Transition.RANGE, Transition.SET, Transition.NOT_SET]: + + if not transition.matches(self._input.LA(1), Token.MIN_USER_TOKEN_TYPE, Lexer.MAX_CHAR_VALUE): + self._errHandler.recoverInline(self) + self.matchWildcard() + + elif tt==Transition.WILDCARD: + + self.matchWildcard() + + elif tt==Transition.RULE: + + ruleStartState = transition.target + ruleIndex = ruleStartState.ruleIndex + ctx = InterpreterRuleContext(self._ctx, p.stateNumber, ruleIndex) + if ruleStartState.isPrecedenceRule: + self.enterRecursionRule(ctx, ruleStartState.stateNumber, ruleIndex, transition.precedence) + else: + self.enterRule(ctx, transition.target.stateNumber, ruleIndex) + + elif tt==Transition.PREDICATE: + + if not self.sempred(self._ctx, transition.ruleIndex, transition.predIndex): + raise FailedPredicateException(self) + + elif tt==Transition.ACTION: + + self.action(self._ctx, transition.ruleIndex, transition.actionIndex) + + elif tt==Transition.PRECEDENCE: + + if not self.precpred(self._ctx, transition.precedence): + msg = "precpred(_ctx, " + str(transition.precedence) + ")" + raise FailedPredicateException(self, msg) + + else: + raise UnsupportedOperationException("Unrecognized ATN transition type.") + + self.state = transition.target.stateNumber + + def visitRuleStopState(self, p:ATNState): + ruleStartState = self.atn.ruleToStartState[p.ruleIndex] + if ruleStartState.isPrecedenceRule: + parentContext = self._parentContextStack.pop() + self.unrollRecursionContexts(parentContext.a) + self.state = parentContext[1] + else: + self.exitRule() + + ruleTransition = self.atn.states[self.state].transitions[0] + self.state = ruleTransition.followState.stateNumber diff --git a/lib/python3.12/site-packages/antlr4/ParserRuleContext.py b/lib/python3.12/site-packages/antlr4/ParserRuleContext.py new file mode 100644 index 0000000000000000000000000000000000000000..f945c5ba94ac394ff7714e4d4e43a86aa31924df --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/ParserRuleContext.py @@ -0,0 +1,186 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. + +#* A rule invocation record for parsing. +# +# Contains all of the information about the current rule not stored in the +# RuleContext. It handles parse tree children list, Any ATN state +# tracing, and the default values available for rule indications: +# start, stop, rule index, current alt number, current +# ATN state. +# +# Subclasses made for each rule and grammar track the parameters, +# return values, locals, and labels specific to that rule. These +# are the objects that are returned from rules. +# +# Note text is not an actual field of a rule return value; it is computed +# from start and stop using the input stream's toString() method. I +# could add a ctor to this so that we can pass in and store the input +# stream, but I'm not sure we want to do that. It would seem to be undefined +# to get the .text property anyway if the rule matches tokens from multiple +# input streams. +# +# I do not use getters for fields of objects that are used simply to +# group values such as this aggregate. The getters/setters are there to +# satisfy the superclass interface. + +from antlr4.RuleContext import RuleContext +from antlr4.Token import Token +from antlr4.tree.Tree import ParseTreeListener, ParseTree, TerminalNodeImpl, ErrorNodeImpl, TerminalNode, \ + INVALID_INTERVAL + +# need forward declaration +ParserRuleContext = None + +class ParserRuleContext(RuleContext): + __slots__ = ('children', 'start', 'stop', 'exception') + def __init__(self, parent:ParserRuleContext = None, invokingStateNumber:int = None ): + super().__init__(parent, invokingStateNumber) + #* If we are debugging or building a parse tree for a visitor, + # we need to track all of the tokens and rule invocations associated + # with this rule's context. This is empty for parsing w/o tree constr. + # operation because we don't the need to track the details about + # how we parse this rule. + #/ + self.children = None + self.start = None + self.stop = None + # The exception that forced this rule to return. If the rule successfully + # completed, this is {@code null}. + self.exception = None + + #* COPY a ctx (I'm deliberately not using copy constructor)#/ + # + # This is used in the generated parser code to flip a generic XContext + # node for rule X to a YContext for alt label Y. In that sense, it is + # not really a generic copy function. + # + # If we do an error sync() at start of a rule, we might add error nodes + # to the generic XContext so this function must copy those nodes to + # the YContext as well else they are lost! + #/ + def copyFrom(self, ctx:ParserRuleContext): + # from RuleContext + self.parentCtx = ctx.parentCtx + self.invokingState = ctx.invokingState + self.children = None + self.start = ctx.start + self.stop = ctx.stop + + # copy any error nodes to alt label node + if ctx.children is not None: + self.children = [] + # reset parent pointer for any error nodes + for child in ctx.children: + if isinstance(child, ErrorNodeImpl): + self.children.append(child) + child.parentCtx = self + + # Double dispatch methods for listeners + def enterRule(self, listener:ParseTreeListener): + pass + + def exitRule(self, listener:ParseTreeListener): + pass + + #* Does not set parent link; other add methods do that#/ + def addChild(self, child:ParseTree): + if self.children is None: + self.children = [] + self.children.append(child) + return child + + #* Used by enterOuterAlt to toss out a RuleContext previously added as + # we entered a rule. If we have # label, we will need to remove + # generic ruleContext object. + #/ + def removeLastChild(self): + if self.children is not None: + del self.children[len(self.children)-1] + + def addTokenNode(self, token:Token): + node = TerminalNodeImpl(token) + self.addChild(node) + node.parentCtx = self + return node + + def addErrorNode(self, badToken:Token): + node = ErrorNodeImpl(badToken) + self.addChild(node) + node.parentCtx = self + return node + + def getChild(self, i:int, ttype:type = None): + if ttype is None: + return self.children[i] if len(self.children)>i else None + else: + for child in self.getChildren(): + if not isinstance(child, ttype): + continue + if i==0: + return child + i -= 1 + return None + + def getChildren(self, predicate = None): + if self.children is not None: + for child in self.children: + if predicate is not None and not predicate(child): + continue + yield child + + def getToken(self, ttype:int, i:int): + for child in self.getChildren(): + if not isinstance(child, TerminalNode): + continue + if child.symbol.type != ttype: + continue + if i==0: + return child + i -= 1 + return None + + def getTokens(self, ttype:int ): + if self.getChildren() is None: + return [] + tokens = [] + for child in self.getChildren(): + if not isinstance(child, TerminalNode): + continue + if child.symbol.type != ttype: + continue + tokens.append(child) + return tokens + + def getTypedRuleContext(self, ctxType:type, i:int): + return self.getChild(i, ctxType) + + def getTypedRuleContexts(self, ctxType:type): + children = self.getChildren() + if children is None: + return [] + contexts = [] + for child in children: + if not isinstance(child, ctxType): + continue + contexts.append(child) + return contexts + + def getChildCount(self): + return len(self.children) if self.children else 0 + + def getSourceInterval(self): + if self.start is None or self.stop is None: + return INVALID_INTERVAL + else: + return (self.start.tokenIndex, self.stop.tokenIndex) + + +RuleContext.EMPTY = ParserRuleContext() + +class InterpreterRuleContext(ParserRuleContext): + + def __init__(self, parent:ParserRuleContext, invokingStateNumber:int, ruleIndex:int): + super().__init__(parent, invokingStateNumber) + self.ruleIndex = ruleIndex diff --git a/lib/python3.12/site-packages/antlr4/PredictionContext.py b/lib/python3.12/site-packages/antlr4/PredictionContext.py new file mode 100644 index 0000000000000000000000000000000000000000..7cbb7e49e1c563d75059ed7b4043f7f75da68132 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/PredictionContext.py @@ -0,0 +1,623 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ +from io import StringIO + +from antlr4.error.Errors import IllegalStateException + +from antlr4.RuleContext import RuleContext +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNState import ATNState + + +class PredictionContext(object): + + # Represents {@code $} in local context prediction, which means wildcard. + # {@code#+x =#}. + #/ + EMPTY = None + + # Represents {@code $} in an array in full context mode, when {@code $} + # doesn't mean wildcard: {@code $ + x = [$,x]}. Here, + # {@code $} = {@link #EMPTY_RETURN_STATE}. + #/ + EMPTY_RETURN_STATE = 0x7FFFFFFF + + globalNodeCount = 1 + id = globalNodeCount + + # Stores the computed hash code of this {@link PredictionContext}. The hash + # code is computed in parts to match the following reference algorithm. + # + #
      +    #  private int referenceHashCode() {
      +    #      int hash = {@link MurmurHash#initialize MurmurHash.initialize}({@link #INITIAL_HASH});
      +    #
      +    #      for (int i = 0; i < {@link #size()}; i++) {
      +    #          hash = {@link MurmurHash#update MurmurHash.update}(hash, {@link #getParent getParent}(i));
      +    #      }
      +    #
      +    #      for (int i = 0; i < {@link #size()}; i++) {
      +    #          hash = {@link MurmurHash#update MurmurHash.update}(hash, {@link #getReturnState getReturnState}(i));
      +    #      }
      +    #
      +    #      hash = {@link MurmurHash#finish MurmurHash.finish}(hash, 2# {@link #size()});
      +    #      return hash;
      +    #  }
      +    # 
      + #/ + + def __init__(self, cachedHashCode:int): + self.cachedHashCode = cachedHashCode + + def __len__(self): + return 0 + + # This means only the {@link #EMPTY} context is in set. + def isEmpty(self): + return self is self.EMPTY + + def hasEmptyPath(self): + return self.getReturnState(len(self) - 1) == self.EMPTY_RETURN_STATE + + def getReturnState(self, index:int): + raise IllegalStateException("illegal!") + + def __hash__(self): + return self.cachedHashCode + +def calculateHashCode(parent:PredictionContext, returnState:int): + return hash("") if parent is None else hash((hash(parent), returnState)) + +def calculateListsHashCode(parents:[], returnStates:[] ): + h = 0 + for parent, returnState in zip(parents, returnStates): + h = hash((h, calculateHashCode(parent, returnState))) + return h + +# Used to cache {@link PredictionContext} objects. Its used for the shared +# context cash associated with contexts in DFA states. This cache +# can be used for both lexers and parsers. + +class PredictionContextCache(object): + + def __init__(self): + self.cache = dict() + + # Add a context to the cache and return it. If the context already exists, + # return that one instead and do not add a new context to the cache. + # Protect shared cache from unsafe thread access. + # + def add(self, ctx:PredictionContext): + if ctx==PredictionContext.EMPTY: + return PredictionContext.EMPTY + existing = self.cache.get(ctx, None) + if existing is not None: + return existing + self.cache[ctx] = ctx + return ctx + + def get(self, ctx:PredictionContext): + return self.cache.get(ctx, None) + + def __len__(self): + return len(self.cache) + + +class SingletonPredictionContext(PredictionContext): + + @staticmethod + def create(parent:PredictionContext , returnState:int ): + if returnState == PredictionContext.EMPTY_RETURN_STATE and parent is None: + # someone can pass in the bits of an array ctx that mean $ + return SingletonPredictionContext.EMPTY + else: + return SingletonPredictionContext(parent, returnState) + + def __init__(self, parent:PredictionContext, returnState:int): + hashCode = calculateHashCode(parent, returnState) + super().__init__(hashCode) + self.parentCtx = parent + self.returnState = returnState + + def __len__(self): + return 1 + + def getParent(self, index:int): + return self.parentCtx + + def getReturnState(self, index:int): + return self.returnState + + def __eq__(self, other): + if self is other: + return True + elif other is None: + return False + elif not isinstance(other, SingletonPredictionContext): + return False + else: + return self.returnState == other.returnState and self.parentCtx == other.parentCtx + + def __hash__(self): + return self.cachedHashCode + + def __str__(self): + up = "" if self.parentCtx is None else str(self.parentCtx) + if len(up)==0: + if self.returnState == self.EMPTY_RETURN_STATE: + return "$" + else: + return str(self.returnState) + else: + return str(self.returnState) + " " + up + + +class EmptyPredictionContext(SingletonPredictionContext): + + def __init__(self): + super().__init__(None, PredictionContext.EMPTY_RETURN_STATE) + + def isEmpty(self): + return True + + def __eq__(self, other): + return self is other + + def __hash__(self): + return self.cachedHashCode + + def __str__(self): + return "$" + + +PredictionContext.EMPTY = EmptyPredictionContext() + +class ArrayPredictionContext(PredictionContext): + # Parent can be null only if full ctx mode and we make an array + # from {@link #EMPTY} and non-empty. We merge {@link #EMPTY} by using null parent and + # returnState == {@link #EMPTY_RETURN_STATE}. + + def __init__(self, parents:list, returnStates:list): + super().__init__(calculateListsHashCode(parents, returnStates)) + self.parents = parents + self.returnStates = returnStates + + def isEmpty(self): + # since EMPTY_RETURN_STATE can only appear in the last position, we + # don't need to verify that size==1 + return self.returnStates[0]==PredictionContext.EMPTY_RETURN_STATE + + def __len__(self): + return len(self.returnStates) + + def getParent(self, index:int): + return self.parents[index] + + def getReturnState(self, index:int): + return self.returnStates[index] + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, ArrayPredictionContext): + return False + elif hash(self) != hash(other): + return False # can't be same if hash is different + else: + return self.returnStates==other.returnStates and self.parents==other.parents + + def __str__(self): + if self.isEmpty(): + return "[]" + with StringIO() as buf: + buf.write("[") + for i in range(0,len(self.returnStates)): + if i>0: + buf.write(", ") + if self.returnStates[i]==PredictionContext.EMPTY_RETURN_STATE: + buf.write("$") + continue + buf.write(str(self.returnStates[i])) + if self.parents[i] is not None: + buf.write(' ') + buf.write(str(self.parents[i])) + else: + buf.write("null") + buf.write("]") + return buf.getvalue() + + def __hash__(self): + return self.cachedHashCode + + + +# Convert a {@link RuleContext} tree to a {@link PredictionContext} graph. +# Return {@link #EMPTY} if {@code outerContext} is empty or null. +#/ +def PredictionContextFromRuleContext(atn:ATN, outerContext:RuleContext=None): + if outerContext is None: + outerContext = RuleContext.EMPTY + + # if we are in RuleContext of start rule, s, then PredictionContext + # is EMPTY. Nobody called us. (if we are empty, return empty) + if outerContext.parentCtx is None or outerContext is RuleContext.EMPTY: + return PredictionContext.EMPTY + + # If we have a parent, convert it to a PredictionContext graph + parent = PredictionContextFromRuleContext(atn, outerContext.parentCtx) + state = atn.states[outerContext.invokingState] + transition = state.transitions[0] + return SingletonPredictionContext.create(parent, transition.followState.stateNumber) + + +def merge(a:PredictionContext, b:PredictionContext, rootIsWildcard:bool, mergeCache:dict): + + # share same graph if both same + if a==b: + return a + + if isinstance(a, SingletonPredictionContext) and isinstance(b, SingletonPredictionContext): + return mergeSingletons(a, b, rootIsWildcard, mergeCache) + + # At least one of a or b is array + # If one is $ and rootIsWildcard, return $ as# wildcard + if rootIsWildcard: + if isinstance( a, EmptyPredictionContext ): + return a + if isinstance( b, EmptyPredictionContext ): + return b + + # convert singleton so both are arrays to normalize + if isinstance( a, SingletonPredictionContext ): + a = ArrayPredictionContext([a.parentCtx], [a.returnState]) + if isinstance( b, SingletonPredictionContext): + b = ArrayPredictionContext([b.parentCtx], [b.returnState]) + return mergeArrays(a, b, rootIsWildcard, mergeCache) + + +# +# Merge two {@link SingletonPredictionContext} instances. +# +#

      Stack tops equal, parents merge is same; return left graph.
      +#

      +# +#

      Same stack top, parents differ; merge parents giving array node, then +# remainders of those graphs. A new root node is created to point to the +# merged parents.
      +#

      +# +#

      Different stack tops pointing to same parent. Make array node for the +# root where both element in the root point to the same (original) +# parent.
      +#

      +# +#

      Different stack tops pointing to different parents. Make array node for +# the root where each element points to the corresponding original +# parent.
      +#

      +# +# @param a the first {@link SingletonPredictionContext} +# @param b the second {@link SingletonPredictionContext} +# @param rootIsWildcard {@code true} if this is a local-context merge, +# otherwise false to indicate a full-context merge +# @param mergeCache +#/ +def mergeSingletons(a:SingletonPredictionContext, b:SingletonPredictionContext, rootIsWildcard:bool, mergeCache:dict): + if mergeCache is not None: + previous = mergeCache.get((a,b), None) + if previous is not None: + return previous + previous = mergeCache.get((b,a), None) + if previous is not None: + return previous + + merged = mergeRoot(a, b, rootIsWildcard) + if merged is not None: + if mergeCache is not None: + mergeCache[(a, b)] = merged + return merged + + if a.returnState==b.returnState: + parent = merge(a.parentCtx, b.parentCtx, rootIsWildcard, mergeCache) + # if parent is same as existing a or b parent or reduced to a parent, return it + if parent == a.parentCtx: + return a # ax + bx = ax, if a=b + if parent == b.parentCtx: + return b # ax + bx = bx, if a=b + # else: ax + ay = a'[x,y] + # merge parents x and y, giving array node with x,y then remainders + # of those graphs. dup a, a' points at merged array + # new joined parent so create new singleton pointing to it, a' + merged = SingletonPredictionContext.create(parent, a.returnState) + if mergeCache is not None: + mergeCache[(a, b)] = merged + return merged + else: # a != b payloads differ + # see if we can collapse parents due to $+x parents if local ctx + singleParent = None + if a is b or (a.parentCtx is not None and a.parentCtx==b.parentCtx): # ax + bx = [a,b]x + singleParent = a.parentCtx + if singleParent is not None: # parents are same + # sort payloads and use same parent + payloads = [ a.returnState, b.returnState ] + if a.returnState > b.returnState: + payloads = [ b.returnState, a.returnState ] + parents = [singleParent, singleParent] + merged = ArrayPredictionContext(parents, payloads) + if mergeCache is not None: + mergeCache[(a, b)] = merged + return merged + # parents differ and can't merge them. Just pack together + # into array; can't merge. + # ax + by = [ax,by] + payloads = [ a.returnState, b.returnState ] + parents = [ a.parentCtx, b.parentCtx ] + if a.returnState > b.returnState: # sort by payload + payloads = [ b.returnState, a.returnState ] + parents = [ b.parentCtx, a.parentCtx ] + merged = ArrayPredictionContext(parents, payloads) + if mergeCache is not None: + mergeCache[(a, b)] = merged + return merged + + +# +# Handle case where at least one of {@code a} or {@code b} is +# {@link #EMPTY}. In the following diagrams, the symbol {@code $} is used +# to represent {@link #EMPTY}. +# +#

      Local-Context Merges

      +# +#

      These local-context merge operations are used when {@code rootIsWildcard} +# is true.

      +# +#

      {@link #EMPTY} is superset of any graph; return {@link #EMPTY}.
      +#

      +# +#

      {@link #EMPTY} and anything is {@code #EMPTY}, so merged parent is +# {@code #EMPTY}; return left graph.
      +#

      +# +#

      Special case of last merge if local context.
      +#

      +# +#

      Full-Context Merges

      +# +#

      These full-context merge operations are used when {@code rootIsWildcard} +# is false.

      +# +#

      +# +#

      Must keep all contexts; {@link #EMPTY} in array is a special value (and +# null parent).
      +#

      +# +#

      +# +# @param a the first {@link SingletonPredictionContext} +# @param b the second {@link SingletonPredictionContext} +# @param rootIsWildcard {@code true} if this is a local-context merge, +# otherwise false to indicate a full-context merge +#/ +def mergeRoot(a:SingletonPredictionContext, b:SingletonPredictionContext, rootIsWildcard:bool): + if rootIsWildcard: + if a == PredictionContext.EMPTY: + return PredictionContext.EMPTY ## + b =# + if b == PredictionContext.EMPTY: + return PredictionContext.EMPTY # a +# =# + else: + if a == PredictionContext.EMPTY and b == PredictionContext.EMPTY: + return PredictionContext.EMPTY # $ + $ = $ + elif a == PredictionContext.EMPTY: # $ + x = [$,x] + payloads = [ b.returnState, PredictionContext.EMPTY_RETURN_STATE ] + parents = [ b.parentCtx, None ] + return ArrayPredictionContext(parents, payloads) + elif b == PredictionContext.EMPTY: # x + $ = [$,x] ($ is always first if present) + payloads = [ a.returnState, PredictionContext.EMPTY_RETURN_STATE ] + parents = [ a.parentCtx, None ] + return ArrayPredictionContext(parents, payloads) + return None + + +# +# Merge two {@link ArrayPredictionContext} instances. +# +#

      Different tops, different parents.
      +#

      +# +#

      Shared top, same parents.
      +#

      +# +#

      Shared top, different parents.
      +#

      +# +#

      Shared top, all shared parents.
      +#

      +# +#

      Equal tops, merge parents and reduce top to +# {@link SingletonPredictionContext}.
      +#

      +#/ +def mergeArrays(a:ArrayPredictionContext, b:ArrayPredictionContext, rootIsWildcard:bool, mergeCache:dict): + if mergeCache is not None: + previous = mergeCache.get((a,b), None) + if previous is not None: + return previous + previous = mergeCache.get((b,a), None) + if previous is not None: + return previous + + # merge sorted payloads a + b => M + i = 0 # walks a + j = 0 # walks b + k = 0 # walks target M array + + mergedReturnStates = [None] * (len(a.returnStates) + len( b.returnStates)) + mergedParents = [None] * len(mergedReturnStates) + # walk and merge to yield mergedParents, mergedReturnStates + while i ax + if bothDollars or ax_ax: + mergedParents[k] = a_parent # choose left + mergedReturnStates[k] = payload + else: # ax+ay -> a'[x,y] + mergedParent = merge(a_parent, b_parent, rootIsWildcard, mergeCache) + mergedParents[k] = mergedParent + mergedReturnStates[k] = payload + i += 1 # hop over left one as usual + j += 1 # but also skip one in right side since we merge + elif a.returnStates[i] a, copy b[j] to M + mergedParents[k] = b_parent + mergedReturnStates[k] = b.returnStates[j] + j += 1 + k += 1 + + # copy over any payloads remaining in either array + if i < len(a.returnStates): + for p in range(i, len(a.returnStates)): + mergedParents[k] = a.parents[p] + mergedReturnStates[k] = a.returnStates[p] + k += 1 + else: + for p in range(j, len(b.returnStates)): + mergedParents[k] = b.parents[p] + mergedReturnStates[k] = b.returnStates[p] + k += 1 + + # trim merged if we combined a few that had same stack tops + if k < len(mergedParents): # write index < last position; trim + if k == 1: # for just one merged element, return singleton top + merged = SingletonPredictionContext.create(mergedParents[0], mergedReturnStates[0]) + if mergeCache is not None: + mergeCache[(a,b)] = merged + return merged + mergedParents = mergedParents[0:k] + mergedReturnStates = mergedReturnStates[0:k] + + merged = ArrayPredictionContext(mergedParents, mergedReturnStates) + + # if we created same array as a or b, return that instead + # TODO: track whether this is possible above during merge sort for speed + if merged==a: + if mergeCache is not None: + mergeCache[(a,b)] = a + return a + if merged==b: + if mergeCache is not None: + mergeCache[(a,b)] = b + return b + combineCommonParents(mergedParents) + + if mergeCache is not None: + mergeCache[(a,b)] = merged + return merged + + +# +# Make pass over all M {@code parents}; merge any {@code equals()} +# ones. +#/ +def combineCommonParents(parents:list): + uniqueParents = dict() + + for p in range(0, len(parents)): + parent = parents[p] + if uniqueParents.get(parent, None) is None: + uniqueParents[parent] = parent + + for p in range(0, len(parents)): + parents[p] = uniqueParents[parents[p]] + +def getCachedPredictionContext(context:PredictionContext, contextCache:PredictionContextCache, visited:dict): + if context.isEmpty(): + return context + existing = visited.get(context) + if existing is not None: + return existing + existing = contextCache.get(context) + if existing is not None: + visited[context] = existing + return existing + changed = False + parents = [None] * len(context) + for i in range(0, len(parents)): + parent = getCachedPredictionContext(context.getParent(i), contextCache, visited) + if changed or parent is not context.getParent(i): + if not changed: + parents = [context.getParent(j) for j in range(len(context))] + changed = True + parents[i] = parent + if not changed: + contextCache.add(context) + visited[context] = context + return context + + updated = None + if len(parents) == 0: + updated = PredictionContext.EMPTY + elif len(parents) == 1: + updated = SingletonPredictionContext.create(parents[0], context.getReturnState(0)) + else: + updated = ArrayPredictionContext(parents, context.returnStates) + + contextCache.add(updated) + visited[updated] = updated + visited[context] = updated + + return updated + + +# # extra structures, but cut/paste/morphed works, so leave it. +# # seems to do a breadth-first walk +# public static List getAllNodes(PredictionContext context) { +# Map visited = +# new IdentityHashMap(); +# Deque workList = new ArrayDeque(); +# workList.add(context); +# visited.put(context, context); +# List nodes = new ArrayList(); +# while (!workList.isEmpty()) { +# PredictionContext current = workList.pop(); +# nodes.add(current); +# for (int i = 0; i < current.size(); i++) { +# PredictionContext parent = current.getParent(i); +# if ( parent!=null && visited.put(parent, parent) == null) { +# workList.push(parent); +# } +# } +# } +# return nodes; +# } + +# ter's recursive version of Sam's getAllNodes() +def getAllContextNodes(context:PredictionContext, nodes:list=None, visited:dict=None): + if nodes is None: + nodes = list() + return getAllContextNodes(context, nodes, visited) + elif visited is None: + visited = dict() + return getAllContextNodes(context, nodes, visited) + else: + if context is None or visited.get(context, None) is not None: + return nodes + visited.put(context, context) + nodes.add(context) + for i in range(0, len(context)): + getAllContextNodes(context.getParent(i), nodes, visited) + return nodes + diff --git a/lib/python3.12/site-packages/antlr4/Recognizer.py b/lib/python3.12/site-packages/antlr4/Recognizer.py new file mode 100644 index 0000000000000000000000000000000000000000..d87738be57ebfd87569615ef24de72e04094ecaf --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/Recognizer.py @@ -0,0 +1,147 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# +from antlr4.RuleContext import RuleContext +from antlr4.Token import Token +from antlr4.error.ErrorListener import ProxyErrorListener, ConsoleErrorListener + +# need forward delcaration +RecognitionException = None + +class Recognizer(object): + __slots__ = ('_listeners', '_interp', '_stateNumber') + + tokenTypeMapCache = dict() + ruleIndexMapCache = dict() + + def __init__(self): + self._listeners = [ ConsoleErrorListener.INSTANCE ] + self._interp = None + self._stateNumber = -1 + + def extractVersion(self, version): + pos = version.find(".") + major = version[0:pos] + version = version[pos+1:] + pos = version.find(".") + if pos==-1: + pos = version.find("-") + if pos==-1: + pos = len(version) + minor = version[0:pos] + return major, minor + + def checkVersion(self, toolVersion): + runtimeVersion = "4.9.3" + rvmajor, rvminor = self.extractVersion(runtimeVersion) + tvmajor, tvminor = self.extractVersion(toolVersion) + if rvmajor!=tvmajor or rvminor!=tvminor: + print("ANTLR runtime and generated code versions disagree: "+runtimeVersion+"!="+toolVersion) + + def addErrorListener(self, listener): + self._listeners.append(listener) + + def removeErrorListener(self, listener): + self._listeners.remove(listener) + + def removeErrorListeners(self): + self._listeners = [] + + def getTokenTypeMap(self): + tokenNames = self.getTokenNames() + if tokenNames is None: + from antlr4.error.Errors import UnsupportedOperationException + raise UnsupportedOperationException("The current recognizer does not provide a list of token names.") + result = self.tokenTypeMapCache.get(tokenNames, None) + if result is None: + result = zip( tokenNames, range(0, len(tokenNames))) + result["EOF"] = Token.EOF + self.tokenTypeMapCache[tokenNames] = result + return result + + # Get a map from rule names to rule indexes. + # + #

      Used for XPath and tree pattern compilation.

      + # + def getRuleIndexMap(self): + ruleNames = self.getRuleNames() + if ruleNames is None: + from antlr4.error.Errors import UnsupportedOperationException + raise UnsupportedOperationException("The current recognizer does not provide a list of rule names.") + result = self.ruleIndexMapCache.get(ruleNames, None) + if result is None: + result = zip( ruleNames, range(0, len(ruleNames))) + self.ruleIndexMapCache[ruleNames] = result + return result + + def getTokenType(self, tokenName:str): + ttype = self.getTokenTypeMap().get(tokenName, None) + if ttype is not None: + return ttype + else: + return Token.INVALID_TYPE + + + # What is the error header, normally line/character position information?# + def getErrorHeader(self, e:RecognitionException): + line = e.getOffendingToken().line + column = e.getOffendingToken().column + return "line "+line+":"+column + + + # How should a token be displayed in an error message? The default + # is to display just the text, but during development you might + # want to have a lot of information spit out. Override in that case + # to use t.toString() (which, for CommonToken, dumps everything about + # the token). This is better than forcing you to override a method in + # your token objects because you don't have to go modify your lexer + # so that it creates a new Java type. + # + # @deprecated This method is not called by the ANTLR 4 Runtime. Specific + # implementations of {@link ANTLRErrorStrategy} may provide a similar + # feature when necessary. For example, see + # {@link DefaultErrorStrategy#getTokenErrorDisplay}. + # + def getTokenErrorDisplay(self, t:Token): + if t is None: + return "" + s = t.text + if s is None: + if t.type==Token.EOF: + s = "" + else: + s = "<" + str(t.type) + ">" + s = s.replace("\n","\\n") + s = s.replace("\r","\\r") + s = s.replace("\t","\\t") + return "'" + s + "'" + + def getErrorListenerDispatch(self): + return ProxyErrorListener(self._listeners) + + # subclass needs to override these if there are sempreds or actions + # that the ATN interp needs to execute + def sempred(self, localctx:RuleContext, ruleIndex:int, actionIndex:int): + return True + + def precpred(self, localctx:RuleContext , precedence:int): + return True + + @property + def state(self): + return self._stateNumber + + # Indicate that the recognizer has changed internal state that is + # consistent with the ATN state passed in. This way we always know + # where we are in the ATN as the parser goes along. The rule + # context objects form a stack that lets us see the stack of + # invoking rules. Combine this and we have complete ATN + # configuration information. + + @state.setter + def state(self, atnState:int): + self._stateNumber = atnState + +del RecognitionException diff --git a/lib/python3.12/site-packages/antlr4/RuleContext.py b/lib/python3.12/site-packages/antlr4/RuleContext.py new file mode 100644 index 0000000000000000000000000000000000000000..7812ba3b1c87cc53dd5a7e64cdd0c556f6a43822 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/RuleContext.py @@ -0,0 +1,227 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + + +# A rule context is a record of a single rule invocation. It knows +# which context invoked it, if any. If there is no parent context, then +# naturally the invoking state is not valid. The parent link +# provides a chain upwards from the current rule invocation to the root +# of the invocation tree, forming a stack. We actually carry no +# information about the rule associated with this context (except +# when parsing). We keep only the state number of the invoking state from +# the ATN submachine that invoked this. Contrast this with the s +# pointer inside ParserRuleContext that tracks the current state +# being "executed" for the current rule. +# +# The parent contexts are useful for computing lookahead sets and +# getting error information. +# +# These objects are used during parsing and prediction. +# For the special case of parsers, we use the subclass +# ParserRuleContext. +# +# @see ParserRuleContext +#/ +from io import StringIO +from antlr4.tree.Tree import RuleNode, INVALID_INTERVAL, ParseTreeVisitor +from antlr4.tree.Trees import Trees + +# need forward declarations +RuleContext = None +Parser = None + +class RuleContext(RuleNode): + __slots__ = ('parentCtx', 'invokingState') + EMPTY = None + + def __init__(self, parent:RuleContext=None, invokingState:int=-1): + super().__init__() + # What context invoked this rule? + self.parentCtx = parent + # What state invoked the rule associated with this context? + # The "return address" is the followState of invokingState + # If parent is null, this should be -1. + self.invokingState = invokingState + + + def depth(self): + n = 0 + p = self + while p is not None: + p = p.parentCtx + n += 1 + return n + + # A context is empty if there is no invoking state; meaning nobody call + # current context. + def isEmpty(self): + return self.invokingState == -1 + + # satisfy the ParseTree / SyntaxTree interface + + def getSourceInterval(self): + return INVALID_INTERVAL + + def getRuleContext(self): + return self + + def getPayload(self): + return self + + # Return the combined text of all child nodes. This method only considers + # tokens which have been added to the parse tree. + #

      + # Since tokens on hidden channels (e.g. whitespace or comments) are not + # added to the parse trees, they will not appear in the output of this + # method. + #/ + def getText(self): + if self.getChildCount() == 0: + return "" + with StringIO() as builder: + for child in self.getChildren(): + builder.write(child.getText()) + return builder.getvalue() + + def getRuleIndex(self): + return -1 + + # For rule associated with this parse tree internal node, return + # the outer alternative number used to match the input. Default + # implementation does not compute nor store this alt num. Create + # a subclass of ParserRuleContext with backing field and set + # option contextSuperClass. + # to set it. + def getAltNumber(self): + return 0 # should use ATN.INVALID_ALT_NUMBER but won't compile + + # Set the outer alternative number for this context node. Default + # implementation does nothing to avoid backing field overhead for + # trees that don't need it. Create + # a subclass of ParserRuleContext with backing field and set + # option contextSuperClass. + def setAltNumber(self, altNumber:int): + pass + + def getChild(self, i:int): + return None + + def getChildCount(self): + return 0 + + def getChildren(self): + for c in []: + yield c + + def accept(self, visitor:ParseTreeVisitor): + return visitor.visitChildren(self) + + # # Call this method to view a parse tree in a dialog box visually.#/ + # public Future inspect(@Nullable Parser parser) { + # List ruleNames = parser != null ? Arrays.asList(parser.getRuleNames()) : null; + # return inspect(ruleNames); + # } + # + # public Future inspect(@Nullable List ruleNames) { + # TreeViewer viewer = new TreeViewer(ruleNames, this); + # return viewer.open(); + # } + # + # # Save this tree in a postscript file#/ + # public void save(@Nullable Parser parser, String fileName) + # throws IOException, PrintException + # { + # List ruleNames = parser != null ? Arrays.asList(parser.getRuleNames()) : null; + # save(ruleNames, fileName); + # } + # + # # Save this tree in a postscript file using a particular font name and size#/ + # public void save(@Nullable Parser parser, String fileName, + # String fontName, int fontSize) + # throws IOException + # { + # List ruleNames = parser != null ? Arrays.asList(parser.getRuleNames()) : null; + # save(ruleNames, fileName, fontName, fontSize); + # } + # + # # Save this tree in a postscript file#/ + # public void save(@Nullable List ruleNames, String fileName) + # throws IOException, PrintException + # { + # Trees.writePS(this, ruleNames, fileName); + # } + # + # # Save this tree in a postscript file using a particular font name and size#/ + # public void save(@Nullable List ruleNames, String fileName, + # String fontName, int fontSize) + # throws IOException + # { + # Trees.writePS(this, ruleNames, fileName, fontName, fontSize); + # } + # + # # Print out a whole tree, not just a node, in LISP format + # # (root child1 .. childN). Print just a node if this is a leaf. + # # We have to know the recognizer so we can get rule names. + # #/ + # @Override + # public String toStringTree(@Nullable Parser recog) { + # return Trees.toStringTree(this, recog); + # } + # + # Print out a whole tree, not just a node, in LISP format + # (root child1 .. childN). Print just a node if this is a leaf. + # + def toStringTree(self, ruleNames:list=None, recog:Parser=None): + return Trees.toStringTree(self, ruleNames=ruleNames, recog=recog) + # } + # + # @Override + # public String toStringTree() { + # return toStringTree((List)null); + # } + # + def __str__(self): + return self.toString(None, None) + + # @Override + # public String toString() { + # return toString((List)null, (RuleContext)null); + # } + # + # public final String toString(@Nullable Recognizer recog) { + # return toString(recog, ParserRuleContext.EMPTY); + # } + # + # public final String toString(@Nullable List ruleNames) { + # return toString(ruleNames, null); + # } + # + # // recog null unless ParserRuleContext, in which case we use subclass toString(...) + # public String toString(@Nullable Recognizer recog, @Nullable RuleContext stop) { + # String[] ruleNames = recog != null ? recog.getRuleNames() : null; + # List ruleNamesList = ruleNames != null ? Arrays.asList(ruleNames) : null; + # return toString(ruleNamesList, stop); + # } + + def toString(self, ruleNames:list, stop:RuleContext)->str: + with StringIO() as buf: + p = self + buf.write("[") + while p is not None and p is not stop: + if ruleNames is None: + if not p.isEmpty(): + buf.write(str(p.invokingState)) + else: + ri = p.getRuleIndex() + ruleName = ruleNames[ri] if ri >= 0 and ri < len(ruleNames) else str(ri) + buf.write(ruleName) + + if p.parentCtx is not None and (ruleNames is not None or not p.parentCtx.isEmpty()): + buf.write(" ") + + p = p.parentCtx + + buf.write("]") + return buf.getvalue() diff --git a/lib/python3.12/site-packages/antlr4/StdinStream.py b/lib/python3.12/site-packages/antlr4/StdinStream.py new file mode 100644 index 0000000000000000000000000000000000000000..f044fc4d770b4bc86cce2a5578f2c2fa00fc7602 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/StdinStream.py @@ -0,0 +1,11 @@ +import codecs +import sys + +from antlr4.InputStream import InputStream + + +class StdinStream(InputStream): + def __init__(self, encoding:str='ascii', errors:str='strict') -> None: + bytes = sys.stdin.buffer.read() + data = codecs.decode(bytes, encoding, errors) + super().__init__(data) diff --git a/lib/python3.12/site-packages/antlr4/Token.py b/lib/python3.12/site-packages/antlr4/Token.py new file mode 100644 index 0000000000000000000000000000000000000000..10a68a8c2260e47838a1c9403def21725aecc244 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/Token.py @@ -0,0 +1,155 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# A token has properties: text, type, line, character position in the line +# (so we can ignore tabs), token channel, index, and source from which +# we obtained this token. +from io import StringIO + + +class Token (object): + __slots__ = ('source', 'type', 'channel', 'start', 'stop', 'tokenIndex', 'line', 'column', '_text') + + INVALID_TYPE = 0 + + # During lookahead operations, this "token" signifies we hit rule end ATN state + # and did not follow it despite needing to. + EPSILON = -2 + + MIN_USER_TOKEN_TYPE = 1 + + EOF = -1 + + # All tokens go to the parser (unless skip() is called in that rule) + # on a particular "channel". The parser tunes to a particular channel + # so that whitespace etc... can go to the parser on a "hidden" channel. + + DEFAULT_CHANNEL = 0 + + # Anything on different channel than DEFAULT_CHANNEL is not parsed + # by parser. + + HIDDEN_CHANNEL = 1 + + def __init__(self): + self.source = None + self.type = None # token type of the token + self.channel = None # The parser ignores everything not on DEFAULT_CHANNEL + self.start = None # optional; return -1 if not implemented. + self.stop = None # optional; return -1 if not implemented. + self.tokenIndex = None # from 0..n-1 of the token object in the input stream + self.line = None # line=1..n of the 1st character + self.column = None # beginning of the line at which it occurs, 0..n-1 + self._text = None # text of the token. + + @property + def text(self): + return self._text + + # Explicitly set the text for this token. If {code text} is not + # {@code null}, then {@link #getText} will return this value rather than + # extracting the text from the input. + # + # @param text The explicit text of the token, or {@code null} if the text + # should be obtained from the input along with the start and stop indexes + # of the token. + + @text.setter + def text(self, text:str): + self._text = text + + + def getTokenSource(self): + return self.source[0] + + def getInputStream(self): + return self.source[1] + +class CommonToken(Token): + + # An empty {@link Pair} which is used as the default value of + # {@link #source} for tokens that do not have a source. + EMPTY_SOURCE = (None, None) + + def __init__(self, source:tuple = EMPTY_SOURCE, type:int = None, channel:int=Token.DEFAULT_CHANNEL, start:int=-1, stop:int=-1): + super().__init__() + self.source = source + self.type = type + self.channel = channel + self.start = start + self.stop = stop + self.tokenIndex = -1 + if source[0] is not None: + self.line = source[0].line + self.column = source[0].column + else: + self.column = -1 + + # Constructs a new {@link CommonToken} as a copy of another {@link Token}. + # + #

      + # If {@code oldToken} is also a {@link CommonToken} instance, the newly + # constructed token will share a reference to the {@link #text} field and + # the {@link Pair} stored in {@link #source}. Otherwise, {@link #text} will + # be assigned the result of calling {@link #getText}, and {@link #source} + # will be constructed from the result of {@link Token#getTokenSource} and + # {@link Token#getInputStream}.

      + # + # @param oldToken The token to copy. + # + def clone(self): + t = CommonToken(self.source, self.type, self.channel, self.start, self.stop) + t.tokenIndex = self.tokenIndex + t.line = self.line + t.column = self.column + t.text = self.text + return t + + @property + def text(self): + if self._text is not None: + return self._text + input = self.getInputStream() + if input is None: + return None + n = input.size + if self.start < n and self.stop < n: + return input.getText(self.start, self.stop) + else: + return "" + + @text.setter + def text(self, text:str): + self._text = text + + def __str__(self): + with StringIO() as buf: + buf.write("[@") + buf.write(str(self.tokenIndex)) + buf.write(",") + buf.write(str(self.start)) + buf.write(":") + buf.write(str(self.stop)) + buf.write("='") + txt = self.text + if txt is not None: + txt = txt.replace("\n","\\n") + txt = txt.replace("\r","\\r") + txt = txt.replace("\t","\\t") + else: + txt = "" + buf.write(txt) + buf.write("',<") + buf.write(str(self.type)) + buf.write(">") + if self.channel > 0: + buf.write(",channel=") + buf.write(str(self.channel)) + buf.write(",") + buf.write(str(self.line)) + buf.write(":") + buf.write(str(self.column)) + buf.write("]") + return buf.getvalue() diff --git a/lib/python3.12/site-packages/antlr4/TokenStreamRewriter.py b/lib/python3.12/site-packages/antlr4/TokenStreamRewriter.py new file mode 100644 index 0000000000000000000000000000000000000000..59baf8f47a1ba447a143842c8df825ef58035918 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/TokenStreamRewriter.py @@ -0,0 +1,255 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +from io import StringIO +from antlr4.Token import Token + +from antlr4.CommonTokenStream import CommonTokenStream + + +class TokenStreamRewriter(object): + __slots__ = ('tokens', 'programs', 'lastRewriteTokenIndexes') + + DEFAULT_PROGRAM_NAME = "default" + PROGRAM_INIT_SIZE = 100 + MIN_TOKEN_INDEX = 0 + + def __init__(self, tokens): + """ + :type tokens: antlr4.BufferedTokenStream.BufferedTokenStream + :param tokens: + :return: + """ + super(TokenStreamRewriter, self).__init__() + self.tokens = tokens + self.programs = {self.DEFAULT_PROGRAM_NAME: []} + self.lastRewriteTokenIndexes = {} + + def getTokenStream(self): + return self.tokens + + def rollback(self, instruction_index, program_name): + ins = self.programs.get(program_name, None) + if ins: + self.programs[program_name] = ins[self.MIN_TOKEN_INDEX: instruction_index] + + def deleteProgram(self, program_name=DEFAULT_PROGRAM_NAME): + self.rollback(self.MIN_TOKEN_INDEX, program_name) + + def insertAfterToken(self, token, text, program_name=DEFAULT_PROGRAM_NAME): + self.insertAfter(token.tokenIndex, text, program_name) + + def insertAfter(self, index, text, program_name=DEFAULT_PROGRAM_NAME): + op = self.InsertAfterOp(self.tokens, index + 1, text) + rewrites = self.getProgram(program_name) + op.instructionIndex = len(rewrites) + rewrites.append(op) + + def insertBeforeIndex(self, index, text): + self.insertBefore(self.DEFAULT_PROGRAM_NAME, index, text) + + def insertBeforeToken(self, token, text, program_name=DEFAULT_PROGRAM_NAME): + self.insertBefore(program_name, token.tokenIndex, text) + + def insertBefore(self, program_name, index, text): + op = self.InsertBeforeOp(self.tokens, index, text) + rewrites = self.getProgram(program_name) + op.instructionIndex = len(rewrites) + rewrites.append(op) + + def replaceIndex(self, index, text): + self.replace(self.DEFAULT_PROGRAM_NAME, index, index, text) + + def replaceRange(self, from_idx, to_idx, text): + self.replace(self.DEFAULT_PROGRAM_NAME, from_idx, to_idx, text) + + def replaceSingleToken(self, token, text): + self.replace(self.DEFAULT_PROGRAM_NAME, token.tokenIndex, token.tokenIndex, text) + + def replaceRangeTokens(self, from_token, to_token, text, program_name=DEFAULT_PROGRAM_NAME): + self.replace(program_name, from_token.tokenIndex, to_token.tokenIndex, text) + + def replace(self, program_name, from_idx, to_idx, text): + if any((from_idx > to_idx, from_idx < 0, to_idx < 0, to_idx >= len(self.tokens.tokens))): + raise ValueError( + 'replace: range invalid: {}..{}(size={})'.format(from_idx, to_idx, len(self.tokens.tokens))) + op = self.ReplaceOp(from_idx, to_idx, self.tokens, text) + rewrites = self.getProgram(program_name) + op.instructionIndex = len(rewrites) + rewrites.append(op) + + def deleteToken(self, token): + self.delete(self.DEFAULT_PROGRAM_NAME, token, token) + + def deleteIndex(self, index): + self.delete(self.DEFAULT_PROGRAM_NAME, index, index) + + def delete(self, program_name, from_idx, to_idx): + if isinstance(from_idx, Token): + self.replace(program_name, from_idx.tokenIndex, to_idx.tokenIndex, "") + else: + self.replace(program_name, from_idx, to_idx, "") + + def lastRewriteTokenIndex(self, program_name=DEFAULT_PROGRAM_NAME): + return self.lastRewriteTokenIndexes.get(program_name, -1) + + def setLastRewriteTokenIndex(self, program_name, i): + self.lastRewriteTokenIndexes[program_name] = i + + def getProgram(self, program_name): + return self.programs.setdefault(program_name, []) + + def getDefaultText(self): + return self.getText(self.DEFAULT_PROGRAM_NAME, 0, len(self.tokens.tokens) - 1) + + def getText(self, program_name, start:int, stop:int): + """ + :return: the text in tokens[start, stop](closed interval) + """ + rewrites = self.programs.get(program_name) + + # ensure start/end are in range + if stop > len(self.tokens.tokens) - 1: + stop = len(self.tokens.tokens) - 1 + if start < 0: + start = 0 + + # if no instructions to execute + if not rewrites: return self.tokens.getText(start, stop) + buf = StringIO() + indexToOp = self._reduceToSingleOperationPerIndex(rewrites) + i = start + while all((i <= stop, i < len(self.tokens.tokens))): + op = indexToOp.pop(i, None) + token = self.tokens.get(i) + if op is None: + if token.type != Token.EOF: buf.write(token.text) + i += 1 + else: + i = op.execute(buf) + + if stop == len(self.tokens.tokens)-1: + for op in indexToOp.values(): + if op.index >= len(self.tokens.tokens)-1: buf.write(op.text) + + return buf.getvalue() + + def _reduceToSingleOperationPerIndex(self, rewrites): + # Walk replaces + for i, rop in enumerate(rewrites): + if any((rop is None, not isinstance(rop, TokenStreamRewriter.ReplaceOp))): + continue + # Wipe prior inserts within range + inserts = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.InsertBeforeOp)] + for iop in inserts: + if iop.index == rop.index: + rewrites[iop.instructionIndex] = None + rop.text = '{}{}'.format(iop.text, rop.text) + elif all((iop.index > rop.index, iop.index <= rop.last_index)): + rewrites[iop.instructionIndex] = None + + # Drop any prior replaces contained within + prevReplaces = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.ReplaceOp)] + for prevRop in prevReplaces: + if all((prevRop.index >= rop.index, prevRop.last_index <= rop.last_index)): + rewrites[prevRop.instructionIndex] = None + continue + isDisjoint = any((prevRop.last_indexrop.last_index)) + if all((prevRop.text is None, rop.text is None, not isDisjoint)): + rewrites[prevRop.instructionIndex] = None + rop.index = min(prevRop.index, rop.index) + rop.last_index = min(prevRop.last_index, rop.last_index) + print('New rop {}'.format(rop)) + elif (not(isDisjoint)): + raise ValueError("replace op boundaries of {} overlap with previous {}".format(rop, prevRop)) + + # Walk inserts + for i, iop in enumerate(rewrites): + if any((iop is None, not isinstance(iop, TokenStreamRewriter.InsertBeforeOp))): + continue + prevInserts = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.InsertBeforeOp)] + for prev_index, prevIop in enumerate(prevInserts): + if prevIop.index == iop.index and type(prevIop) is TokenStreamRewriter.InsertBeforeOp: + iop.text += prevIop.text + rewrites[prev_index] = None + elif prevIop.index == iop.index and type(prevIop) is TokenStreamRewriter.InsertAfterOp: + iop.text = prevIop.text + iop.text + rewrites[prev_index] = None + # look for replaces where iop.index is in range; error + prevReplaces = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.ReplaceOp)] + for rop in prevReplaces: + if iop.index == rop.index: + rop.text = iop.text + rop.text + rewrites[i] = None + continue + if all((iop.index >= rop.index, iop.index <= rop.last_index)): + raise ValueError("insert op {} within boundaries of previous {}".format(iop, rop)) + + reduced = {} + for i, op in enumerate(rewrites): + if op is None: continue + if reduced.get(op.index): raise ValueError('should be only one op per index') + reduced[op.index] = op + + return reduced + + class RewriteOperation(object): + __slots__ = ('tokens', 'index', 'text', 'instructionIndex') + + def __init__(self, tokens, index, text=""): + """ + :type tokens: CommonTokenStream + :param tokens: + :param index: + :param text: + :return: + """ + self.tokens = tokens + self.index = index + self.text = text + self.instructionIndex = 0 + + def execute(self, buf): + """ + :type buf: StringIO.StringIO + :param buf: + :return: + """ + return self.index + + def __str__(self): + return '<{}@{}:"{}">'.format(self.__class__.__name__, self.tokens.get(self.index), self.text) + + class InsertBeforeOp(RewriteOperation): + + def __init__(self, tokens, index, text=""): + super(TokenStreamRewriter.InsertBeforeOp, self).__init__(tokens, index, text) + + def execute(self, buf): + buf.write(self.text) + if self.tokens.get(self.index).type != Token.EOF: + buf.write(self.tokens.get(self.index).text) + return self.index + 1 + + class InsertAfterOp(InsertBeforeOp): + pass + + class ReplaceOp(RewriteOperation): + __slots__ = 'last_index' + + def __init__(self, from_idx, to_idx, tokens, text): + super(TokenStreamRewriter.ReplaceOp, self).__init__(tokens, from_idx, text) + self.last_index = to_idx + + def execute(self, buf): + if self.text: + buf.write(self.text) + return self.last_index + 1 + + def __str__(self): + if self.text: + return ''.format(self.tokens.get(self.index), self.tokens.get(self.last_index), + self.text) diff --git a/lib/python3.12/site-packages/antlr4/Utils.py b/lib/python3.12/site-packages/antlr4/Utils.py new file mode 100644 index 0000000000000000000000000000000000000000..88c870dae63ef4bda74e25aea47befb75be34a5d --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/Utils.py @@ -0,0 +1,33 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +from io import StringIO + +def str_list(val): + with StringIO() as buf: + buf.write('[') + first = True + for item in val: + if not first: + buf.write(', ') + buf.write(str(item)) + first = False + buf.write(']') + return buf.getvalue() + +def escapeWhitespace(s:str, escapeSpaces:bool): + with StringIO() as buf: + for c in s: + if c==' ' and escapeSpaces: + buf.write('\u00B7') + elif c=='\t': + buf.write("\\t") + elif c=='\n': + buf.write("\\n") + elif c=='\r': + buf.write("\\r") + else: + buf.write(c) + return buf.getvalue() diff --git a/lib/python3.12/site-packages/antlr4/__init__.py b/lib/python3.12/site-packages/antlr4/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..42027289e7af293a297b538f07f43ca4a566ef62 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/__init__.py @@ -0,0 +1,21 @@ +from antlr4.Token import Token +from antlr4.InputStream import InputStream +from antlr4.FileStream import FileStream +from antlr4.StdinStream import StdinStream +from antlr4.BufferedTokenStream import TokenStream +from antlr4.CommonTokenStream import CommonTokenStream +from antlr4.Lexer import Lexer +from antlr4.Parser import Parser +from antlr4.dfa.DFA import DFA +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNDeserializer import ATNDeserializer +from antlr4.atn.LexerATNSimulator import LexerATNSimulator +from antlr4.atn.ParserATNSimulator import ParserATNSimulator +from antlr4.atn.PredictionMode import PredictionMode +from antlr4.PredictionContext import PredictionContextCache +from antlr4.ParserRuleContext import RuleContext, ParserRuleContext +from antlr4.tree.Tree import ParseTreeListener, ParseTreeVisitor, ParseTreeWalker, TerminalNode, ErrorNode, RuleNode +from antlr4.error.Errors import RecognitionException, IllegalStateException, NoViableAltException +from antlr4.error.ErrorStrategy import BailErrorStrategy +from antlr4.error.DiagnosticErrorListener import DiagnosticErrorListener +from antlr4.Utils import str_list diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/BufferedTokenStream.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/BufferedTokenStream.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9b61c7fbdda1da20f9799ffad59935551e079d0 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/BufferedTokenStream.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/CommonTokenFactory.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/CommonTokenFactory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15f0df0a0614f58b62d9d121cfc9a42cdb92f5c6 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/CommonTokenFactory.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/CommonTokenStream.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/CommonTokenStream.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..094d79ef856fbfb4bedd75bdd1490318871adeb6 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/CommonTokenStream.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/FileStream.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/FileStream.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60c34f39505cd133b94203cbf3d384d94c8b04bd Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/FileStream.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/InputStream.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/InputStream.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee65b5fbf0691c6edbbf6f6af010dec87d75affe Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/InputStream.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/IntervalSet.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/IntervalSet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44747fbd5481f79d1ed6eb2afc0c81a95e93c5cc Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/IntervalSet.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/LL1Analyzer.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/LL1Analyzer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5d7f298c4649e3ec14ee43206235638ece7d7ce Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/LL1Analyzer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/Lexer.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/Lexer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..942cd0a129dbdae32e9f6c7fe2cf036f4b599300 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/Lexer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/ListTokenSource.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/ListTokenSource.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df0367988ac96a7e44cf9cb1253386fd0f41f76e Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/ListTokenSource.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/Parser.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/Parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b859b08d4be73788fedd4e2365d17beecc5c641e Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/Parser.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/ParserInterpreter.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/ParserInterpreter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae2efe0110e6b0c08f75602003293520c311cab0 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/ParserInterpreter.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/ParserRuleContext.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/ParserRuleContext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e1698a4c87c2de72338fc55f1d5765506673a57 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/ParserRuleContext.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/PredictionContext.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/PredictionContext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68f18099cd2f20cedc472ce81394f20cdbfb7f1c Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/PredictionContext.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/Recognizer.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/Recognizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45d183fe4809d86ca0eb753e7996c1542a2c5b2a Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/Recognizer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/RuleContext.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/RuleContext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d6e16bf2b3824bccccff4086b2150ae6e078bf8 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/RuleContext.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/StdinStream.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/StdinStream.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6072ea8fbb6c5a483344a07e8d6936fa9f28dee9 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/StdinStream.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/Token.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/Token.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a2244f6df60f2e2d9198561af9f06035adadf74 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/Token.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/TokenStreamRewriter.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/TokenStreamRewriter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd8f077f5926536e1e18784dbab98943ef471a8d Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/TokenStreamRewriter.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/Utils.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/Utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76d5d1ff6614b3038988e5c3d6cd1820499f7fd5 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/Utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4e315d06b792f112c4503ad7aa57a328efffca6 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/ATN.py b/lib/python3.12/site-packages/antlr4/atn/ATN.py new file mode 100644 index 0000000000000000000000000000000000000000..3f1abe0a4a7faacde5140d5631dfe48a79325cd4 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATN.py @@ -0,0 +1,132 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ +from antlr4.IntervalSet import IntervalSet + +from antlr4.RuleContext import RuleContext + +from antlr4.Token import Token +from antlr4.atn.ATNType import ATNType +from antlr4.atn.ATNState import ATNState, DecisionState + + +class ATN(object): + __slots__ = ( + 'grammarType', 'maxTokenType', 'states', 'decisionToState', + 'ruleToStartState', 'ruleToStopState', 'modeNameToStartState', + 'ruleToTokenType', 'lexerActions', 'modeToStartState' + ) + + INVALID_ALT_NUMBER = 0 + + # Used for runtime deserialization of ATNs from strings#/ + def __init__(self, grammarType:ATNType , maxTokenType:int ): + # The type of the ATN. + self.grammarType = grammarType + # The maximum value for any symbol recognized by a transition in the ATN. + self.maxTokenType = maxTokenType + self.states = [] + # Each subrule/rule is a decision point and we must track them so we + # can go back later and build DFA predictors for them. This includes + # all the rules, subrules, optional blocks, ()+, ()* etc... + self.decisionToState = [] + # Maps from rule index to starting state number. + self.ruleToStartState = [] + # Maps from rule index to stop state number. + self.ruleToStopState = None + self.modeNameToStartState = dict() + # For lexer ATNs, this maps the rule index to the resulting token type. + # For parser ATNs, this maps the rule index to the generated bypass token + # type if the + # {@link ATNDeserializationOptions#isGenerateRuleBypassTransitions} + # deserialization option was specified; otherwise, this is {@code null}. + self.ruleToTokenType = None + # For lexer ATNs, this is an array of {@link LexerAction} objects which may + # be referenced by action transitions in the ATN. + self.lexerActions = None + self.modeToStartState = [] + + # Compute the set of valid tokens that can occur starting in state {@code s}. + # If {@code ctx} is null, the set of tokens will not include what can follow + # the rule surrounding {@code s}. In other words, the set will be + # restricted to tokens reachable staying within {@code s}'s rule. + def nextTokensInContext(self, s:ATNState, ctx:RuleContext): + from antlr4.LL1Analyzer import LL1Analyzer + anal = LL1Analyzer(self) + return anal.LOOK(s, ctx=ctx) + + # Compute the set of valid tokens that can occur starting in {@code s} and + # staying in same rule. {@link Token#EPSILON} is in set if we reach end of + # rule. + def nextTokensNoContext(self, s:ATNState): + if s.nextTokenWithinRule is not None: + return s.nextTokenWithinRule + s.nextTokenWithinRule = self.nextTokensInContext(s, None) + s.nextTokenWithinRule.readonly = True + return s.nextTokenWithinRule + + def nextTokens(self, s:ATNState, ctx:RuleContext = None): + if ctx==None: + return self.nextTokensNoContext(s) + else: + return self.nextTokensInContext(s, ctx) + + def addState(self, state:ATNState): + if state is not None: + state.atn = self + state.stateNumber = len(self.states) + self.states.append(state) + + def removeState(self, state:ATNState): + self.states[state.stateNumber] = None # just free mem, don't shift states in list + + def defineDecisionState(self, s:DecisionState): + self.decisionToState.append(s) + s.decision = len(self.decisionToState)-1 + return s.decision + + def getDecisionState(self, decision:int): + if len(self.decisionToState)==0: + return None + else: + return self.decisionToState[decision] + + # Computes the set of input symbols which could follow ATN state number + # {@code stateNumber} in the specified full {@code context}. This method + # considers the complete parser context, but does not evaluate semantic + # predicates (i.e. all predicates encountered during the calculation are + # assumed true). If a path in the ATN exists from the starting state to the + # {@link RuleStopState} of the outermost context without matching any + # symbols, {@link Token#EOF} is added to the returned set. + # + #

      If {@code context} is {@code null}, it is treated as + # {@link ParserRuleContext#EMPTY}.

      + # + # @param stateNumber the ATN state number + # @param context the full parse context + # @return The set of potentially valid input symbols which could follow the + # specified state in the specified context. + # @throws IllegalArgumentException if the ATN does not contain a state with + # number {@code stateNumber} + #/ + def getExpectedTokens(self, stateNumber:int, ctx:RuleContext ): + if stateNumber < 0 or stateNumber >= len(self.states): + raise Exception("Invalid state number.") + s = self.states[stateNumber] + following = self.nextTokens(s) + if Token.EPSILON not in following: + return following + expected = IntervalSet() + expected.addSet(following) + expected.removeOne(Token.EPSILON) + while (ctx != None and ctx.invokingState >= 0 and Token.EPSILON in following): + invokingState = self.states[ctx.invokingState] + rt = invokingState.transitions[0] + following = self.nextTokens(rt.followState) + expected.addSet(following) + expected.removeOne(Token.EPSILON) + ctx = ctx.parentCtx + if Token.EPSILON in following: + expected.addOne(Token.EOF) + return expected diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNConfig.py b/lib/python3.12/site-packages/antlr4/atn/ATNConfig.py new file mode 100644 index 0000000000000000000000000000000000000000..e008fb2efac45a24b6d9566170bbd227cee252b6 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNConfig.py @@ -0,0 +1,159 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# A tuple: (ATN state, predicted alt, syntactic, semantic context). +# The syntactic context is a graph-structured stack node whose +# path(s) to the root is the rule invocation(s) +# chain used to arrive at the state. The semantic context is +# the tree of semantic predicates encountered before reaching +# an ATN state. +#/ +from io import StringIO +from antlr4.PredictionContext import PredictionContext +from antlr4.atn.ATNState import ATNState, DecisionState +from antlr4.atn.LexerActionExecutor import LexerActionExecutor +from antlr4.atn.SemanticContext import SemanticContext + +# need a forward declaration +ATNConfig = None + +class ATNConfig(object): + __slots__ = ( + 'state', 'alt', 'context', 'semanticContext', 'reachesIntoOuterContext', + 'precedenceFilterSuppressed' + ) + + def __init__(self, state:ATNState=None, alt:int=None, context:PredictionContext=None, semantic:SemanticContext=None, config:ATNConfig=None): + if config is not None: + if state is None: + state = config.state + if alt is None: + alt = config.alt + if context is None: + context = config.context + if semantic is None: + semantic = config.semanticContext + if semantic is None: + semantic = SemanticContext.NONE + # The ATN state associated with this configuration#/ + self.state = state + # What alt (or lexer rule) is predicted by this configuration#/ + self.alt = alt + # The stack of invoking states leading to the rule/states associated + # with this config. We track only those contexts pushed during + # execution of the ATN simulator. + self.context = context + self.semanticContext = semantic + # We cannot execute predicates dependent upon local context unless + # we know for sure we are in the correct context. Because there is + # no way to do this efficiently, we simply cannot evaluate + # dependent predicates unless we are in the rule that initially + # invokes the ATN simulator. + # + # closure() tracks the depth of how far we dip into the + # outer context: depth > 0. Note that it may not be totally + # accurate depth since I don't ever decrement. TODO: make it a boolean then + self.reachesIntoOuterContext = 0 if config is None else config.reachesIntoOuterContext + self.precedenceFilterSuppressed = False if config is None else config.precedenceFilterSuppressed + + # An ATN configuration is equal to another if both have + # the same state, they predict the same alternative, and + # syntactic/semantic contexts are the same. + #/ + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, ATNConfig): + return False + else: + return self.state.stateNumber==other.state.stateNumber \ + and self.alt==other.alt \ + and ((self.context is other.context) or (self.context==other.context)) \ + and self.semanticContext==other.semanticContext \ + and self.precedenceFilterSuppressed==other.precedenceFilterSuppressed + + def __hash__(self): + return hash((self.state.stateNumber, self.alt, self.context, self.semanticContext)) + + def hashCodeForConfigSet(self): + return hash((self.state.stateNumber, self.alt, hash(self.semanticContext))) + + def equalsForConfigSet(self, other): + if self is other: + return True + elif not isinstance(other, ATNConfig): + return False + else: + return self.state.stateNumber==other.state.stateNumber \ + and self.alt==other.alt \ + and self.semanticContext==other.semanticContext + + def __str__(self): + with StringIO() as buf: + buf.write('(') + buf.write(str(self.state)) + buf.write(",") + buf.write(str(self.alt)) + if self.context is not None: + buf.write(",[") + buf.write(str(self.context)) + buf.write("]") + if self.semanticContext is not None and self.semanticContext is not SemanticContext.NONE: + buf.write(",") + buf.write(str(self.semanticContext)) + if self.reachesIntoOuterContext>0: + buf.write(",up=") + buf.write(str(self.reachesIntoOuterContext)) + buf.write(')') + return buf.getvalue() + +# need a forward declaration +LexerATNConfig = None + +class LexerATNConfig(ATNConfig): + __slots__ = ('lexerActionExecutor', 'passedThroughNonGreedyDecision') + + def __init__(self, state:ATNState, alt:int=None, context:PredictionContext=None, semantic:SemanticContext=SemanticContext.NONE, + lexerActionExecutor:LexerActionExecutor=None, config:LexerATNConfig=None): + super().__init__(state=state, alt=alt, context=context, semantic=semantic, config=config) + if config is not None: + if lexerActionExecutor is None: + lexerActionExecutor = config.lexerActionExecutor + # This is the backing field for {@link #getLexerActionExecutor}. + self.lexerActionExecutor = lexerActionExecutor + self.passedThroughNonGreedyDecision = False if config is None else self.checkNonGreedyDecision(config, state) + + def __hash__(self): + return hash((self.state.stateNumber, self.alt, self.context, + self.semanticContext, self.passedThroughNonGreedyDecision, + self.lexerActionExecutor)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerATNConfig): + return False + if self.passedThroughNonGreedyDecision != other.passedThroughNonGreedyDecision: + return False + if not(self.lexerActionExecutor == other.lexerActionExecutor): + return False + return super().__eq__(other) + + + + def hashCodeForConfigSet(self): + return hash(self) + + + + def equalsForConfigSet(self, other): + return self==other + + + + def checkNonGreedyDecision(self, source:LexerATNConfig, target:ATNState): + return source.passedThroughNonGreedyDecision \ + or isinstance(target, DecisionState) and target.nonGreedy diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNConfigSet.py b/lib/python3.12/site-packages/antlr4/atn/ATNConfigSet.py new file mode 100644 index 0000000000000000000000000000000000000000..9e9a512a682431e46a3f5b848420a762af3c7914 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNConfigSet.py @@ -0,0 +1,212 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. + +# +# Specialized {@link Set}{@code <}{@link ATNConfig}{@code >} that can track +# info about the set, with support for combining similar configurations using a +# graph-structured stack. +#/ +from io import StringIO +from functools import reduce +from antlr4.PredictionContext import PredictionContext, merge +from antlr4.Utils import str_list +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNConfig import ATNConfig +from antlr4.atn.SemanticContext import SemanticContext +from antlr4.error.Errors import UnsupportedOperationException, IllegalStateException + +ATNSimulator = None + +class ATNConfigSet(object): + __slots__ = ( + 'configLookup', 'fullCtx', 'readonly', 'configs', 'uniqueAlt', + 'conflictingAlts', 'hasSemanticContext', 'dipsIntoOuterContext', + 'cachedHashCode' + ) + + # + # The reason that we need this is because we don't want the hash map to use + # the standard hash code and equals. We need all configurations with the same + # {@code (s,i,_,semctx)} to be equal. Unfortunately, this key effectively doubles + # the number of objects associated with ATNConfigs. The other solution is to + # use a hash table that lets us specify the equals/hashcode operation. + + def __init__(self, fullCtx:bool=True): + # All configs but hashed by (s, i, _, pi) not including context. Wiped out + # when we go readonly as this set becomes a DFA state. + self.configLookup = dict() + # Indicates that this configuration set is part of a full context + # LL prediction. It will be used to determine how to merge $. With SLL + # it's a wildcard whereas it is not for LL context merge. + self.fullCtx = fullCtx + # Indicates that the set of configurations is read-only. Do not + # allow any code to manipulate the set; DFA states will point at + # the sets and they must not change. This does not protect the other + # fields; in particular, conflictingAlts is set after + # we've made this readonly. + self.readonly = False + # Track the elements as they are added to the set; supports get(i)#/ + self.configs = [] + + # TODO: these fields make me pretty uncomfortable but nice to pack up info together, saves recomputation + # TODO: can we track conflicts as they are added to save scanning configs later? + self.uniqueAlt = 0 + self.conflictingAlts = None + + # Used in parser and lexer. In lexer, it indicates we hit a pred + # while computing a closure operation. Don't make a DFA state from this. + self.hasSemanticContext = False + self.dipsIntoOuterContext = False + + self.cachedHashCode = -1 + + def __iter__(self): + return self.configs.__iter__() + + # Adding a new config means merging contexts with existing configs for + # {@code (s, i, pi, _)}, where {@code s} is the + # {@link ATNConfig#state}, {@code i} is the {@link ATNConfig#alt}, and + # {@code pi} is the {@link ATNConfig#semanticContext}. We use + # {@code (s,i,pi)} as key. + # + #

      This method updates {@link #dipsIntoOuterContext} and + # {@link #hasSemanticContext} when necessary.

      + #/ + def add(self, config:ATNConfig, mergeCache=None): + if self.readonly: + raise Exception("This set is readonly") + if config.semanticContext is not SemanticContext.NONE: + self.hasSemanticContext = True + if config.reachesIntoOuterContext > 0: + self.dipsIntoOuterContext = True + existing = self.getOrAdd(config) + if existing is config: + self.cachedHashCode = -1 + self.configs.append(config) # track order here + return True + # a previous (s,i,pi,_), merge with it and save result + rootIsWildcard = not self.fullCtx + merged = merge(existing.context, config.context, rootIsWildcard, mergeCache) + # no need to check for existing.context, config.context in cache + # since only way to create new graphs is "call rule" and here. + # We cache at both places. + existing.reachesIntoOuterContext = max(existing.reachesIntoOuterContext, config.reachesIntoOuterContext) + # make sure to preserve the precedence filter suppression during the merge + if config.precedenceFilterSuppressed: + existing.precedenceFilterSuppressed = True + existing.context = merged # replace context; no need to alt mapping + return True + + def getOrAdd(self, config:ATNConfig): + h = config.hashCodeForConfigSet() + l = self.configLookup.get(h, None) + if l is not None: + r = next((cfg for cfg in l if config.equalsForConfigSet(cfg)), None) + if r is not None: + return r + if l is None: + l = [config] + self.configLookup[h] = l + else: + l.append(config) + return config + + def getStates(self): + return set(c.state for c in self.configs) + + def getPredicates(self): + return list(cfg.semanticContext for cfg in self.configs if cfg.semanticContext!=SemanticContext.NONE) + + def get(self, i:int): + return self.configs[i] + + def optimizeConfigs(self, interpreter:ATNSimulator): + if self.readonly: + raise IllegalStateException("This set is readonly") + if len(self.configs)==0: + return + for config in self.configs: + config.context = interpreter.getCachedContext(config.context) + + def addAll(self, coll:list): + for c in coll: + self.add(c) + return False + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, ATNConfigSet): + return False + + same = self.configs is not None and \ + self.configs==other.configs and \ + self.fullCtx == other.fullCtx and \ + self.uniqueAlt == other.uniqueAlt and \ + self.conflictingAlts == other.conflictingAlts and \ + self.hasSemanticContext == other.hasSemanticContext and \ + self.dipsIntoOuterContext == other.dipsIntoOuterContext + + return same + + def __hash__(self): + if self.readonly: + if self.cachedHashCode == -1: + self.cachedHashCode = self.hashConfigs() + return self.cachedHashCode + return self.hashConfigs() + + def hashConfigs(self): + return reduce(lambda h, cfg: hash((h, cfg)), self.configs, 0) + + def __len__(self): + return len(self.configs) + + def isEmpty(self): + return len(self.configs)==0 + + def __contains__(self, config): + if self.configLookup is None: + raise UnsupportedOperationException("This method is not implemented for readonly sets.") + h = config.hashCodeForConfigSet() + l = self.configLookup.get(h, None) + if l is not None: + for c in l: + if config.equalsForConfigSet(c): + return True + return False + + def clear(self): + if self.readonly: + raise IllegalStateException("This set is readonly") + self.configs.clear() + self.cachedHashCode = -1 + self.configLookup.clear() + + def setReadonly(self, readonly:bool): + self.readonly = readonly + self.configLookup = None # can't mod, no need for lookup cache + + def __str__(self): + with StringIO() as buf: + buf.write(str_list(self.configs)) + if self.hasSemanticContext: + buf.write(",hasSemanticContext=") + buf.write(str(self.hasSemanticContext)) + if self.uniqueAlt!=ATN.INVALID_ALT_NUMBER: + buf.write(",uniqueAlt=") + buf.write(str(self.uniqueAlt)) + if self.conflictingAlts is not None: + buf.write(",conflictingAlts=") + buf.write(str(self.conflictingAlts)) + if self.dipsIntoOuterContext: + buf.write(",dipsIntoOuterContext") + return buf.getvalue() + + +class OrderedATNConfigSet(ATNConfigSet): + + def __init__(self): + super().__init__() diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNDeserializationOptions.py b/lib/python3.12/site-packages/antlr4/atn/ATNDeserializationOptions.py new file mode 100644 index 0000000000000000000000000000000000000000..69d5437f35437874a60bcc76118f018edbf65fed --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNDeserializationOptions.py @@ -0,0 +1,24 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. + +# need a forward declaration +ATNDeserializationOptions = None + +class ATNDeserializationOptions(object): + __slots__ = ('readonly', 'verifyATN', 'generateRuleBypassTransitions') + + defaultOptions = None + + def __init__(self, copyFrom:ATNDeserializationOptions = None): + self.readonly = False + self.verifyATN = True if copyFrom is None else copyFrom.verifyATN + self.generateRuleBypassTransitions = False if copyFrom is None else copyFrom.generateRuleBypassTransitions + + def __setattr__(self, key, value): + if key!="readonly" and self.readonly: + raise Exception("The object is read only.") + super(type(self), self).__setattr__(key,value) + +ATNDeserializationOptions.defaultOptions = ATNDeserializationOptions() +ATNDeserializationOptions.defaultOptions.readonly = True diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNDeserializer.py b/lib/python3.12/site-packages/antlr4/atn/ATNDeserializer.py new file mode 100644 index 0000000000000000000000000000000000000000..cc100d05a43e10b91390dd47a7b05f2eff6fe23f --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNDeserializer.py @@ -0,0 +1,529 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ +from uuid import UUID +from io import StringIO +from typing import Callable +from antlr4.Token import Token +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNType import ATNType +from antlr4.atn.ATNState import * +from antlr4.atn.Transition import * +from antlr4.atn.LexerAction import * +from antlr4.atn.ATNDeserializationOptions import ATNDeserializationOptions + +# This is the earliest supported serialized UUID. +BASE_SERIALIZED_UUID = UUID("AADB8D7E-AEEF-4415-AD2B-8204D6CF042E") + +# This UUID indicates the serialized ATN contains two sets of +# IntervalSets, where the second set's values are encoded as +# 32-bit integers to support the full Unicode SMP range up to U+10FFFF. +ADDED_UNICODE_SMP = UUID("59627784-3BE5-417A-B9EB-8131A7286089") + +# This list contains all of the currently supported UUIDs, ordered by when +# the feature first appeared in this branch. +SUPPORTED_UUIDS = [ BASE_SERIALIZED_UUID, ADDED_UNICODE_SMP ] + +SERIALIZED_VERSION = 3 + +# This is the current serialized UUID. +SERIALIZED_UUID = ADDED_UNICODE_SMP + +class ATNDeserializer (object): + __slots__ = ('deserializationOptions', 'data', 'pos', 'uuid') + + def __init__(self, options : ATNDeserializationOptions = None): + if options is None: + options = ATNDeserializationOptions.defaultOptions + self.deserializationOptions = options + + # Determines if a particular serialized representation of an ATN supports + # a particular feature, identified by the {@link UUID} used for serializing + # the ATN at the time the feature was first introduced. + # + # @param feature The {@link UUID} marking the first time the feature was + # supported in the serialized ATN. + # @param actualUuid The {@link UUID} of the actual serialized ATN which is + # currently being deserialized. + # @return {@code true} if the {@code actualUuid} value represents a + # serialized ATN at or after the feature identified by {@code feature} was + # introduced; otherwise, {@code false}. + + def isFeatureSupported(self, feature : UUID , actualUuid : UUID ): + idx1 = SUPPORTED_UUIDS.index(feature) + if idx1<0: + return False + idx2 = SUPPORTED_UUIDS.index(actualUuid) + return idx2 >= idx1 + + def deserialize(self, data : str): + self.reset(data) + self.checkVersion() + self.checkUUID() + atn = self.readATN() + self.readStates(atn) + self.readRules(atn) + self.readModes(atn) + sets = [] + # First, read all sets with 16-bit Unicode code points <= U+FFFF. + self.readSets(atn, sets, self.readInt) + # Next, if the ATN was serialized with the Unicode SMP feature, + # deserialize sets with 32-bit arguments <= U+10FFFF. + if self.isFeatureSupported(ADDED_UNICODE_SMP, self.uuid): + self.readSets(atn, sets, self.readInt32) + self.readEdges(atn, sets) + self.readDecisions(atn) + self.readLexerActions(atn) + self.markPrecedenceDecisions(atn) + self.verifyATN(atn) + if self.deserializationOptions.generateRuleBypassTransitions \ + and atn.grammarType == ATNType.PARSER: + self.generateRuleBypassTransitions(atn) + # re-verify after modification + self.verifyATN(atn) + return atn + + def reset(self, data:str): + def adjust(c): + v = ord(c) + return v-2 if v>1 else v + 65533 + temp = [ adjust(c) for c in data ] + # don't adjust the first value since that's the version number + temp[0] = ord(data[0]) + self.data = temp + self.pos = 0 + + def checkVersion(self): + version = self.readInt() + if version != SERIALIZED_VERSION: + raise Exception("Could not deserialize ATN with version " + str(version) + " (expected " + str(SERIALIZED_VERSION) + ").") + + def checkUUID(self): + uuid = self.readUUID() + if not uuid in SUPPORTED_UUIDS: + raise Exception("Could not deserialize ATN with UUID: " + str(uuid) + \ + " (expected " + str(SERIALIZED_UUID) + " or a legacy UUID).", uuid, SERIALIZED_UUID) + self.uuid = uuid + + def readATN(self): + idx = self.readInt() + grammarType = ATNType.fromOrdinal(idx) + maxTokenType = self.readInt() + return ATN(grammarType, maxTokenType) + + def readStates(self, atn:ATN): + loopBackStateNumbers = [] + endStateNumbers = [] + nstates = self.readInt() + for i in range(0, nstates): + stype = self.readInt() + # ignore bad type of states + if stype==ATNState.INVALID_TYPE: + atn.addState(None) + continue + ruleIndex = self.readInt() + if ruleIndex == 0xFFFF: + ruleIndex = -1 + + s = self.stateFactory(stype, ruleIndex) + if stype == ATNState.LOOP_END: # special case + loopBackStateNumber = self.readInt() + loopBackStateNumbers.append((s, loopBackStateNumber)) + elif isinstance(s, BlockStartState): + endStateNumber = self.readInt() + endStateNumbers.append((s, endStateNumber)) + + atn.addState(s) + + # delay the assignment of loop back and end states until we know all the state instances have been initialized + for pair in loopBackStateNumbers: + pair[0].loopBackState = atn.states[pair[1]] + + for pair in endStateNumbers: + pair[0].endState = atn.states[pair[1]] + + numNonGreedyStates = self.readInt() + for i in range(0, numNonGreedyStates): + stateNumber = self.readInt() + atn.states[stateNumber].nonGreedy = True + + numPrecedenceStates = self.readInt() + for i in range(0, numPrecedenceStates): + stateNumber = self.readInt() + atn.states[stateNumber].isPrecedenceRule = True + + def readRules(self, atn:ATN): + nrules = self.readInt() + if atn.grammarType == ATNType.LEXER: + atn.ruleToTokenType = [0] * nrules + + atn.ruleToStartState = [0] * nrules + for i in range(0, nrules): + s = self.readInt() + startState = atn.states[s] + atn.ruleToStartState[i] = startState + if atn.grammarType == ATNType.LEXER: + tokenType = self.readInt() + if tokenType == 0xFFFF: + tokenType = Token.EOF + + atn.ruleToTokenType[i] = tokenType + + atn.ruleToStopState = [0] * nrules + for state in atn.states: + if not isinstance(state, RuleStopState): + continue + atn.ruleToStopState[state.ruleIndex] = state + atn.ruleToStartState[state.ruleIndex].stopState = state + + def readModes(self, atn:ATN): + nmodes = self.readInt() + for i in range(0, nmodes): + s = self.readInt() + atn.modeToStartState.append(atn.states[s]) + + def readSets(self, atn:ATN, sets:list, readUnicode:Callable[[], int]): + m = self.readInt() + for i in range(0, m): + iset = IntervalSet() + sets.append(iset) + n = self.readInt() + containsEof = self.readInt() + if containsEof!=0: + iset.addOne(-1) + for j in range(0, n): + i1 = readUnicode() + i2 = readUnicode() + iset.addRange(range(i1, i2 + 1)) # range upper limit is exclusive + + def readEdges(self, atn:ATN, sets:list): + nedges = self.readInt() + for i in range(0, nedges): + src = self.readInt() + trg = self.readInt() + ttype = self.readInt() + arg1 = self.readInt() + arg2 = self.readInt() + arg3 = self.readInt() + trans = self.edgeFactory(atn, ttype, src, trg, arg1, arg2, arg3, sets) + srcState = atn.states[src] + srcState.addTransition(trans) + + # edges for rule stop states can be derived, so they aren't serialized + for state in atn.states: + for i in range(0, len(state.transitions)): + t = state.transitions[i] + if not isinstance(t, RuleTransition): + continue + outermostPrecedenceReturn = -1 + if atn.ruleToStartState[t.target.ruleIndex].isPrecedenceRule: + if t.precedence == 0: + outermostPrecedenceReturn = t.target.ruleIndex + trans = EpsilonTransition(t.followState, outermostPrecedenceReturn) + atn.ruleToStopState[t.target.ruleIndex].addTransition(trans) + + for state in atn.states: + if isinstance(state, BlockStartState): + # we need to know the end state to set its start state + if state.endState is None: + raise Exception("IllegalState") + # block end states can only be associated to a single block start state + if state.endState.startState is not None: + raise Exception("IllegalState") + state.endState.startState = state + + if isinstance(state, PlusLoopbackState): + for i in range(0, len(state.transitions)): + target = state.transitions[i].target + if isinstance(target, PlusBlockStartState): + target.loopBackState = state + elif isinstance(state, StarLoopbackState): + for i in range(0, len(state.transitions)): + target = state.transitions[i].target + if isinstance(target, StarLoopEntryState): + target.loopBackState = state + + def readDecisions(self, atn:ATN): + ndecisions = self.readInt() + for i in range(0, ndecisions): + s = self.readInt() + decState = atn.states[s] + atn.decisionToState.append(decState) + decState.decision = i + + def readLexerActions(self, atn:ATN): + if atn.grammarType == ATNType.LEXER: + count = self.readInt() + atn.lexerActions = [ None ] * count + for i in range(0, count): + actionType = self.readInt() + data1 = self.readInt() + if data1 == 0xFFFF: + data1 = -1 + data2 = self.readInt() + if data2 == 0xFFFF: + data2 = -1 + lexerAction = self.lexerActionFactory(actionType, data1, data2) + atn.lexerActions[i] = lexerAction + + def generateRuleBypassTransitions(self, atn:ATN): + + count = len(atn.ruleToStartState) + atn.ruleToTokenType = [ 0 ] * count + for i in range(0, count): + atn.ruleToTokenType[i] = atn.maxTokenType + i + 1 + + for i in range(0, count): + self.generateRuleBypassTransition(atn, i) + + def generateRuleBypassTransition(self, atn:ATN, idx:int): + + bypassStart = BasicBlockStartState() + bypassStart.ruleIndex = idx + atn.addState(bypassStart) + + bypassStop = BlockEndState() + bypassStop.ruleIndex = idx + atn.addState(bypassStop) + + bypassStart.endState = bypassStop + atn.defineDecisionState(bypassStart) + + bypassStop.startState = bypassStart + + excludeTransition = None + + if atn.ruleToStartState[idx].isPrecedenceRule: + # wrap from the beginning of the rule to the StarLoopEntryState + endState = None + for state in atn.states: + if self.stateIsEndStateFor(state, idx): + endState = state + excludeTransition = state.loopBackState.transitions[0] + break + + if excludeTransition is None: + raise Exception("Couldn't identify final state of the precedence rule prefix section.") + + else: + + endState = atn.ruleToStopState[idx] + + # all non-excluded transitions that currently target end state need to target blockEnd instead + for state in atn.states: + for transition in state.transitions: + if transition == excludeTransition: + continue + if transition.target == endState: + transition.target = bypassStop + + # all transitions leaving the rule start state need to leave blockStart instead + ruleToStartState = atn.ruleToStartState[idx] + count = len(ruleToStartState.transitions) + while count > 0: + bypassStart.addTransition(ruleToStartState.transitions[count-1]) + del ruleToStartState.transitions[-1] + + # link the new states + atn.ruleToStartState[idx].addTransition(EpsilonTransition(bypassStart)) + bypassStop.addTransition(EpsilonTransition(endState)) + + matchState = BasicState() + atn.addState(matchState) + matchState.addTransition(AtomTransition(bypassStop, atn.ruleToTokenType[idx])) + bypassStart.addTransition(EpsilonTransition(matchState)) + + + def stateIsEndStateFor(self, state:ATNState, idx:int): + if state.ruleIndex != idx: + return None + if not isinstance(state, StarLoopEntryState): + return None + + maybeLoopEndState = state.transitions[len(state.transitions) - 1].target + if not isinstance(maybeLoopEndState, LoopEndState): + return None + + if maybeLoopEndState.epsilonOnlyTransitions and \ + isinstance(maybeLoopEndState.transitions[0].target, RuleStopState): + return state + else: + return None + + + # + # Analyze the {@link StarLoopEntryState} states in the specified ATN to set + # the {@link StarLoopEntryState#isPrecedenceDecision} field to the + # correct value. + # + # @param atn The ATN. + # + def markPrecedenceDecisions(self, atn:ATN): + for state in atn.states: + if not isinstance(state, StarLoopEntryState): + continue + + # We analyze the ATN to determine if this ATN decision state is the + # decision for the closure block that determines whether a + # precedence rule should continue or complete. + # + if atn.ruleToStartState[state.ruleIndex].isPrecedenceRule: + maybeLoopEndState = state.transitions[len(state.transitions) - 1].target + if isinstance(maybeLoopEndState, LoopEndState): + if maybeLoopEndState.epsilonOnlyTransitions and \ + isinstance(maybeLoopEndState.transitions[0].target, RuleStopState): + state.isPrecedenceDecision = True + + def verifyATN(self, atn:ATN): + if not self.deserializationOptions.verifyATN: + return + # verify assumptions + for state in atn.states: + if state is None: + continue + + self.checkCondition(state.epsilonOnlyTransitions or len(state.transitions) <= 1) + + if isinstance(state, PlusBlockStartState): + self.checkCondition(state.loopBackState is not None) + + if isinstance(state, StarLoopEntryState): + self.checkCondition(state.loopBackState is not None) + self.checkCondition(len(state.transitions) == 2) + + if isinstance(state.transitions[0].target, StarBlockStartState): + self.checkCondition(isinstance(state.transitions[1].target, LoopEndState)) + self.checkCondition(not state.nonGreedy) + elif isinstance(state.transitions[0].target, LoopEndState): + self.checkCondition(isinstance(state.transitions[1].target, StarBlockStartState)) + self.checkCondition(state.nonGreedy) + else: + raise Exception("IllegalState") + + if isinstance(state, StarLoopbackState): + self.checkCondition(len(state.transitions) == 1) + self.checkCondition(isinstance(state.transitions[0].target, StarLoopEntryState)) + + if isinstance(state, LoopEndState): + self.checkCondition(state.loopBackState is not None) + + if isinstance(state, RuleStartState): + self.checkCondition(state.stopState is not None) + + if isinstance(state, BlockStartState): + self.checkCondition(state.endState is not None) + + if isinstance(state, BlockEndState): + self.checkCondition(state.startState is not None) + + if isinstance(state, DecisionState): + self.checkCondition(len(state.transitions) <= 1 or state.decision >= 0) + else: + self.checkCondition(len(state.transitions) <= 1 or isinstance(state, RuleStopState)) + + def checkCondition(self, condition:bool, message=None): + if not condition: + if message is None: + message = "IllegalState" + raise Exception(message) + + def readInt(self): + i = self.data[self.pos] + self.pos += 1 + return i + + def readInt32(self): + low = self.readInt() + high = self.readInt() + return low | (high << 16) + + def readLong(self): + low = self.readInt32() + high = self.readInt32() + return (low & 0x00000000FFFFFFFF) | (high << 32) + + def readUUID(self): + low = self.readLong() + high = self.readLong() + allBits = (low & 0xFFFFFFFFFFFFFFFF) | (high << 64) + return UUID(int=allBits) + + edgeFactories = [ lambda args : None, + lambda atn, src, trg, arg1, arg2, arg3, sets, target : EpsilonTransition(target), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + RangeTransition(target, Token.EOF, arg2) if arg3 != 0 else RangeTransition(target, arg1, arg2), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + RuleTransition(atn.states[arg1], arg2, arg3, target), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + PredicateTransition(target, arg1, arg2, arg3 != 0), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + AtomTransition(target, Token.EOF) if arg3 != 0 else AtomTransition(target, arg1), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + ActionTransition(target, arg1, arg2, arg3 != 0), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + SetTransition(target, sets[arg1]), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + NotSetTransition(target, sets[arg1]), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + WildcardTransition(target), + lambda atn, src, trg, arg1, arg2, arg3, sets, target : \ + PrecedencePredicateTransition(target, arg1) + ] + + def edgeFactory(self, atn:ATN, type:int, src:int, trg:int, arg1:int, arg2:int, arg3:int, sets:list): + target = atn.states[trg] + if type > len(self.edgeFactories) or self.edgeFactories[type] is None: + raise Exception("The specified transition type: " + str(type) + " is not valid.") + else: + return self.edgeFactories[type](atn, src, trg, arg1, arg2, arg3, sets, target) + + stateFactories = [ lambda : None, + lambda : BasicState(), + lambda : RuleStartState(), + lambda : BasicBlockStartState(), + lambda : PlusBlockStartState(), + lambda : StarBlockStartState(), + lambda : TokensStartState(), + lambda : RuleStopState(), + lambda : BlockEndState(), + lambda : StarLoopbackState(), + lambda : StarLoopEntryState(), + lambda : PlusLoopbackState(), + lambda : LoopEndState() + ] + + def stateFactory(self, type:int, ruleIndex:int): + if type> len(self.stateFactories) or self.stateFactories[type] is None: + raise Exception("The specified state type " + str(type) + " is not valid.") + else: + s = self.stateFactories[type]() + if s is not None: + s.ruleIndex = ruleIndex + return s + + CHANNEL = 0 #The type of a {@link LexerChannelAction} action. + CUSTOM = 1 #The type of a {@link LexerCustomAction} action. + MODE = 2 #The type of a {@link LexerModeAction} action. + MORE = 3 #The type of a {@link LexerMoreAction} action. + POP_MODE = 4 #The type of a {@link LexerPopModeAction} action. + PUSH_MODE = 5 #The type of a {@link LexerPushModeAction} action. + SKIP = 6 #The type of a {@link LexerSkipAction} action. + TYPE = 7 #The type of a {@link LexerTypeAction} action. + + actionFactories = [ lambda data1, data2: LexerChannelAction(data1), + lambda data1, data2: LexerCustomAction(data1, data2), + lambda data1, data2: LexerModeAction(data1), + lambda data1, data2: LexerMoreAction.INSTANCE, + lambda data1, data2: LexerPopModeAction.INSTANCE, + lambda data1, data2: LexerPushModeAction(data1), + lambda data1, data2: LexerSkipAction.INSTANCE, + lambda data1, data2: LexerTypeAction(data1) + ] + + def lexerActionFactory(self, type:int, data1:int, data2:int): + + if type > len(self.actionFactories) or self.actionFactories[type] is None: + raise Exception("The specified lexer action type " + str(type) + " is not valid.") + else: + return self.actionFactories[type](data1, data2) diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNSimulator.py b/lib/python3.12/site-packages/antlr4/atn/ATNSimulator.py new file mode 100644 index 0000000000000000000000000000000000000000..4f6f53f488fa2b731fb39ceecf350b0cb43d1c39 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNSimulator.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ +from antlr4.PredictionContext import PredictionContextCache, PredictionContext, getCachedPredictionContext +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNConfigSet import ATNConfigSet +from antlr4.dfa.DFAState import DFAState + + +class ATNSimulator(object): + __slots__ = ('atn', 'sharedContextCache', '__dict__') + + # Must distinguish between missing edge and edge we know leads nowhere#/ + ERROR = DFAState(configs=ATNConfigSet()) + ERROR.stateNumber = 0x7FFFFFFF + + # The context cache maps all PredictionContext objects that are == + # to a single cached copy. This cache is shared across all contexts + # in all ATNConfigs in all DFA states. We rebuild each ATNConfigSet + # to use only cached nodes/graphs in addDFAState(). We don't want to + # fill this during closure() since there are lots of contexts that + # pop up but are not used ever again. It also greatly slows down closure(). + # + #

      This cache makes a huge difference in memory and a little bit in speed. + # For the Java grammar on java.*, it dropped the memory requirements + # at the end from 25M to 16M. We don't store any of the full context + # graphs in the DFA because they are limited to local context only, + # but apparently there's a lot of repetition there as well. We optimize + # the config contexts before storing the config set in the DFA states + # by literally rebuilding them with cached subgraphs only.

      + # + #

      I tried a cache for use during closure operations, that was + # whacked after each adaptivePredict(). It cost a little bit + # more time I think and doesn't save on the overall footprint + # so it's not worth the complexity.

      + #/ + def __init__(self, atn:ATN, sharedContextCache:PredictionContextCache): + self.atn = atn + self.sharedContextCache = sharedContextCache + + def getCachedContext(self, context:PredictionContext): + if self.sharedContextCache is None: + return context + visited = dict() + return getCachedPredictionContext(context, self.sharedContextCache, visited) diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNState.py b/lib/python3.12/site-packages/antlr4/atn/ATNState.py new file mode 100644 index 0000000000000000000000000000000000000000..fbf6a7b9442905c992bb1a88ce343a8a9d63f5b7 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNState.py @@ -0,0 +1,264 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# The following images show the relation of states and +# {@link ATNState#transitions} for various grammar constructs. +# +#
        +# +#
      • Solid edges marked with an ε indicate a required +# {@link EpsilonTransition}.
      • +# +#
      • Dashed edges indicate locations where any transition derived from +# {@link Transition} might appear.
      • +# +#
      • Dashed nodes are place holders for either a sequence of linked +# {@link BasicState} states or the inclusion of a block representing a nested +# construct in one of the forms below.
      • +# +#
      • Nodes showing multiple outgoing alternatives with a {@code ...} support +# any number of alternatives (one or more). Nodes without the {@code ...} only +# support the exact number of alternatives shown in the diagram.
      • +# +#
      +# +#

      Basic Blocks

      +# +#

      Rule

      +# +# +# +#

      Block of 1 or more alternatives

      +# +# +# +#

      Greedy Loops

      +# +#

      Greedy Closure: {@code (...)*}

      +# +# +# +#

      Greedy Positive Closure: {@code (...)+}

      +# +# +# +#

      Greedy Optional: {@code (...)?}

      +# +# +# +#

      Non-Greedy Loops

      +# +#

      Non-Greedy Closure: {@code (...)*?}

      +# +# +# +#

      Non-Greedy Positive Closure: {@code (...)+?}

      +# +# +# +#

      Non-Greedy Optional: {@code (...)??}

      +# +# +# + +from antlr4.atn.Transition import Transition + +INITIAL_NUM_TRANSITIONS = 4 + +class ATNState(object): + __slots__ = ( + 'atn', 'stateNumber', 'stateType', 'ruleIndex', 'epsilonOnlyTransitions', + 'transitions', 'nextTokenWithinRule', + ) + + # constants for serialization + INVALID_TYPE = 0 + BASIC = 1 + RULE_START = 2 + BLOCK_START = 3 + PLUS_BLOCK_START = 4 + STAR_BLOCK_START = 5 + TOKEN_START = 6 + RULE_STOP = 7 + BLOCK_END = 8 + STAR_LOOP_BACK = 9 + STAR_LOOP_ENTRY = 10 + PLUS_LOOP_BACK = 11 + LOOP_END = 12 + + serializationNames = [ + "INVALID", + "BASIC", + "RULE_START", + "BLOCK_START", + "PLUS_BLOCK_START", + "STAR_BLOCK_START", + "TOKEN_START", + "RULE_STOP", + "BLOCK_END", + "STAR_LOOP_BACK", + "STAR_LOOP_ENTRY", + "PLUS_LOOP_BACK", + "LOOP_END" ] + + INVALID_STATE_NUMBER = -1 + + def __init__(self): + # Which ATN are we in? + self.atn = None + self.stateNumber = ATNState.INVALID_STATE_NUMBER + self.stateType = None + self.ruleIndex = 0 # at runtime, we don't have Rule objects + self.epsilonOnlyTransitions = False + # Track the transitions emanating from this ATN state. + self.transitions = [] + # Used to cache lookahead during parsing, not used during construction + self.nextTokenWithinRule = None + + def __hash__(self): + return self.stateNumber + + def __eq__(self, other): + return isinstance(other, ATNState) and self.stateNumber==other.stateNumber + + def onlyHasEpsilonTransitions(self): + return self.epsilonOnlyTransitions + + def isNonGreedyExitState(self): + return False + + def __str__(self): + return str(self.stateNumber) + + def addTransition(self, trans:Transition, index:int=-1): + if len(self.transitions)==0: + self.epsilonOnlyTransitions = trans.isEpsilon + elif self.epsilonOnlyTransitions != trans.isEpsilon: + self.epsilonOnlyTransitions = False + # TODO System.err.format(Locale.getDefault(), "ATN state %d has both epsilon and non-epsilon transitions.\n", stateNumber); + if index==-1: + self.transitions.append(trans) + else: + self.transitions.insert(index, trans) + +class BasicState(ATNState): + + def __init__(self): + super().__init__() + self.stateType = self.BASIC + + +class DecisionState(ATNState): + __slots__ = ('decision', 'nonGreedy') + def __init__(self): + super().__init__() + self.decision = -1 + self.nonGreedy = False + +# The start of a regular {@code (...)} block. +class BlockStartState(DecisionState): + __slots__ = 'endState' + + def __init__(self): + super().__init__() + self.endState = None + +class BasicBlockStartState(BlockStartState): + + def __init__(self): + super().__init__() + self.stateType = self.BLOCK_START + +# Terminal node of a simple {@code (a|b|c)} block. +class BlockEndState(ATNState): + __slots__ = 'startState' + + def __init__(self): + super().__init__() + self.stateType = self.BLOCK_END + self.startState = None + +# The last node in the ATN for a rule, unless that rule is the start symbol. +# In that case, there is one transition to EOF. Later, we might encode +# references to all calls to this rule to compute FOLLOW sets for +# error handling. +# +class RuleStopState(ATNState): + + def __init__(self): + super().__init__() + self.stateType = self.RULE_STOP + +class RuleStartState(ATNState): + __slots__ = ('stopState', 'isPrecedenceRule') + + def __init__(self): + super().__init__() + self.stateType = self.RULE_START + self.stopState = None + self.isPrecedenceRule = False + +# Decision state for {@code A+} and {@code (A|B)+}. It has two transitions: +# one to the loop back to start of the block and one to exit. +# +class PlusLoopbackState(DecisionState): + + def __init__(self): + super().__init__() + self.stateType = self.PLUS_LOOP_BACK + +# Start of {@code (A|B|...)+} loop. Technically a decision state, but +# we don't use for code generation; somebody might need it, so I'm defining +# it for completeness. In reality, the {@link PlusLoopbackState} node is the +# real decision-making note for {@code A+}. +# +class PlusBlockStartState(BlockStartState): + __slots__ = 'loopBackState' + + def __init__(self): + super().__init__() + self.stateType = self.PLUS_BLOCK_START + self.loopBackState = None + +# The block that begins a closure loop. +class StarBlockStartState(BlockStartState): + + def __init__(self): + super().__init__() + self.stateType = self.STAR_BLOCK_START + +class StarLoopbackState(ATNState): + + def __init__(self): + super().__init__() + self.stateType = self.STAR_LOOP_BACK + + +class StarLoopEntryState(DecisionState): + __slots__ = ('loopBackState', 'isPrecedenceDecision') + + def __init__(self): + super().__init__() + self.stateType = self.STAR_LOOP_ENTRY + self.loopBackState = None + # Indicates whether this state can benefit from a precedence DFA during SLL decision making. + self.isPrecedenceDecision = None + +# Mark the end of a * or + loop. +class LoopEndState(ATNState): + __slots__ = 'loopBackState' + + def __init__(self): + super().__init__() + self.stateType = self.LOOP_END + self.loopBackState = None + +# The Tokens rule start state linking to each lexer rule start state */ +class TokensStartState(DecisionState): + + def __init__(self): + super().__init__() + self.stateType = self.TOKEN_START diff --git a/lib/python3.12/site-packages/antlr4/atn/ATNType.py b/lib/python3.12/site-packages/antlr4/atn/ATNType.py new file mode 100644 index 0000000000000000000000000000000000000000..cc0d4d925c6babee1caeee08f5e32fe4a1d6d0d4 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ATNType.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +from enum import IntEnum + +# Represents the type of recognizer an ATN applies to. + +class ATNType(IntEnum): + + LEXER = 0 + PARSER = 1 + + @classmethod + def fromOrdinal(cls, i:int): + return cls._value2member_map_[i] diff --git a/lib/python3.12/site-packages/antlr4/atn/LexerATNSimulator.py b/lib/python3.12/site-packages/antlr4/atn/LexerATNSimulator.py new file mode 100644 index 0000000000000000000000000000000000000000..71201ff5f989820d340aac8672ab31db8579b55c --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/LexerATNSimulator.py @@ -0,0 +1,570 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# When we hit an accept state in either the DFA or the ATN, we +# have to notify the character stream to start buffering characters +# via {@link IntStream#mark} and record the current state. The current sim state +# includes the current index into the input, the current line, +# and current character position in that line. Note that the Lexer is +# tracking the starting line and characterization of the token. These +# variables track the "state" of the simulator when it hits an accept state. +# +#

      We track these variables separately for the DFA and ATN simulation +# because the DFA simulation often has to fail over to the ATN +# simulation. If the ATN simulation fails, we need the DFA to fall +# back to its previously accepted state, if any. If the ATN succeeds, +# then the ATN does the accept and the DFA simulator that invoked it +# can simply return the predicted token type.

      +#/ + +from antlr4.PredictionContext import PredictionContextCache, SingletonPredictionContext, PredictionContext +from antlr4.InputStream import InputStream +from antlr4.Token import Token +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNConfig import LexerATNConfig +from antlr4.atn.ATNSimulator import ATNSimulator +from antlr4.atn.ATNConfigSet import ATNConfigSet, OrderedATNConfigSet +from antlr4.atn.ATNState import RuleStopState, ATNState +from antlr4.atn.LexerActionExecutor import LexerActionExecutor +from antlr4.atn.Transition import Transition +from antlr4.dfa.DFAState import DFAState +from antlr4.error.Errors import LexerNoViableAltException, UnsupportedOperationException + +class SimState(object): + __slots__ = ('index', 'line', 'column', 'dfaState') + + def __init__(self): + self.reset() + + def reset(self): + self.index = -1 + self.line = 0 + self.column = -1 + self.dfaState = None + +# need forward declaration +Lexer = None +LexerATNSimulator = None + +class LexerATNSimulator(ATNSimulator): + __slots__ = ( + 'decisionToDFA', 'recog', 'startIndex', 'line', 'column', 'mode', + 'DEFAULT_MODE', 'MAX_CHAR_VALUE', 'prevAccept' + ) + + debug = False + dfa_debug = False + + MIN_DFA_EDGE = 0 + MAX_DFA_EDGE = 127 # forces unicode to stay in ATN + + ERROR = None + + def __init__(self, recog:Lexer, atn:ATN, decisionToDFA:list, sharedContextCache:PredictionContextCache): + super().__init__(atn, sharedContextCache) + self.decisionToDFA = decisionToDFA + self.recog = recog + # The current token's starting index into the character stream. + # Shared across DFA to ATN simulation in case the ATN fails and the + # DFA did not have a previous accept state. In this case, we use the + # ATN-generated exception object. + self.startIndex = -1 + # line number 1..n within the input#/ + self.line = 1 + # The index of the character relative to the beginning of the line 0..n-1#/ + self.column = 0 + from antlr4.Lexer import Lexer + self.mode = Lexer.DEFAULT_MODE + # Cache Lexer properties to avoid further imports + self.DEFAULT_MODE = Lexer.DEFAULT_MODE + self.MAX_CHAR_VALUE = Lexer.MAX_CHAR_VALUE + # Used during DFA/ATN exec to record the most recent accept configuration info + self.prevAccept = SimState() + + + def copyState(self, simulator:LexerATNSimulator ): + self.column = simulator.column + self.line = simulator.line + self.mode = simulator.mode + self.startIndex = simulator.startIndex + + def match(self, input:InputStream , mode:int): + self.mode = mode + mark = input.mark() + try: + self.startIndex = input.index + self.prevAccept.reset() + dfa = self.decisionToDFA[mode] + if dfa.s0 is None: + return self.matchATN(input) + else: + return self.execATN(input, dfa.s0) + finally: + input.release(mark) + + def reset(self): + self.prevAccept.reset() + self.startIndex = -1 + self.line = 1 + self.column = 0 + self.mode = self.DEFAULT_MODE + + def matchATN(self, input:InputStream): + startState = self.atn.modeToStartState[self.mode] + + if LexerATNSimulator.debug: + print("matchATN mode " + str(self.mode) + " start: " + str(startState)) + + old_mode = self.mode + s0_closure = self.computeStartState(input, startState) + suppressEdge = s0_closure.hasSemanticContext + s0_closure.hasSemanticContext = False + + next = self.addDFAState(s0_closure) + if not suppressEdge: + self.decisionToDFA[self.mode].s0 = next + + predict = self.execATN(input, next) + + if LexerATNSimulator.debug: + print("DFA after matchATN: " + str(self.decisionToDFA[old_mode].toLexerString())) + + return predict + + def execATN(self, input:InputStream, ds0:DFAState): + if LexerATNSimulator.debug: + print("start state closure=" + str(ds0.configs)) + + if ds0.isAcceptState: + # allow zero-length tokens + self.captureSimState(self.prevAccept, input, ds0) + + t = input.LA(1) + s = ds0 # s is current/from DFA state + + while True: # while more work + if LexerATNSimulator.debug: + print("execATN loop starting closure:", str(s.configs)) + + # As we move src->trg, src->trg, we keep track of the previous trg to + # avoid looking up the DFA state again, which is expensive. + # If the previous target was already part of the DFA, we might + # be able to avoid doing a reach operation upon t. If s!=null, + # it means that semantic predicates didn't prevent us from + # creating a DFA state. Once we know s!=null, we check to see if + # the DFA state has an edge already for t. If so, we can just reuse + # it's configuration set; there's no point in re-computing it. + # This is kind of like doing DFA simulation within the ATN + # simulation because DFA simulation is really just a way to avoid + # computing reach/closure sets. Technically, once we know that + # we have a previously added DFA state, we could jump over to + # the DFA simulator. But, that would mean popping back and forth + # a lot and making things more complicated algorithmically. + # This optimization makes a lot of sense for loops within DFA. + # A character will take us back to an existing DFA state + # that already has lots of edges out of it. e.g., .* in comments. + # print("Target for:" + str(s) + " and:" + str(t)) + target = self.getExistingTargetState(s, t) + # print("Existing:" + str(target)) + if target is None: + target = self.computeTargetState(input, s, t) + # print("Computed:" + str(target)) + + if target == self.ERROR: + break + + # If this is a consumable input element, make sure to consume before + # capturing the accept state so the input index, line, and char + # position accurately reflect the state of the interpreter at the + # end of the token. + if t != Token.EOF: + self.consume(input) + + if target.isAcceptState: + self.captureSimState(self.prevAccept, input, target) + if t == Token.EOF: + break + + t = input.LA(1) + + s = target # flip; current DFA target becomes new src/from state + + return self.failOrAccept(self.prevAccept, input, s.configs, t) + + # Get an existing target state for an edge in the DFA. If the target state + # for the edge has not yet been computed or is otherwise not available, + # this method returns {@code null}. + # + # @param s The current DFA state + # @param t The next input symbol + # @return The existing target DFA state for the given input symbol + # {@code t}, or {@code null} if the target state for this edge is not + # already cached + def getExistingTargetState(self, s:DFAState, t:int): + if s.edges is None or t < self.MIN_DFA_EDGE or t > self.MAX_DFA_EDGE: + return None + + target = s.edges[t - self.MIN_DFA_EDGE] + if LexerATNSimulator.debug and target is not None: + print("reuse state", str(s.stateNumber), "edge to", str(target.stateNumber)) + + return target + + # Compute a target state for an edge in the DFA, and attempt to add the + # computed state and corresponding edge to the DFA. + # + # @param input The input stream + # @param s The current DFA state + # @param t The next input symbol + # + # @return The computed target DFA state for the given input symbol + # {@code t}. If {@code t} does not lead to a valid DFA state, this method + # returns {@link #ERROR}. + def computeTargetState(self, input:InputStream, s:DFAState, t:int): + reach = OrderedATNConfigSet() + + # if we don't find an existing DFA state + # Fill reach starting from closure, following t transitions + self.getReachableConfigSet(input, s.configs, reach, t) + + if len(reach)==0: # we got nowhere on t from s + if not reach.hasSemanticContext: + # we got nowhere on t, don't throw out this knowledge; it'd + # cause a failover from DFA later. + self. addDFAEdge(s, t, self.ERROR) + + # stop when we can't match any more char + return self.ERROR + + # Add an edge from s to target DFA found/created for reach + return self.addDFAEdge(s, t, cfgs=reach) + + def failOrAccept(self, prevAccept:SimState , input:InputStream, reach:ATNConfigSet, t:int): + if self.prevAccept.dfaState is not None: + lexerActionExecutor = prevAccept.dfaState.lexerActionExecutor + self.accept(input, lexerActionExecutor, self.startIndex, prevAccept.index, prevAccept.line, prevAccept.column) + return prevAccept.dfaState.prediction + else: + # if no accept and EOF is first char, return EOF + if t==Token.EOF and input.index==self.startIndex: + return Token.EOF + raise LexerNoViableAltException(self.recog, input, self.startIndex, reach) + + # Given a starting configuration set, figure out all ATN configurations + # we can reach upon input {@code t}. Parameter {@code reach} is a return + # parameter. + def getReachableConfigSet(self, input:InputStream, closure:ATNConfigSet, reach:ATNConfigSet, t:int): + # this is used to skip processing for configs which have a lower priority + # than a config that already reached an accept state for the same rule + skipAlt = ATN.INVALID_ALT_NUMBER + for cfg in closure: + currentAltReachedAcceptState = ( cfg.alt == skipAlt ) + if currentAltReachedAcceptState and cfg.passedThroughNonGreedyDecision: + continue + + if LexerATNSimulator.debug: + print("testing", self.getTokenName(t), "at", str(cfg)) + + for trans in cfg.state.transitions: # for each transition + target = self.getReachableTarget(trans, t) + if target is not None: + lexerActionExecutor = cfg.lexerActionExecutor + if lexerActionExecutor is not None: + lexerActionExecutor = lexerActionExecutor.fixOffsetBeforeMatch(input.index - self.startIndex) + + treatEofAsEpsilon = (t == Token.EOF) + config = LexerATNConfig(state=target, lexerActionExecutor=lexerActionExecutor, config=cfg) + if self.closure(input, config, reach, currentAltReachedAcceptState, True, treatEofAsEpsilon): + # any remaining configs for this alt have a lower priority than + # the one that just reached an accept state. + skipAlt = cfg.alt + + def accept(self, input:InputStream, lexerActionExecutor:LexerActionExecutor, startIndex:int, index:int, line:int, charPos:int): + if LexerATNSimulator.debug: + print("ACTION", lexerActionExecutor) + + # seek to after last char in token + input.seek(index) + self.line = line + self.column = charPos + + if lexerActionExecutor is not None and self.recog is not None: + lexerActionExecutor.execute(self.recog, input, startIndex) + + def getReachableTarget(self, trans:Transition, t:int): + if trans.matches(t, 0, self.MAX_CHAR_VALUE): + return trans.target + else: + return None + + def computeStartState(self, input:InputStream, p:ATNState): + initialContext = PredictionContext.EMPTY + configs = OrderedATNConfigSet() + for i in range(0,len(p.transitions)): + target = p.transitions[i].target + c = LexerATNConfig(state=target, alt=i+1, context=initialContext) + self.closure(input, c, configs, False, False, False) + return configs + + # Since the alternatives within any lexer decision are ordered by + # preference, this method stops pursuing the closure as soon as an accept + # state is reached. After the first accept state is reached by depth-first + # search from {@code config}, all other (potentially reachable) states for + # this rule would have a lower priority. + # + # @return {@code true} if an accept state is reached, otherwise + # {@code false}. + def closure(self, input:InputStream, config:LexerATNConfig, configs:ATNConfigSet, currentAltReachedAcceptState:bool, + speculative:bool, treatEofAsEpsilon:bool): + if LexerATNSimulator.debug: + print("closure(" + str(config) + ")") + + if isinstance( config.state, RuleStopState ): + if LexerATNSimulator.debug: + if self.recog is not None: + print("closure at", self.recog.symbolicNames[config.state.ruleIndex], "rule stop", str(config)) + else: + print("closure at rule stop", str(config)) + + if config.context is None or config.context.hasEmptyPath(): + if config.context is None or config.context.isEmpty(): + configs.add(config) + return True + else: + configs.add(LexerATNConfig(state=config.state, config=config, context=PredictionContext.EMPTY)) + currentAltReachedAcceptState = True + + if config.context is not None and not config.context.isEmpty(): + for i in range(0,len(config.context)): + if config.context.getReturnState(i) != PredictionContext.EMPTY_RETURN_STATE: + newContext = config.context.getParent(i) # "pop" return state + returnState = self.atn.states[config.context.getReturnState(i)] + c = LexerATNConfig(state=returnState, config=config, context=newContext) + currentAltReachedAcceptState = self.closure(input, c, configs, + currentAltReachedAcceptState, speculative, treatEofAsEpsilon) + + return currentAltReachedAcceptState + + # optimization + if not config.state.epsilonOnlyTransitions: + if not currentAltReachedAcceptState or not config.passedThroughNonGreedyDecision: + configs.add(config) + + for t in config.state.transitions: + c = self.getEpsilonTarget(input, config, t, configs, speculative, treatEofAsEpsilon) + if c is not None: + currentAltReachedAcceptState = self.closure(input, c, configs, currentAltReachedAcceptState, speculative, treatEofAsEpsilon) + + return currentAltReachedAcceptState + + # side-effect: can alter configs.hasSemanticContext + def getEpsilonTarget(self, input:InputStream, config:LexerATNConfig, t:Transition, configs:ATNConfigSet, + speculative:bool, treatEofAsEpsilon:bool): + c = None + if t.serializationType==Transition.RULE: + newContext = SingletonPredictionContext.create(config.context, t.followState.stateNumber) + c = LexerATNConfig(state=t.target, config=config, context=newContext) + + elif t.serializationType==Transition.PRECEDENCE: + raise UnsupportedOperationException("Precedence predicates are not supported in lexers.") + + elif t.serializationType==Transition.PREDICATE: + # Track traversing semantic predicates. If we traverse, + # we cannot add a DFA state for this "reach" computation + # because the DFA would not test the predicate again in the + # future. Rather than creating collections of semantic predicates + # like v3 and testing them on prediction, v4 will test them on the + # fly all the time using the ATN not the DFA. This is slower but + # semantically it's not used that often. One of the key elements to + # this predicate mechanism is not adding DFA states that see + # predicates immediately afterwards in the ATN. For example, + + # a : ID {p1}? | ID {p2}? ; + + # should create the start state for rule 'a' (to save start state + # competition), but should not create target of ID state. The + # collection of ATN states the following ID references includes + # states reached by traversing predicates. Since this is when we + # test them, we cannot cash the DFA state target of ID. + + if LexerATNSimulator.debug: + print("EVAL rule "+ str(t.ruleIndex) + ":" + str(t.predIndex)) + configs.hasSemanticContext = True + if self.evaluatePredicate(input, t.ruleIndex, t.predIndex, speculative): + c = LexerATNConfig(state=t.target, config=config) + + elif t.serializationType==Transition.ACTION: + if config.context is None or config.context.hasEmptyPath(): + # execute actions anywhere in the start rule for a token. + # + # TODO: if the entry rule is invoked recursively, some + # actions may be executed during the recursive call. The + # problem can appear when hasEmptyPath() is true but + # isEmpty() is false. In this case, the config needs to be + # split into two contexts - one with just the empty path + # and another with everything but the empty path. + # Unfortunately, the current algorithm does not allow + # getEpsilonTarget to return two configurations, so + # additional modifications are needed before we can support + # the split operation. + lexerActionExecutor = LexerActionExecutor.append(config.lexerActionExecutor, + self.atn.lexerActions[t.actionIndex]) + c = LexerATNConfig(state=t.target, config=config, lexerActionExecutor=lexerActionExecutor) + + else: + # ignore actions in referenced rules + c = LexerATNConfig(state=t.target, config=config) + + elif t.serializationType==Transition.EPSILON: + c = LexerATNConfig(state=t.target, config=config) + + elif t.serializationType in [ Transition.ATOM, Transition.RANGE, Transition.SET ]: + if treatEofAsEpsilon: + if t.matches(Token.EOF, 0, self.MAX_CHAR_VALUE): + c = LexerATNConfig(state=t.target, config=config) + + return c + + # Evaluate a predicate specified in the lexer. + # + #

      If {@code speculative} is {@code true}, this method was called before + # {@link #consume} for the matched character. This method should call + # {@link #consume} before evaluating the predicate to ensure position + # sensitive values, including {@link Lexer#getText}, {@link Lexer#getLine}, + # and {@link Lexer#getcolumn}, properly reflect the current + # lexer state. This method should restore {@code input} and the simulator + # to the original state before returning (i.e. undo the actions made by the + # call to {@link #consume}.

      + # + # @param input The input stream. + # @param ruleIndex The rule containing the predicate. + # @param predIndex The index of the predicate within the rule. + # @param speculative {@code true} if the current index in {@code input} is + # one character before the predicate's location. + # + # @return {@code true} if the specified predicate evaluates to + # {@code true}. + #/ + def evaluatePredicate(self, input:InputStream, ruleIndex:int, predIndex:int, speculative:bool): + # assume true if no recognizer was provided + if self.recog is None: + return True + + if not speculative: + return self.recog.sempred(None, ruleIndex, predIndex) + + savedcolumn = self.column + savedLine = self.line + index = input.index + marker = input.mark() + try: + self.consume(input) + return self.recog.sempred(None, ruleIndex, predIndex) + finally: + self.column = savedcolumn + self.line = savedLine + input.seek(index) + input.release(marker) + + def captureSimState(self, settings:SimState, input:InputStream, dfaState:DFAState): + settings.index = input.index + settings.line = self.line + settings.column = self.column + settings.dfaState = dfaState + + def addDFAEdge(self, from_:DFAState, tk:int, to:DFAState=None, cfgs:ATNConfigSet=None) -> DFAState: + + if to is None and cfgs is not None: + # leading to this call, ATNConfigSet.hasSemanticContext is used as a + # marker indicating dynamic predicate evaluation makes this edge + # dependent on the specific input sequence, so the static edge in the + # DFA should be omitted. The target DFAState is still created since + # execATN has the ability to resynchronize with the DFA state cache + # following the predicate evaluation step. + # + # TJP notes: next time through the DFA, we see a pred again and eval. + # If that gets us to a previously created (but dangling) DFA + # state, we can continue in pure DFA mode from there. + #/ + suppressEdge = cfgs.hasSemanticContext + cfgs.hasSemanticContext = False + + to = self.addDFAState(cfgs) + + if suppressEdge: + return to + + # add the edge + if tk < self.MIN_DFA_EDGE or tk > self.MAX_DFA_EDGE: + # Only track edges within the DFA bounds + return to + + if LexerATNSimulator.debug: + print("EDGE " + str(from_) + " -> " + str(to) + " upon "+ chr(tk)) + + if from_.edges is None: + # make room for tokens 1..n and -1 masquerading as index 0 + from_.edges = [ None ] * (self.MAX_DFA_EDGE - self.MIN_DFA_EDGE + 1) + + from_.edges[tk - self.MIN_DFA_EDGE] = to # connect + + return to + + + # Add a new DFA state if there isn't one with this set of + # configurations already. This method also detects the first + # configuration containing an ATN rule stop state. Later, when + # traversing the DFA, we will know which rule to accept. + def addDFAState(self, configs:ATNConfigSet) -> DFAState: + + proposed = DFAState(configs=configs) + firstConfigWithRuleStopState = next((cfg for cfg in configs if isinstance(cfg.state, RuleStopState)), None) + + if firstConfigWithRuleStopState is not None: + proposed.isAcceptState = True + proposed.lexerActionExecutor = firstConfigWithRuleStopState.lexerActionExecutor + proposed.prediction = self.atn.ruleToTokenType[firstConfigWithRuleStopState.state.ruleIndex] + + dfa = self.decisionToDFA[self.mode] + existing = dfa.states.get(proposed, None) + if existing is not None: + return existing + + newState = proposed + + newState.stateNumber = len(dfa.states) + configs.setReadonly(True) + newState.configs = configs + dfa.states[newState] = newState + return newState + + def getDFA(self, mode:int): + return self.decisionToDFA[mode] + + # Get the text matched so far for the current token. + def getText(self, input:InputStream): + # index is first lookahead char, don't include. + return input.getText(self.startIndex, input.index-1) + + def consume(self, input:InputStream): + curChar = input.LA(1) + if curChar==ord('\n'): + self.line += 1 + self.column = 0 + else: + self.column += 1 + input.consume() + + def getTokenName(self, t:int): + if t==-1: + return "EOF" + else: + return "'" + chr(t) + "'" + + +LexerATNSimulator.ERROR = DFAState(0x7FFFFFFF, ATNConfigSet()) + +del Lexer diff --git a/lib/python3.12/site-packages/antlr4/atn/LexerAction.py b/lib/python3.12/site-packages/antlr4/atn/LexerAction.py new file mode 100644 index 0000000000000000000000000000000000000000..0fa7a895f31cea4415b71def30ca1ec90167f1b3 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/LexerAction.py @@ -0,0 +1,298 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. + # + +from enum import IntEnum + +# need forward declaration +Lexer = None + + +class LexerActionType(IntEnum): + + CHANNEL = 0 #The type of a {@link LexerChannelAction} action. + CUSTOM = 1 #The type of a {@link LexerCustomAction} action. + MODE = 2 #The type of a {@link LexerModeAction} action. + MORE = 3 #The type of a {@link LexerMoreAction} action. + POP_MODE = 4 #The type of a {@link LexerPopModeAction} action. + PUSH_MODE = 5 #The type of a {@link LexerPushModeAction} action. + SKIP = 6 #The type of a {@link LexerSkipAction} action. + TYPE = 7 #The type of a {@link LexerTypeAction} action. + +class LexerAction(object): + __slots__ = ('actionType', 'isPositionDependent') + + def __init__(self, action:LexerActionType): + self.actionType = action + self.isPositionDependent = False + + def __hash__(self): + return hash(self.actionType) + + def __eq__(self, other): + return self is other + + +# +# Implements the {@code skip} lexer action by calling {@link Lexer#skip}. +# +#

      The {@code skip} command does not have any parameters, so this action is +# implemented as a singleton instance exposed by {@link #INSTANCE}.

      +class LexerSkipAction(LexerAction): + + # Provides a singleton instance of this parameterless lexer action. + INSTANCE = None + + def __init__(self): + super().__init__(LexerActionType.SKIP) + + def execute(self, lexer:Lexer): + lexer.skip() + + def __str__(self): + return "skip" + +LexerSkipAction.INSTANCE = LexerSkipAction() + +# Implements the {@code type} lexer action by calling {@link Lexer#setType} +# with the assigned type. +class LexerTypeAction(LexerAction): + __slots__ = 'type' + + def __init__(self, type:int): + super().__init__(LexerActionType.TYPE) + self.type = type + + def execute(self, lexer:Lexer): + lexer.type = self.type + + def __hash__(self): + return hash((self.actionType, self.type)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerTypeAction): + return False + else: + return self.type == other.type + + def __str__(self): + return "type(" + str(self.type) + ")" + + +# Implements the {@code pushMode} lexer action by calling +# {@link Lexer#pushMode} with the assigned mode. +class LexerPushModeAction(LexerAction): + __slots__ = 'mode' + + def __init__(self, mode:int): + super().__init__(LexerActionType.PUSH_MODE) + self.mode = mode + + #

      This action is implemented by calling {@link Lexer#pushMode} with the + # value provided by {@link #getMode}.

      + def execute(self, lexer:Lexer): + lexer.pushMode(self.mode) + + def __hash__(self): + return hash((self.actionType, self.mode)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerPushModeAction): + return False + else: + return self.mode == other.mode + + def __str__(self): + return "pushMode(" + str(self.mode) + ")" + + +# Implements the {@code popMode} lexer action by calling {@link Lexer#popMode}. +# +#

      The {@code popMode} command does not have any parameters, so this action is +# implemented as a singleton instance exposed by {@link #INSTANCE}.

      +class LexerPopModeAction(LexerAction): + + INSTANCE = None + + def __init__(self): + super().__init__(LexerActionType.POP_MODE) + + #

      This action is implemented by calling {@link Lexer#popMode}.

      + def execute(self, lexer:Lexer): + lexer.popMode() + + def __str__(self): + return "popMode" + +LexerPopModeAction.INSTANCE = LexerPopModeAction() + +# Implements the {@code more} lexer action by calling {@link Lexer#more}. +# +#

      The {@code more} command does not have any parameters, so this action is +# implemented as a singleton instance exposed by {@link #INSTANCE}.

      +class LexerMoreAction(LexerAction): + + INSTANCE = None + + def __init__(self): + super().__init__(LexerActionType.MORE) + + #

      This action is implemented by calling {@link Lexer#popMode}.

      + def execute(self, lexer:Lexer): + lexer.more() + + def __str__(self): + return "more" + +LexerMoreAction.INSTANCE = LexerMoreAction() + +# Implements the {@code mode} lexer action by calling {@link Lexer#mode} with +# the assigned mode. +class LexerModeAction(LexerAction): + __slots__ = 'mode' + + def __init__(self, mode:int): + super().__init__(LexerActionType.MODE) + self.mode = mode + + #

      This action is implemented by calling {@link Lexer#mode} with the + # value provided by {@link #getMode}.

      + def execute(self, lexer:Lexer): + lexer.mode(self.mode) + + def __hash__(self): + return hash((self.actionType, self.mode)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerModeAction): + return False + else: + return self.mode == other.mode + + def __str__(self): + return "mode(" + str(self.mode) + ")" + +# Executes a custom lexer action by calling {@link Recognizer#action} with the +# rule and action indexes assigned to the custom action. The implementation of +# a custom action is added to the generated code for the lexer in an override +# of {@link Recognizer#action} when the grammar is compiled. +# +#

      This class may represent embedded actions created with the {...} +# syntax in ANTLR 4, as well as actions created for lexer commands where the +# command argument could not be evaluated when the grammar was compiled.

      + +class LexerCustomAction(LexerAction): + __slots__ = ('ruleIndex', 'actionIndex') + + # Constructs a custom lexer action with the specified rule and action + # indexes. + # + # @param ruleIndex The rule index to use for calls to + # {@link Recognizer#action}. + # @param actionIndex The action index to use for calls to + # {@link Recognizer#action}. + #/ + def __init__(self, ruleIndex:int, actionIndex:int): + super().__init__(LexerActionType.CUSTOM) + self.ruleIndex = ruleIndex + self.actionIndex = actionIndex + self.isPositionDependent = True + + #

      Custom actions are implemented by calling {@link Lexer#action} with the + # appropriate rule and action indexes.

      + def execute(self, lexer:Lexer): + lexer.action(None, self.ruleIndex, self.actionIndex) + + def __hash__(self): + return hash((self.actionType, self.ruleIndex, self.actionIndex)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerCustomAction): + return False + else: + return self.ruleIndex == other.ruleIndex and self.actionIndex == other.actionIndex + +# Implements the {@code channel} lexer action by calling +# {@link Lexer#setChannel} with the assigned channel. +class LexerChannelAction(LexerAction): + __slots__ = 'channel' + + # Constructs a new {@code channel} action with the specified channel value. + # @param channel The channel value to pass to {@link Lexer#setChannel}. + def __init__(self, channel:int): + super().__init__(LexerActionType.CHANNEL) + self.channel = channel + + #

      This action is implemented by calling {@link Lexer#setChannel} with the + # value provided by {@link #getChannel}.

      + def execute(self, lexer:Lexer): + lexer._channel = self.channel + + def __hash__(self): + return hash((self.actionType, self.channel)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerChannelAction): + return False + else: + return self.channel == other.channel + + def __str__(self): + return "channel(" + str(self.channel) + ")" + +# This implementation of {@link LexerAction} is used for tracking input offsets +# for position-dependent actions within a {@link LexerActionExecutor}. +# +#

      This action is not serialized as part of the ATN, and is only required for +# position-dependent lexer actions which appear at a location other than the +# end of a rule. For more information about DFA optimizations employed for +# lexer actions, see {@link LexerActionExecutor#append} and +# {@link LexerActionExecutor#fixOffsetBeforeMatch}.

      +class LexerIndexedCustomAction(LexerAction): + __slots__ = ('offset', 'action') + + # Constructs a new indexed custom action by associating a character offset + # with a {@link LexerAction}. + # + #

      Note: This class is only required for lexer actions for which + # {@link LexerAction#isPositionDependent} returns {@code true}.

      + # + # @param offset The offset into the input {@link CharStream}, relative to + # the token start index, at which the specified lexer action should be + # executed. + # @param action The lexer action to execute at a particular offset in the + # input {@link CharStream}. + def __init__(self, offset:int, action:LexerAction): + super().__init__(action.actionType) + self.offset = offset + self.action = action + self.isPositionDependent = True + + #

      This method calls {@link #execute} on the result of {@link #getAction} + # using the provided {@code lexer}.

      + def execute(self, lexer:Lexer): + # assume the input stream position was properly set by the calling code + self.action.execute(lexer) + + def __hash__(self): + return hash((self.actionType, self.offset, self.action)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerIndexedCustomAction): + return False + else: + return self.offset == other.offset and self.action == other.action diff --git a/lib/python3.12/site-packages/antlr4/atn/LexerActionExecutor.py b/lib/python3.12/site-packages/antlr4/atn/LexerActionExecutor.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6462c3a28f4ccbcb0a65dcde96f497dd392416 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/LexerActionExecutor.py @@ -0,0 +1,143 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# Represents an executor for a sequence of lexer actions which traversed during +# the matching operation of a lexer rule (token). +# +#

      The executor tracks position information for position-dependent lexer actions +# efficiently, ensuring that actions appearing only at the end of the rule do +# not cause bloating of the {@link DFA} created for the lexer.

      + + +from antlr4.InputStream import InputStream +from antlr4.atn.LexerAction import LexerAction, LexerIndexedCustomAction + +# need a forward declaration +Lexer = None +LexerActionExecutor = None + +class LexerActionExecutor(object): + __slots__ = ('lexerActions', 'hashCode') + + def __init__(self, lexerActions:list=list()): + self.lexerActions = lexerActions + # Caches the result of {@link #hashCode} since the hash code is an element + # of the performance-critical {@link LexerATNConfig#hashCode} operation. + self.hashCode = hash("".join([str(la) for la in lexerActions])) + + + # Creates a {@link LexerActionExecutor} which executes the actions for + # the input {@code lexerActionExecutor} followed by a specified + # {@code lexerAction}. + # + # @param lexerActionExecutor The executor for actions already traversed by + # the lexer while matching a token within a particular + # {@link LexerATNConfig}. If this is {@code null}, the method behaves as + # though it were an empty executor. + # @param lexerAction The lexer action to execute after the actions + # specified in {@code lexerActionExecutor}. + # + # @return A {@link LexerActionExecutor} for executing the combine actions + # of {@code lexerActionExecutor} and {@code lexerAction}. + @staticmethod + def append(lexerActionExecutor:LexerActionExecutor , lexerAction:LexerAction ): + if lexerActionExecutor is None: + return LexerActionExecutor([ lexerAction ]) + + lexerActions = lexerActionExecutor.lexerActions + [ lexerAction ] + return LexerActionExecutor(lexerActions) + + # Creates a {@link LexerActionExecutor} which encodes the current offset + # for position-dependent lexer actions. + # + #

      Normally, when the executor encounters lexer actions where + # {@link LexerAction#isPositionDependent} returns {@code true}, it calls + # {@link IntStream#seek} on the input {@link CharStream} to set the input + # position to the end of the current token. This behavior provides + # for efficient DFA representation of lexer actions which appear at the end + # of a lexer rule, even when the lexer rule matches a variable number of + # characters.

      + # + #

      Prior to traversing a match transition in the ATN, the current offset + # from the token start index is assigned to all position-dependent lexer + # actions which have not already been assigned a fixed offset. By storing + # the offsets relative to the token start index, the DFA representation of + # lexer actions which appear in the middle of tokens remains efficient due + # to sharing among tokens of the same length, regardless of their absolute + # position in the input stream.

      + # + #

      If the current executor already has offsets assigned to all + # position-dependent lexer actions, the method returns {@code this}.

      + # + # @param offset The current offset to assign to all position-dependent + # lexer actions which do not already have offsets assigned. + # + # @return A {@link LexerActionExecutor} which stores input stream offsets + # for all position-dependent lexer actions. + #/ + def fixOffsetBeforeMatch(self, offset:int): + updatedLexerActions = None + for i in range(0, len(self.lexerActions)): + if self.lexerActions[i].isPositionDependent and not isinstance(self.lexerActions[i], LexerIndexedCustomAction): + if updatedLexerActions is None: + updatedLexerActions = [ la for la in self.lexerActions ] + updatedLexerActions[i] = LexerIndexedCustomAction(offset, self.lexerActions[i]) + + if updatedLexerActions is None: + return self + else: + return LexerActionExecutor(updatedLexerActions) + + + # Execute the actions encapsulated by this executor within the context of a + # particular {@link Lexer}. + # + #

      This method calls {@link IntStream#seek} to set the position of the + # {@code input} {@link CharStream} prior to calling + # {@link LexerAction#execute} on a position-dependent action. Before the + # method returns, the input position will be restored to the same position + # it was in when the method was invoked.

      + # + # @param lexer The lexer instance. + # @param input The input stream which is the source for the current token. + # When this method is called, the current {@link IntStream#index} for + # {@code input} should be the start of the following token, i.e. 1 + # character past the end of the current token. + # @param startIndex The token start index. This value may be passed to + # {@link IntStream#seek} to set the {@code input} position to the beginning + # of the token. + #/ + def execute(self, lexer:Lexer, input:InputStream, startIndex:int): + requiresSeek = False + stopIndex = input.index + try: + for lexerAction in self.lexerActions: + if isinstance(lexerAction, LexerIndexedCustomAction): + offset = lexerAction.offset + input.seek(startIndex + offset) + lexerAction = lexerAction.action + requiresSeek = (startIndex + offset) != stopIndex + elif lexerAction.isPositionDependent: + input.seek(stopIndex) + requiresSeek = False + lexerAction.execute(lexer) + finally: + if requiresSeek: + input.seek(stopIndex) + + def __hash__(self): + return self.hashCode + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, LexerActionExecutor): + return False + else: + return self.hashCode == other.hashCode \ + and self.lexerActions == other.lexerActions + +del Lexer diff --git a/lib/python3.12/site-packages/antlr4/atn/ParserATNSimulator.py b/lib/python3.12/site-packages/antlr4/atn/ParserATNSimulator.py new file mode 100644 index 0000000000000000000000000000000000000000..d1fb3d7ed8bab47fb851d85da88d20dc39715627 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/ParserATNSimulator.py @@ -0,0 +1,1649 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# The embodiment of the adaptive LL(*), ALL(*), parsing strategy. +# +#

      +# The basic complexity of the adaptive strategy makes it harder to understand. +# We begin with ATN simulation to build paths in a DFA. Subsequent prediction +# requests go through the DFA first. If they reach a state without an edge for +# the current symbol, the algorithm fails over to the ATN simulation to +# complete the DFA path for the current input (until it finds a conflict state +# or uniquely predicting state).

      +# +#

      +# All of that is done without using the outer context because we want to create +# a DFA that is not dependent upon the rule invocation stack when we do a +# prediction. One DFA works in all contexts. We avoid using context not +# necessarily because it's slower, although it can be, but because of the DFA +# caching problem. The closure routine only considers the rule invocation stack +# created during prediction beginning in the decision rule. For example, if +# prediction occurs without invoking another rule's ATN, there are no context +# stacks in the configurations. When lack of context leads to a conflict, we +# don't know if it's an ambiguity or a weakness in the strong LL(*) parsing +# strategy (versus full LL(*)).

      +# +#

      +# When SLL yields a configuration set with conflict, we rewind the input and +# retry the ATN simulation, this time using full outer context without adding +# to the DFA. Configuration context stacks will be the full invocation stacks +# from the start rule. If we get a conflict using full context, then we can +# definitively say we have a true ambiguity for that input sequence. If we +# don't get a conflict, it implies that the decision is sensitive to the outer +# context. (It is not context-sensitive in the sense of context-sensitive +# grammars.)

      +# +#

      +# The next time we reach this DFA state with an SLL conflict, through DFA +# simulation, we will again retry the ATN simulation using full context mode. +# This is slow because we can't save the results and have to "interpret" the +# ATN each time we get that input.

      +# +#

      +# CACHING FULL CONTEXT PREDICTIONS

      +# +#

      +# We could cache results from full context to predicted alternative easily and +# that saves a lot of time but doesn't work in presence of predicates. The set +# of visible predicates from the ATN start state changes depending on the +# context, because closure can fall off the end of a rule. I tried to cache +# tuples (stack context, semantic context, predicted alt) but it was slower +# than interpreting and much more complicated. Also required a huge amount of +# memory. The goal is not to create the world's fastest parser anyway. I'd like +# to keep this algorithm simple. By launching multiple threads, we can improve +# the speed of parsing across a large number of files.

      +# +#

      +# There is no strict ordering between the amount of input used by SLL vs LL, +# which makes it really hard to build a cache for full context. Let's say that +# we have input A B C that leads to an SLL conflict with full context X. That +# implies that using X we might only use A B but we could also use A B C D to +# resolve conflict. Input A B C D could predict alternative 1 in one position +# in the input and A B C E could predict alternative 2 in another position in +# input. The conflicting SLL configurations could still be non-unique in the +# full context prediction, which would lead us to requiring more input than the +# original A B C. To make a prediction cache work, we have to track the exact +# input used during the previous prediction. That amounts to a cache that maps +# X to a specific DFA for that context.

      +# +#

      +# Something should be done for left-recursive expression predictions. They are +# likely LL(1) + pred eval. Easier to do the whole SLL unless error and retry +# with full LL thing Sam does.

      +# +#

      +# AVOIDING FULL CONTEXT PREDICTION

      +# +#

      +# We avoid doing full context retry when the outer context is empty, we did not +# dip into the outer context by falling off the end of the decision state rule, +# or when we force SLL mode.

      +# +#

      +# As an example of the not dip into outer context case, consider as super +# constructor calls versus function calls. One grammar might look like +# this:

      +# +#
      +# ctorBody
      +#   : '{' superCall? stat* '}'
      +#   ;
      +# 
      +# +#

      +# Or, you might see something like

      +# +#
      +# stat
      +#   : superCall ';'
      +#   | expression ';'
      +#   | ...
      +#   ;
      +# 
      +# +#

      +# In both cases I believe that no closure operations will dip into the outer +# context. In the first case ctorBody in the worst case will stop at the '}'. +# In the 2nd case it should stop at the ';'. Both cases should stay within the +# entry rule and not dip into the outer context.

      +# +#

      +# PREDICATES

      +# +#

      +# Predicates are always evaluated if present in either SLL or LL both. SLL and +# LL simulation deals with predicates differently. SLL collects predicates as +# it performs closure operations like ANTLR v3 did. It delays predicate +# evaluation until it reaches and accept state. This allows us to cache the SLL +# ATN simulation whereas, if we had evaluated predicates on-the-fly during +# closure, the DFA state configuration sets would be different and we couldn't +# build up a suitable DFA.

      +# +#

      +# When building a DFA accept state during ATN simulation, we evaluate any +# predicates and return the sole semantically valid alternative. If there is +# more than 1 alternative, we report an ambiguity. If there are 0 alternatives, +# we throw an exception. Alternatives without predicates act like they have +# true predicates. The simple way to think about it is to strip away all +# alternatives with false predicates and choose the minimum alternative that +# remains.

      +# +#

      +# When we start in the DFA and reach an accept state that's predicated, we test +# those and return the minimum semantically viable alternative. If no +# alternatives are viable, we throw an exception.

      +# +#

      +# During full LL ATN simulation, closure always evaluates predicates and +# on-the-fly. This is crucial to reducing the configuration set size during +# closure. It hits a landmine when parsing with the Java grammar, for example, +# without this on-the-fly evaluation.

      +# +#

      +# SHARING DFA

      +# +#

      +# All instances of the same parser share the same decision DFAs through a +# static field. Each instance gets its own ATN simulator but they share the +# same {@link #decisionToDFA} field. They also share a +# {@link PredictionContextCache} object that makes sure that all +# {@link PredictionContext} objects are shared among the DFA states. This makes +# a big size difference.

      +# +#

      +# THREAD SAFETY

      +# +#

      +# The {@link ParserATNSimulator} locks on the {@link #decisionToDFA} field when +# it adds a new DFA object to that array. {@link #addDFAEdge} +# locks on the DFA for the current decision when setting the +# {@link DFAState#edges} field. {@link #addDFAState} locks on +# the DFA for the current decision when looking up a DFA state to see if it +# already exists. We must make sure that all requests to add DFA states that +# are equivalent result in the same shared DFA object. This is because lots of +# threads will be trying to update the DFA at once. The +# {@link #addDFAState} method also locks inside the DFA lock +# but this time on the shared context cache when it rebuilds the +# configurations' {@link PredictionContext} objects using cached +# subgraphs/nodes. No other locking occurs, even during DFA simulation. This is +# safe as long as we can guarantee that all threads referencing +# {@code s.edge[t]} get the same physical target {@link DFAState}, or +# {@code null}. Once into the DFA, the DFA simulation does not reference the +# {@link DFA#states} map. It follows the {@link DFAState#edges} field to new +# targets. The DFA simulator will either find {@link DFAState#edges} to be +# {@code null}, to be non-{@code null} and {@code dfa.edges[t]} null, or +# {@code dfa.edges[t]} to be non-null. The +# {@link #addDFAEdge} method could be racing to set the field +# but in either case the DFA simulator works; if {@code null}, and requests ATN +# simulation. It could also race trying to get {@code dfa.edges[t]}, but either +# way it will work because it's not doing a test and set operation.

      +# +#

      +# Starting with SLL then failing to combined SLL/LL (Two-Stage +# Parsing)

      +# +#

      +# Sam pointed out that if SLL does not give a syntax error, then there is no +# point in doing full LL, which is slower. We only have to try LL if we get a +# syntax error. For maximum speed, Sam starts the parser set to pure SLL +# mode with the {@link BailErrorStrategy}:

      +# +#
      +# parser.{@link Parser#getInterpreter() getInterpreter()}.{@link #setPredictionMode setPredictionMode}{@code (}{@link PredictionMode#SLL}{@code )};
      +# parser.{@link Parser#setErrorHandler setErrorHandler}(new {@link BailErrorStrategy}());
      +# 
      +# +#

      +# If it does not get a syntax error, then we're done. If it does get a syntax +# error, we need to retry with the combined SLL/LL strategy.

      +# +#

      +# The reason this works is as follows. If there are no SLL conflicts, then the +# grammar is SLL (at least for that input set). If there is an SLL conflict, +# the full LL analysis must yield a set of viable alternatives which is a +# subset of the alternatives reported by SLL. If the LL set is a singleton, +# then the grammar is LL but not SLL. If the LL set is the same size as the SLL +# set, the decision is SLL. If the LL set has size > 1, then that decision +# is truly ambiguous on the current input. If the LL set is smaller, then the +# SLL conflict resolution might choose an alternative that the full LL would +# rule out as a possibility based upon better context information. If that's +# the case, then the SLL parse will definitely get an error because the full LL +# analysis says it's not viable. If SLL conflict resolution chooses an +# alternative within the LL set, them both SLL and LL would choose the same +# alternative because they both choose the minimum of multiple conflicting +# alternatives.

      +# +#

      +# Let's say we have a set of SLL conflicting alternatives {@code {1, 2, 3}} and +# a smaller LL set called s. If s is {@code {2, 3}}, then SLL +# parsing will get an error because SLL will pursue alternative 1. If +# s is {@code {1, 2}} or {@code {1, 3}} then both SLL and LL will +# choose the same alternative because alternative one is the minimum of either +# set. If s is {@code {2}} or {@code {3}} then SLL will get a syntax +# error. If s is {@code {1}} then SLL will succeed.

      +# +#

      +# Of course, if the input is invalid, then we will get an error for sure in +# both SLL and LL parsing. Erroneous input will therefore require 2 passes over +# the input.

      +# +import sys +from antlr4 import DFA +from antlr4.PredictionContext import PredictionContextCache, PredictionContext, SingletonPredictionContext, \ + PredictionContextFromRuleContext +from antlr4.BufferedTokenStream import TokenStream +from antlr4.Parser import Parser +from antlr4.ParserRuleContext import ParserRuleContext +from antlr4.RuleContext import RuleContext +from antlr4.Token import Token +from antlr4.Utils import str_list +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNConfig import ATNConfig +from antlr4.atn.ATNConfigSet import ATNConfigSet +from antlr4.atn.ATNSimulator import ATNSimulator +from antlr4.atn.ATNState import StarLoopEntryState, DecisionState, RuleStopState, ATNState +from antlr4.atn.PredictionMode import PredictionMode +from antlr4.atn.SemanticContext import SemanticContext, AND, andContext, orContext +from antlr4.atn.Transition import Transition, RuleTransition, ActionTransition, PrecedencePredicateTransition, \ + PredicateTransition, AtomTransition, SetTransition, NotSetTransition +from antlr4.dfa.DFAState import DFAState, PredPrediction +from antlr4.error.Errors import NoViableAltException + + +class ParserATNSimulator(ATNSimulator): + __slots__ = ( + 'parser', 'decisionToDFA', 'predictionMode', '_input', '_startIndex', + '_outerContext', '_dfa', 'mergeCache' + ) + + debug = False + debug_list_atn_decisions = False + dfa_debug = False + retry_debug = False + + + def __init__(self, parser:Parser, atn:ATN, decisionToDFA:list, sharedContextCache:PredictionContextCache): + super().__init__(atn, sharedContextCache) + self.parser = parser + self.decisionToDFA = decisionToDFA + # SLL, LL, or LL + exact ambig detection?# + self.predictionMode = PredictionMode.LL + # LAME globals to avoid parameters!!!!! I need these down deep in predTransition + self._input = None + self._startIndex = 0 + self._outerContext = None + self._dfa = None + # Each prediction operation uses a cache for merge of prediction contexts. + # Don't keep around as it wastes huge amounts of memory. DoubleKeyMap + # isn't synchronized but we're ok since two threads shouldn't reuse same + # parser/atnsim object because it can only handle one input at a time. + # This maps graphs a and b to merged result c. (a,b)→c. We can avoid + # the merge if we ever see a and b again. Note that (b,a)→c should + # also be examined during cache lookup. + # + self.mergeCache = None + + + def reset(self): + pass + + def adaptivePredict(self, input:TokenStream, decision:int, outerContext:ParserRuleContext): + if ParserATNSimulator.debug or ParserATNSimulator.debug_list_atn_decisions: + print("adaptivePredict decision " + str(decision) + + " exec LA(1)==" + self.getLookaheadName(input) + + " line " + str(input.LT(1).line) + ":" + + str(input.LT(1).column)) + self._input = input + self._startIndex = input.index + self._outerContext = outerContext + + dfa = self.decisionToDFA[decision] + self._dfa = dfa + m = input.mark() + index = input.index + + # Now we are certain to have a specific decision's DFA + # But, do we still need an initial state? + try: + if dfa.precedenceDfa: + # the start state for a precedence DFA depends on the current + # parser precedence, and is provided by a DFA method. + s0 = dfa.getPrecedenceStartState(self.parser.getPrecedence()) + else: + # the start state for a "regular" DFA is just s0 + s0 = dfa.s0 + + if s0 is None: + if outerContext is None: + outerContext = ParserRuleContext.EMPTY + if ParserATNSimulator.debug or ParserATNSimulator.debug_list_atn_decisions: + print("predictATN decision " + str(dfa.decision) + + " exec LA(1)==" + self.getLookaheadName(input) + + ", outerContext=" + outerContext.toString(self.parser.literalNames, None)) + + fullCtx = False + s0_closure = self.computeStartState(dfa.atnStartState, ParserRuleContext.EMPTY, fullCtx) + + if dfa.precedenceDfa: + # If this is a precedence DFA, we use applyPrecedenceFilter + # to convert the computed start state to a precedence start + # state. We then use DFA.setPrecedenceStartState to set the + # appropriate start state for the precedence level rather + # than simply setting DFA.s0. + # + dfa.s0.configs = s0_closure # not used for prediction but useful to know start configs anyway + s0_closure = self.applyPrecedenceFilter(s0_closure) + s0 = self.addDFAState(dfa, DFAState(configs=s0_closure)) + dfa.setPrecedenceStartState(self.parser.getPrecedence(), s0) + else: + s0 = self.addDFAState(dfa, DFAState(configs=s0_closure)) + dfa.s0 = s0 + + alt = self.execATN(dfa, s0, input, index, outerContext) + if ParserATNSimulator.debug: + print("DFA after predictATN: " + dfa.toString(self.parser.literalNames)) + return alt + finally: + self._dfa = None + self.mergeCache = None # wack cache after each prediction + input.seek(index) + input.release(m) + + # Performs ATN simulation to compute a predicted alternative based + # upon the remaining input, but also updates the DFA cache to avoid + # having to traverse the ATN again for the same input sequence. + + # There are some key conditions we're looking for after computing a new + # set of ATN configs (proposed DFA state): + # if the set is empty, there is no viable alternative for current symbol + # does the state uniquely predict an alternative? + # does the state have a conflict that would prevent us from + # putting it on the work list? + + # We also have some key operations to do: + # add an edge from previous DFA state to potentially new DFA state, D, + # upon current symbol but only if adding to work list, which means in all + # cases except no viable alternative (and possibly non-greedy decisions?) + # collecting predicates and adding semantic context to DFA accept states + # adding rule context to context-sensitive DFA accept states + # consuming an input symbol + # reporting a conflict + # reporting an ambiguity + # reporting a context sensitivity + # reporting insufficient predicates + + # cover these cases: + # dead end + # single alt + # single alt + preds + # conflict + # conflict + preds + # + def execATN(self, dfa:DFA, s0:DFAState, input:TokenStream, startIndex:int, outerContext:ParserRuleContext ): + if ParserATNSimulator.debug or ParserATNSimulator.debug_list_atn_decisions: + print("execATN decision " + str(dfa.decision) + + " exec LA(1)==" + self.getLookaheadName(input) + + " line " + str(input.LT(1).line) + ":" + str(input.LT(1).column)) + + previousD = s0 + + if ParserATNSimulator.debug: + print("s0 = " + str(s0)) + + t = input.LA(1) + + while True: # while more work + D = self.getExistingTargetState(previousD, t) + if D is None: + D = self.computeTargetState(dfa, previousD, t) + if D is self.ERROR: + # if any configs in previous dipped into outer context, that + # means that input up to t actually finished entry rule + # at least for SLL decision. Full LL doesn't dip into outer + # so don't need special case. + # We will get an error no matter what so delay until after + # decision; better error message. Also, no reachable target + # ATN states in SLL implies LL will also get nowhere. + # If conflict in states that dip out, choose min since we + # will get error no matter what. + e = self.noViableAlt(input, outerContext, previousD.configs, startIndex) + input.seek(startIndex) + alt = self.getSynValidOrSemInvalidAltThatFinishedDecisionEntryRule(previousD.configs, outerContext) + if alt!=ATN.INVALID_ALT_NUMBER: + return alt + raise e + + if D.requiresFullContext and self.predictionMode != PredictionMode.SLL: + # IF PREDS, MIGHT RESOLVE TO SINGLE ALT => SLL (or syntax error) + conflictingAlts = D.configs.conflictingAlts + if D.predicates is not None: + if ParserATNSimulator.debug: + print("DFA state has preds in DFA sim LL failover") + conflictIndex = input.index + if conflictIndex != startIndex: + input.seek(startIndex) + + conflictingAlts = self.evalSemanticContext(D.predicates, outerContext, True) + if len(conflictingAlts)==1: + if ParserATNSimulator.debug: + print("Full LL avoided") + return min(conflictingAlts) + + if conflictIndex != startIndex: + # restore the index so reporting the fallback to full + # context occurs with the index at the correct spot + input.seek(conflictIndex) + + if ParserATNSimulator.dfa_debug: + print("ctx sensitive state " + str(outerContext) +" in " + str(D)) + fullCtx = True + s0_closure = self.computeStartState(dfa.atnStartState, outerContext, fullCtx) + self.reportAttemptingFullContext(dfa, conflictingAlts, D.configs, startIndex, input.index) + alt = self.execATNWithFullContext(dfa, D, s0_closure, input, startIndex, outerContext) + return alt + + if D.isAcceptState: + if D.predicates is None: + return D.prediction + + stopIndex = input.index + input.seek(startIndex) + alts = self.evalSemanticContext(D.predicates, outerContext, True) + if len(alts)==0: + raise self.noViableAlt(input, outerContext, D.configs, startIndex) + elif len(alts)==1: + return min(alts) + else: + # report ambiguity after predicate evaluation to make sure the correct + # set of ambig alts is reported. + self.reportAmbiguity(dfa, D, startIndex, stopIndex, False, alts, D.configs) + return min(alts) + + previousD = D + + if t != Token.EOF: + input.consume() + t = input.LA(1) + + # + # Get an existing target state for an edge in the DFA. If the target state + # for the edge has not yet been computed or is otherwise not available, + # this method returns {@code null}. + # + # @param previousD The current DFA state + # @param t The next input symbol + # @return The existing target DFA state for the given input symbol + # {@code t}, or {@code null} if the target state for this edge is not + # already cached + # + def getExistingTargetState(self, previousD:DFAState, t:int): + edges = previousD.edges + if edges is None or t + 1 < 0 or t + 1 >= len(edges): + return None + else: + return edges[t + 1] + + # + # Compute a target state for an edge in the DFA, and attempt to add the + # computed state and corresponding edge to the DFA. + # + # @param dfa The DFA + # @param previousD The current DFA state + # @param t The next input symbol + # + # @return The computed target DFA state for the given input symbol + # {@code t}. If {@code t} does not lead to a valid DFA state, this method + # returns {@link #ERROR}. + # + def computeTargetState(self, dfa:DFA, previousD:DFAState, t:int): + reach = self.computeReachSet(previousD.configs, t, False) + if reach is None: + self.addDFAEdge(dfa, previousD, t, self.ERROR) + return self.ERROR + + # create new target state; we'll add to DFA after it's complete + D = DFAState(configs=reach) + + predictedAlt = self.getUniqueAlt(reach) + + if ParserATNSimulator.debug: + altSubSets = PredictionMode.getConflictingAltSubsets(reach) + print("SLL altSubSets=" + str(altSubSets) + ", configs=" + str(reach) + + ", predict=" + str(predictedAlt) + ", allSubsetsConflict=" + + str(PredictionMode.allSubsetsConflict(altSubSets)) + ", conflictingAlts=" + + str(self.getConflictingAlts(reach))) + + if predictedAlt!=ATN.INVALID_ALT_NUMBER: + # NO CONFLICT, UNIQUELY PREDICTED ALT + D.isAcceptState = True + D.configs.uniqueAlt = predictedAlt + D.prediction = predictedAlt + elif PredictionMode.hasSLLConflictTerminatingPrediction(self.predictionMode, reach): + # MORE THAN ONE VIABLE ALTERNATIVE + D.configs.conflictingAlts = self.getConflictingAlts(reach) + D.requiresFullContext = True + # in SLL-only mode, we will stop at this state and return the minimum alt + D.isAcceptState = True + D.prediction = min(D.configs.conflictingAlts) + + if D.isAcceptState and D.configs.hasSemanticContext: + self.predicateDFAState(D, self.atn.getDecisionState(dfa.decision)) + if D.predicates is not None: + D.prediction = ATN.INVALID_ALT_NUMBER + + # all adds to dfa are done after we've created full D state + D = self.addDFAEdge(dfa, previousD, t, D) + return D + + def predicateDFAState(self, dfaState:DFAState, decisionState:DecisionState): + # We need to test all predicates, even in DFA states that + # uniquely predict alternative. + nalts = len(decisionState.transitions) + # Update DFA so reach becomes accept state with (predicate,alt) + # pairs if preds found for conflicting alts + altsToCollectPredsFrom = self.getConflictingAltsOrUniqueAlt(dfaState.configs) + altToPred = self.getPredsForAmbigAlts(altsToCollectPredsFrom, dfaState.configs, nalts) + if altToPred is not None: + dfaState.predicates = self.getPredicatePredictions(altsToCollectPredsFrom, altToPred) + dfaState.prediction = ATN.INVALID_ALT_NUMBER # make sure we use preds + else: + # There are preds in configs but they might go away + # when OR'd together like {p}? || NONE == NONE. If neither + # alt has preds, resolve to min alt + dfaState.prediction = min(altsToCollectPredsFrom) + + # comes back with reach.uniqueAlt set to a valid alt + def execATNWithFullContext(self, dfa:DFA, D:DFAState, # how far we got before failing over + s0:ATNConfigSet, + input:TokenStream, + startIndex:int, + outerContext:ParserRuleContext): + if ParserATNSimulator.debug or ParserATNSimulator.debug_list_atn_decisions: + print("execATNWithFullContext", str(s0)) + fullCtx = True + foundExactAmbig = False + reach = None + previous = s0 + input.seek(startIndex) + t = input.LA(1) + predictedAlt = -1 + while (True): # while more work + reach = self.computeReachSet(previous, t, fullCtx) + if reach is None: + # if any configs in previous dipped into outer context, that + # means that input up to t actually finished entry rule + # at least for LL decision. Full LL doesn't dip into outer + # so don't need special case. + # We will get an error no matter what so delay until after + # decision; better error message. Also, no reachable target + # ATN states in SLL implies LL will also get nowhere. + # If conflict in states that dip out, choose min since we + # will get error no matter what. + e = self.noViableAlt(input, outerContext, previous, startIndex) + input.seek(startIndex) + alt = self.getSynValidOrSemInvalidAltThatFinishedDecisionEntryRule(previous, outerContext) + if alt!=ATN.INVALID_ALT_NUMBER: + return alt + else: + raise e + + altSubSets = PredictionMode.getConflictingAltSubsets(reach) + if ParserATNSimulator.debug: + print("LL altSubSets=" + str(altSubSets) + ", predict=" + + str(PredictionMode.getUniqueAlt(altSubSets)) + ", resolvesToJustOneViableAlt=" + + str(PredictionMode.resolvesToJustOneViableAlt(altSubSets))) + + reach.uniqueAlt = self.getUniqueAlt(reach) + # unique prediction? + if reach.uniqueAlt!=ATN.INVALID_ALT_NUMBER: + predictedAlt = reach.uniqueAlt + break + elif self.predictionMode is not PredictionMode.LL_EXACT_AMBIG_DETECTION: + predictedAlt = PredictionMode.resolvesToJustOneViableAlt(altSubSets) + if predictedAlt != ATN.INVALID_ALT_NUMBER: + break + else: + # In exact ambiguity mode, we never try to terminate early. + # Just keeps scarfing until we know what the conflict is + if PredictionMode.allSubsetsConflict(altSubSets) and PredictionMode.allSubsetsEqual(altSubSets): + foundExactAmbig = True + predictedAlt = PredictionMode.getSingleViableAlt(altSubSets) + break + # else there are multiple non-conflicting subsets or + # we're not sure what the ambiguity is yet. + # So, keep going. + + previous = reach + if t != Token.EOF: + input.consume() + t = input.LA(1) + + # If the configuration set uniquely predicts an alternative, + # without conflict, then we know that it's a full LL decision + # not SLL. + if reach.uniqueAlt != ATN.INVALID_ALT_NUMBER : + self.reportContextSensitivity(dfa, predictedAlt, reach, startIndex, input.index) + return predictedAlt + + # We do not check predicates here because we have checked them + # on-the-fly when doing full context prediction. + + # + # In non-exact ambiguity detection mode, we might actually be able to + # detect an exact ambiguity, but I'm not going to spend the cycles + # needed to check. We only emit ambiguity warnings in exact ambiguity + # mode. + # + # For example, we might know that we have conflicting configurations. + # But, that does not mean that there is no way forward without a + # conflict. It's possible to have nonconflicting alt subsets as in: + + # altSubSets=[{1, 2}, {1, 2}, {1}, {1, 2}] + + # from + # + # [(17,1,[5 $]), (13,1,[5 10 $]), (21,1,[5 10 $]), (11,1,[$]), + # (13,2,[5 10 $]), (21,2,[5 10 $]), (11,2,[$])] + # + # In this case, (17,1,[5 $]) indicates there is some next sequence that + # would resolve this without conflict to alternative 1. Any other viable + # next sequence, however, is associated with a conflict. We stop + # looking for input because no amount of further lookahead will alter + # the fact that we should predict alternative 1. We just can't say for + # sure that there is an ambiguity without looking further. + + self.reportAmbiguity(dfa, D, startIndex, input.index, foundExactAmbig, None, reach) + + return predictedAlt + + def computeReachSet(self, closure:ATNConfigSet, t:int, fullCtx:bool): + if ParserATNSimulator.debug: + print("in computeReachSet, starting closure: " + str(closure)) + + if self.mergeCache is None: + self.mergeCache = dict() + + intermediate = ATNConfigSet(fullCtx) + + # Configurations already in a rule stop state indicate reaching the end + # of the decision rule (local context) or end of the start rule (full + # context). Once reached, these configurations are never updated by a + # closure operation, so they are handled separately for the performance + # advantage of having a smaller intermediate set when calling closure. + # + # For full-context reach operations, separate handling is required to + # ensure that the alternative matching the longest overall sequence is + # chosen when multiple such configurations can match the input. + + skippedStopStates = None + + # First figure out where we can reach on input t + for c in closure: + if ParserATNSimulator.debug: + print("testing " + self.getTokenName(t) + " at " + str(c)) + + if isinstance(c.state, RuleStopState): + if fullCtx or t == Token.EOF: + if skippedStopStates is None: + skippedStopStates = list() + skippedStopStates.append(c) + continue + + for trans in c.state.transitions: + target = self.getReachableTarget(trans, t) + if target is not None: + intermediate.add(ATNConfig(state=target, config=c), self.mergeCache) + + # Now figure out where the reach operation can take us... + + reach = None + + # This block optimizes the reach operation for intermediate sets which + # trivially indicate a termination state for the overall + # adaptivePredict operation. + # + # The conditions assume that intermediate + # contains all configurations relevant to the reach set, but this + # condition is not true when one or more configurations have been + # withheld in skippedStopStates, or when the current symbol is EOF. + # + if skippedStopStates is None and t!=Token.EOF: + if len(intermediate)==1: + # Don't pursue the closure if there is just one state. + # It can only have one alternative; just add to result + # Also don't pursue the closure if there is unique alternative + # among the configurations. + reach = intermediate + elif self.getUniqueAlt(intermediate)!=ATN.INVALID_ALT_NUMBER: + # Also don't pursue the closure if there is unique alternative + # among the configurations. + reach = intermediate + + # If the reach set could not be trivially determined, perform a closure + # operation on the intermediate set to compute its initial value. + # + if reach is None: + reach = ATNConfigSet(fullCtx) + closureBusy = set() + treatEofAsEpsilon = t == Token.EOF + for c in intermediate: + self.closure(c, reach, closureBusy, False, fullCtx, treatEofAsEpsilon) + + if t == Token.EOF: + # After consuming EOF no additional input is possible, so we are + # only interested in configurations which reached the end of the + # decision rule (local context) or end of the start rule (full + # context). Update reach to contain only these configurations. This + # handles both explicit EOF transitions in the grammar and implicit + # EOF transitions following the end of the decision or start rule. + # + # When reach==intermediate, no closure operation was performed. In + # this case, removeAllConfigsNotInRuleStopState needs to check for + # reachable rule stop states as well as configurations already in + # a rule stop state. + # + # This is handled before the configurations in skippedStopStates, + # because any configurations potentially added from that list are + # already guaranteed to meet this condition whether or not it's + # required. + # + reach = self.removeAllConfigsNotInRuleStopState(reach, reach is intermediate) + + # If skippedStopStates is not null, then it contains at least one + # configuration. For full-context reach operations, these + # configurations reached the end of the start rule, in which case we + # only add them back to reach if no configuration during the current + # closure operation reached such a state. This ensures adaptivePredict + # chooses an alternative matching the longest overall sequence when + # multiple alternatives are viable. + # + if skippedStopStates is not None and ( (not fullCtx) or (not PredictionMode.hasConfigInRuleStopState(reach))): + for c in skippedStopStates: + reach.add(c, self.mergeCache) + if len(reach)==0: + return None + else: + return reach + + # + # Return a configuration set containing only the configurations from + # {@code configs} which are in a {@link RuleStopState}. If all + # configurations in {@code configs} are already in a rule stop state, this + # method simply returns {@code configs}. + # + #

      When {@code lookToEndOfRule} is true, this method uses + # {@link ATN#nextTokens} for each configuration in {@code configs} which is + # not already in a rule stop state to see if a rule stop state is reachable + # from the configuration via epsilon-only transitions.

      + # + # @param configs the configuration set to update + # @param lookToEndOfRule when true, this method checks for rule stop states + # reachable by epsilon-only transitions from each configuration in + # {@code configs}. + # + # @return {@code configs} if all configurations in {@code configs} are in a + # rule stop state, otherwise return a new configuration set containing only + # the configurations from {@code configs} which are in a rule stop state + # + def removeAllConfigsNotInRuleStopState(self, configs:ATNConfigSet, lookToEndOfRule:bool): + if PredictionMode.allConfigsInRuleStopStates(configs): + return configs + result = ATNConfigSet(configs.fullCtx) + for config in configs: + if isinstance(config.state, RuleStopState): + result.add(config, self.mergeCache) + continue + if lookToEndOfRule and config.state.epsilonOnlyTransitions: + nextTokens = self.atn.nextTokens(config.state) + if Token.EPSILON in nextTokens: + endOfRuleState = self.atn.ruleToStopState[config.state.ruleIndex] + result.add(ATNConfig(state=endOfRuleState, config=config), self.mergeCache) + return result + + def computeStartState(self, p:ATNState, ctx:RuleContext, fullCtx:bool): + # always at least the implicit call to start rule + initialContext = PredictionContextFromRuleContext(self.atn, ctx) + configs = ATNConfigSet(fullCtx) + + for i in range(0, len(p.transitions)): + target = p.transitions[i].target + c = ATNConfig(target, i+1, initialContext) + closureBusy = set() + self.closure(c, configs, closureBusy, True, fullCtx, False) + return configs + + # + # This method transforms the start state computed by + # {@link #computeStartState} to the special start state used by a + # precedence DFA for a particular precedence value. The transformation + # process applies the following changes to the start state's configuration + # set. + # + #
        + #
      1. Evaluate the precedence predicates for each configuration using + # {@link SemanticContext#evalPrecedence}.
      2. + #
      3. Remove all configurations which predict an alternative greater than + # 1, for which another configuration that predicts alternative 1 is in the + # same ATN state with the same prediction context. This transformation is + # valid for the following reasons: + #
          + #
        • The closure block cannot contain any epsilon transitions which bypass + # the body of the closure, so all states reachable via alternative 1 are + # part of the precedence alternatives of the transformed left-recursive + # rule.
        • + #
        • The "primary" portion of a left recursive rule cannot contain an + # epsilon transition, so the only way an alternative other than 1 can exist + # in a state that is also reachable via alternative 1 is by nesting calls + # to the left-recursive rule, with the outer calls not being at the + # preferred precedence level.
        • + #
        + #
      4. + #
      + # + #

      + # The prediction context must be considered by this filter to address + # situations like the following. + #

      + # + #
      +    # grammar TA;
      +    # prog: statement* EOF;
      +    # statement: letterA | statement letterA 'b' ;
      +    # letterA: 'a';
      +    # 
      + #
      + #

      + # If the above grammar, the ATN state immediately before the token + # reference {@code 'a'} in {@code letterA} is reachable from the left edge + # of both the primary and closure blocks of the left-recursive rule + # {@code statement}. The prediction context associated with each of these + # configurations distinguishes between them, and prevents the alternative + # which stepped out to {@code prog} (and then back in to {@code statement} + # from being eliminated by the filter. + #

      + # + # @param configs The configuration set computed by + # {@link #computeStartState} as the start state for the DFA. + # @return The transformed configuration set representing the start state + # for a precedence DFA at a particular precedence level (determined by + # calling {@link Parser#getPrecedence}). + # + def applyPrecedenceFilter(self, configs:ATNConfigSet): + statesFromAlt1 = dict() + configSet = ATNConfigSet(configs.fullCtx) + for config in configs: + # handle alt 1 first + if config.alt != 1: + continue + updatedContext = config.semanticContext.evalPrecedence(self.parser, self._outerContext) + if updatedContext is None: + # the configuration was eliminated + continue + + statesFromAlt1[config.state.stateNumber] = config.context + if updatedContext is not config.semanticContext: + configSet.add(ATNConfig(config=config, semantic=updatedContext), self.mergeCache) + else: + configSet.add(config, self.mergeCache) + + for config in configs: + if config.alt == 1: + # already handled + continue + + # In the future, this elimination step could be updated to also + # filter the prediction context for alternatives predicting alt>1 + # (basically a graph subtraction algorithm). + # + if not config.precedenceFilterSuppressed: + context = statesFromAlt1.get(config.state.stateNumber, None) + if context==config.context: + # eliminated + continue + + configSet.add(config, self.mergeCache) + + return configSet + + def getReachableTarget(self, trans:Transition, ttype:int): + if trans.matches(ttype, 0, self.atn.maxTokenType): + return trans.target + else: + return None + + def getPredsForAmbigAlts(self, ambigAlts:set, configs:ATNConfigSet, nalts:int): + # REACH=[1|1|[]|0:0, 1|2|[]|0:1] + # altToPred starts as an array of all null contexts. The entry at index i + # corresponds to alternative i. altToPred[i] may have one of three values: + # 1. null: no ATNConfig c is found such that c.alt==i + # 2. SemanticContext.NONE: At least one ATNConfig c exists such that + # c.alt==i and c.semanticContext==SemanticContext.NONE. In other words, + # alt i has at least one unpredicated config. + # 3. Non-NONE Semantic Context: There exists at least one, and for all + # ATNConfig c such that c.alt==i, c.semanticContext!=SemanticContext.NONE. + # + # From this, it is clear that NONE||anything==NONE. + # + altToPred = [None] * (nalts + 1) + for c in configs: + if c.alt in ambigAlts: + altToPred[c.alt] = orContext(altToPred[c.alt], c.semanticContext) + + nPredAlts = 0 + for i in range(1, nalts+1): + if altToPred[i] is None: + altToPred[i] = SemanticContext.NONE + elif altToPred[i] is not SemanticContext.NONE: + nPredAlts += 1 + + # nonambig alts are null in altToPred + if nPredAlts==0: + altToPred = None + if ParserATNSimulator.debug: + print("getPredsForAmbigAlts result " + str_list(altToPred)) + return altToPred + + def getPredicatePredictions(self, ambigAlts:set, altToPred:list): + pairs = [] + containsPredicate = False + for i in range(1, len(altToPred)): + pred = altToPred[i] + # unpredicated is indicated by SemanticContext.NONE + if ambigAlts is not None and i in ambigAlts: + pairs.append(PredPrediction(pred, i)) + if pred is not SemanticContext.NONE: + containsPredicate = True + + if not containsPredicate: + return None + + return pairs + + # + # This method is used to improve the localization of error messages by + # choosing an alternative rather than throwing a + # {@link NoViableAltException} in particular prediction scenarios where the + # {@link #ERROR} state was reached during ATN simulation. + # + #

      + # The default implementation of this method uses the following + # algorithm to identify an ATN configuration which successfully parsed the + # decision entry rule. Choosing such an alternative ensures that the + # {@link ParserRuleContext} returned by the calling rule will be complete + # and valid, and the syntax error will be reported later at a more + # localized location.

      + # + #
        + #
      • If a syntactically valid path or paths reach the end of the decision rule and + # they are semantically valid if predicated, return the min associated alt.
      • + #
      • Else, if a semantically invalid but syntactically valid path exist + # or paths exist, return the minimum associated alt. + #
      • + #
      • Otherwise, return {@link ATN#INVALID_ALT_NUMBER}.
      • + #
      + # + #

      + # In some scenarios, the algorithm described above could predict an + # alternative which will result in a {@link FailedPredicateException} in + # the parser. Specifically, this could occur if the only configuration + # capable of successfully parsing to the end of the decision rule is + # blocked by a semantic predicate. By choosing this alternative within + # {@link #adaptivePredict} instead of throwing a + # {@link NoViableAltException}, the resulting + # {@link FailedPredicateException} in the parser will identify the specific + # predicate which is preventing the parser from successfully parsing the + # decision rule, which helps developers identify and correct logic errors + # in semantic predicates. + #

      + # + # @param configs The ATN configurations which were valid immediately before + # the {@link #ERROR} state was reached + # @param outerContext The is the \gamma_0 initial parser context from the paper + # or the parser stack at the instant before prediction commences. + # + # @return The value to return from {@link #adaptivePredict}, or + # {@link ATN#INVALID_ALT_NUMBER} if a suitable alternative was not + # identified and {@link #adaptivePredict} should report an error instead. + # + def getSynValidOrSemInvalidAltThatFinishedDecisionEntryRule(self, configs:ATNConfigSet, outerContext:ParserRuleContext): + semValidConfigs, semInvalidConfigs = self.splitAccordingToSemanticValidity(configs, outerContext) + alt = self.getAltThatFinishedDecisionEntryRule(semValidConfigs) + if alt!=ATN.INVALID_ALT_NUMBER: # semantically/syntactically viable path exists + return alt + # Is there a syntactically valid path with a failed pred? + if len(semInvalidConfigs)>0: + alt = self.getAltThatFinishedDecisionEntryRule(semInvalidConfigs) + if alt!=ATN.INVALID_ALT_NUMBER: # syntactically viable path exists + return alt + return ATN.INVALID_ALT_NUMBER + + def getAltThatFinishedDecisionEntryRule(self, configs:ATNConfigSet): + alts = set() + for c in configs: + if c.reachesIntoOuterContext>0 or (isinstance(c.state, RuleStopState) and c.context.hasEmptyPath() ): + alts.add(c.alt) + if len(alts)==0: + return ATN.INVALID_ALT_NUMBER + else: + return min(alts) + + # Walk the list of configurations and split them according to + # those that have preds evaluating to true/false. If no pred, assume + # true pred and include in succeeded set. Returns Pair of sets. + # + # Create a new set so as not to alter the incoming parameter. + # + # Assumption: the input stream has been restored to the starting point + # prediction, which is where predicates need to evaluate. + # + def splitAccordingToSemanticValidity(self, configs:ATNConfigSet, outerContext:ParserRuleContext): + succeeded = ATNConfigSet(configs.fullCtx) + failed = ATNConfigSet(configs.fullCtx) + for c in configs: + if c.semanticContext is not SemanticContext.NONE: + predicateEvaluationResult = c.semanticContext.eval(self.parser, outerContext) + if predicateEvaluationResult: + succeeded.add(c) + else: + failed.add(c) + else: + succeeded.add(c) + return (succeeded,failed) + + # Look through a list of predicate/alt pairs, returning alts for the + # pairs that win. A {@code NONE} predicate indicates an alt containing an + # unpredicated config which behaves as "always true." If !complete + # then we stop at the first predicate that evaluates to true. This + # includes pairs with null predicates. + # + def evalSemanticContext(self, predPredictions:list, outerContext:ParserRuleContext, complete:bool): + predictions = set() + for pair in predPredictions: + if pair.pred is SemanticContext.NONE: + predictions.add(pair.alt) + if not complete: + break + continue + predicateEvaluationResult = pair.pred.eval(self.parser, outerContext) + if ParserATNSimulator.debug or ParserATNSimulator.dfa_debug: + print("eval pred " + str(pair) + "=" + str(predicateEvaluationResult)) + + if predicateEvaluationResult: + if ParserATNSimulator.debug or ParserATNSimulator.dfa_debug: + print("PREDICT " + str(pair.alt)) + predictions.add(pair.alt) + if not complete: + break + return predictions + + + # TODO: If we are doing predicates, there is no point in pursuing + # closure operations if we reach a DFA state that uniquely predicts + # alternative. We will not be caching that DFA state and it is a + # waste to pursue the closure. Might have to advance when we do + # ambig detection thought :( + # + + def closure(self, config:ATNConfig, configs:ATNConfigSet, closureBusy:set, collectPredicates:bool, fullCtx:bool, treatEofAsEpsilon:bool): + initialDepth = 0 + self.closureCheckingStopState(config, configs, closureBusy, collectPredicates, + fullCtx, initialDepth, treatEofAsEpsilon) + + + def closureCheckingStopState(self, config:ATNConfig, configs:ATNConfigSet, closureBusy:set, collectPredicates:bool, fullCtx:bool, depth:int, treatEofAsEpsilon:bool): + if ParserATNSimulator.debug: + print("closure(" + str(config) + ")") + + if isinstance(config.state, RuleStopState): + # We hit rule end. If we have context info, use it + # run thru all possible stack tops in ctx + if not config.context.isEmpty(): + for i in range(0, len(config.context)): + state = config.context.getReturnState(i) + if state is PredictionContext.EMPTY_RETURN_STATE: + if fullCtx: + configs.add(ATNConfig(state=config.state, context=PredictionContext.EMPTY, config=config), self.mergeCache) + continue + else: + # we have no context info, just chase follow links (if greedy) + if ParserATNSimulator.debug: + print("FALLING off rule " + self.getRuleName(config.state.ruleIndex)) + self.closure_(config, configs, closureBusy, collectPredicates, + fullCtx, depth, treatEofAsEpsilon) + continue + returnState = self.atn.states[state] + newContext = config.context.getParent(i) # "pop" return state + c = ATNConfig(state=returnState, alt=config.alt, context=newContext, semantic=config.semanticContext) + # While we have context to pop back from, we may have + # gotten that context AFTER having falling off a rule. + # Make sure we track that we are now out of context. + c.reachesIntoOuterContext = config.reachesIntoOuterContext + self.closureCheckingStopState(c, configs, closureBusy, collectPredicates, fullCtx, depth - 1, treatEofAsEpsilon) + return + elif fullCtx: + # reached end of start rule + configs.add(config, self.mergeCache) + return + else: + # else if we have no context info, just chase follow links (if greedy) + if ParserATNSimulator.debug: + print("FALLING off rule " + self.getRuleName(config.state.ruleIndex)) + + self.closure_(config, configs, closureBusy, collectPredicates, fullCtx, depth, treatEofAsEpsilon) + + # Do the actual work of walking epsilon edges# + def closure_(self, config:ATNConfig, configs:ATNConfigSet, closureBusy:set, collectPredicates:bool, fullCtx:bool, depth:int, treatEofAsEpsilon:bool): + p = config.state + # optimization + if not p.epsilonOnlyTransitions: + configs.add(config, self.mergeCache) + # make sure to not return here, because EOF transitions can act as + # both epsilon transitions and non-epsilon transitions. + + first = True + for t in p.transitions: + if first: + first = False + if self.canDropLoopEntryEdgeInLeftRecursiveRule(config): + continue + + continueCollecting = collectPredicates and not isinstance(t, ActionTransition) + c = self.getEpsilonTarget(config, t, continueCollecting, depth == 0, fullCtx, treatEofAsEpsilon) + if c is not None: + newDepth = depth + if isinstance( config.state, RuleStopState): + # target fell off end of rule; mark resulting c as having dipped into outer context + # We can't get here if incoming config was rule stop and we had context + # track how far we dip into outer context. Might + # come in handy and we avoid evaluating context dependent + # preds if this is > 0. + if self._dfa is not None and self._dfa.precedenceDfa: + if t.outermostPrecedenceReturn == self._dfa.atnStartState.ruleIndex: + c.precedenceFilterSuppressed = True + c.reachesIntoOuterContext += 1 + if c in closureBusy: + # avoid infinite recursion for right-recursive rules + continue + closureBusy.add(c) + configs.dipsIntoOuterContext = True # TODO: can remove? only care when we add to set per middle of this method + newDepth -= 1 + if ParserATNSimulator.debug: + print("dips into outer ctx: " + str(c)) + else: + if not t.isEpsilon: + if c in closureBusy: + # avoid infinite recursion for EOF* and EOF+ + continue + closureBusy.add(c) + if isinstance(t, RuleTransition): + # latch when newDepth goes negative - once we step out of the entry context we can't return + if newDepth >= 0: + newDepth += 1 + + self.closureCheckingStopState(c, configs, closureBusy, continueCollecting, fullCtx, newDepth, treatEofAsEpsilon) + + + + # Implements first-edge (loop entry) elimination as an optimization + # during closure operations. See antlr/antlr4#1398. + # + # The optimization is to avoid adding the loop entry config when + # the exit path can only lead back to the same + # StarLoopEntryState after popping context at the rule end state + # (traversing only epsilon edges, so we're still in closure, in + # this same rule). + # + # We need to detect any state that can reach loop entry on + # epsilon w/o exiting rule. We don't have to look at FOLLOW + # links, just ensure that all stack tops for config refer to key + # states in LR rule. + # + # To verify we are in the right situation we must first check + # closure is at a StarLoopEntryState generated during LR removal. + # Then we check that each stack top of context is a return state + # from one of these cases: + # + # 1. 'not' expr, '(' type ')' expr. The return state points at loop entry state + # 2. expr op expr. The return state is the block end of internal block of (...)* + # 3. 'between' expr 'and' expr. The return state of 2nd expr reference. + # That state points at block end of internal block of (...)*. + # 4. expr '?' expr ':' expr. The return state points at block end, + # which points at loop entry state. + # + # If any is true for each stack top, then closure does not add a + # config to the current config set for edge[0], the loop entry branch. + # + # Conditions fail if any context for the current config is: + # + # a. empty (we'd fall out of expr to do a global FOLLOW which could + # even be to some weird spot in expr) or, + # b. lies outside of expr or, + # c. lies within expr but at a state not the BlockEndState + # generated during LR removal + # + # Do we need to evaluate predicates ever in closure for this case? + # + # No. Predicates, including precedence predicates, are only + # evaluated when computing a DFA start state. I.e., only before + # the lookahead (but not parser) consumes a token. + # + # There are no epsilon edges allowed in LR rule alt blocks or in + # the "primary" part (ID here). If closure is in + # StarLoopEntryState any lookahead operation will have consumed a + # token as there are no epsilon-paths that lead to + # StarLoopEntryState. We do not have to evaluate predicates + # therefore if we are in the generated StarLoopEntryState of a LR + # rule. Note that when making a prediction starting at that + # decision point, decision d=2, compute-start-state performs + # closure starting at edges[0], edges[1] emanating from + # StarLoopEntryState. That means it is not performing closure on + # StarLoopEntryState during compute-start-state. + # + # How do we know this always gives same prediction answer? + # + # Without predicates, loop entry and exit paths are ambiguous + # upon remaining input +b (in, say, a+b). Either paths lead to + # valid parses. Closure can lead to consuming + immediately or by + # falling out of this call to expr back into expr and loop back + # again to StarLoopEntryState to match +b. In this special case, + # we choose the more efficient path, which is to take the bypass + # path. + # + # The lookahead language has not changed because closure chooses + # one path over the other. Both paths lead to consuming the same + # remaining input during a lookahead operation. If the next token + # is an operator, lookahead will enter the choice block with + # operators. If it is not, lookahead will exit expr. Same as if + # closure had chosen to enter the choice block immediately. + # + # Closure is examining one config (some loopentrystate, some alt, + # context) which means it is considering exactly one alt. Closure + # always copies the same alt to any derived configs. + # + # How do we know this optimization doesn't mess up precedence in + # our parse trees? + # + # Looking through expr from left edge of stat only has to confirm + # that an input, say, a+b+c; begins with any valid interpretation + # of an expression. The precedence actually doesn't matter when + # making a decision in stat seeing through expr. It is only when + # parsing rule expr that we must use the precedence to get the + # right interpretation and, hence, parse tree. + # + # @since 4.6 + # + def canDropLoopEntryEdgeInLeftRecursiveRule(self, config): + # return False + p = config.state + # First check to see if we are in StarLoopEntryState generated during + # left-recursion elimination. For efficiency, also check if + # the context has an empty stack case. If so, it would mean + # global FOLLOW so we can't perform optimization + # Are we the special loop entry/exit state? or SLL wildcard + if p.stateType != ATNState.STAR_LOOP_ENTRY \ + or not p.isPrecedenceDecision \ + or config.context.isEmpty() \ + or config.context.hasEmptyPath(): + return False + + # Require all return states to return back to the same rule + # that p is in. + numCtxs = len(config.context) + for i in range(0, numCtxs): # for each stack context + returnState = self.atn.states[config.context.getReturnState(i)] + if returnState.ruleIndex != p.ruleIndex: + return False + + decisionStartState = p.transitions[0].target + blockEndStateNum = decisionStartState.endState.stateNumber + blockEndState = self.atn.states[blockEndStateNum] + + # Verify that the top of each stack context leads to loop entry/exit + # state through epsilon edges and w/o leaving rule. + for i in range(0, numCtxs): # for each stack context + returnStateNumber = config.context.getReturnState(i) + returnState = self.atn.states[returnStateNumber] + # all states must have single outgoing epsilon edge + if len(returnState.transitions) != 1 or not returnState.transitions[0].isEpsilon: + return False + + # Look for prefix op case like 'not expr', (' type ')' expr + returnStateTarget = returnState.transitions[0].target + if returnState.stateType == ATNState.BLOCK_END and returnStateTarget is p: + continue + + # Look for 'expr op expr' or case where expr's return state is block end + # of (...)* internal block; the block end points to loop back + # which points to p but we don't need to check that + if returnState is blockEndState: + continue + + # Look for ternary expr ? expr : expr. The return state points at block end, + # which points at loop entry state + if returnStateTarget is blockEndState: + continue + + # Look for complex prefix 'between expr and expr' case where 2nd expr's + # return state points at block end state of (...)* internal block + if returnStateTarget.stateType == ATNState.BLOCK_END \ + and len(returnStateTarget.transitions) == 1 \ + and returnStateTarget.transitions[0].isEpsilon \ + and returnStateTarget.transitions[0].target is p: + continue + + # anything else ain't conforming + return False + + return True + + + def getRuleName(self, index:int): + if self.parser is not None and index>=0: + return self.parser.ruleNames[index] + else: + return "" + + epsilonTargetMethods = dict() + epsilonTargetMethods[Transition.RULE] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + sim.ruleTransition(config, t) + epsilonTargetMethods[Transition.PRECEDENCE] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + sim.precedenceTransition(config, t, collectPredicates, inContext, fullCtx) + epsilonTargetMethods[Transition.PREDICATE] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + sim.predTransition(config, t, collectPredicates, inContext, fullCtx) + epsilonTargetMethods[Transition.ACTION] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + sim.actionTransition(config, t) + epsilonTargetMethods[Transition.EPSILON] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + ATNConfig(state=t.target, config=config) + epsilonTargetMethods[Transition.ATOM] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + ATNConfig(state=t.target, config=config) if treatEofAsEpsilon and t.matches(Token.EOF, 0, 1) else None + epsilonTargetMethods[Transition.RANGE] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + ATNConfig(state=t.target, config=config) if treatEofAsEpsilon and t.matches(Token.EOF, 0, 1) else None + epsilonTargetMethods[Transition.SET] = lambda sim, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon: \ + ATNConfig(state=t.target, config=config) if treatEofAsEpsilon and t.matches(Token.EOF, 0, 1) else None + + def getEpsilonTarget(self, config:ATNConfig, t:Transition, collectPredicates:bool, inContext:bool, fullCtx:bool, treatEofAsEpsilon:bool): + m = self.epsilonTargetMethods.get(t.serializationType, None) + if m is None: + return None + else: + return m(self, config, t, collectPredicates, inContext, fullCtx, treatEofAsEpsilon) + + def actionTransition(self, config:ATNConfig, t:ActionTransition): + if ParserATNSimulator.debug: + print("ACTION edge " + str(t.ruleIndex) + ":" + str(t.actionIndex)) + return ATNConfig(state=t.target, config=config) + + def precedenceTransition(self, config:ATNConfig, pt:PrecedencePredicateTransition, collectPredicates:bool, inContext:bool, fullCtx:bool): + if ParserATNSimulator.debug: + print("PRED (collectPredicates=" + str(collectPredicates) + ") " + + str(pt.precedence) + ">=_p, ctx dependent=true") + if self.parser is not None: + print("context surrounding pred is " + str(self.parser.getRuleInvocationStack())) + + c = None + if collectPredicates and inContext: + if fullCtx: + # In full context mode, we can evaluate predicates on-the-fly + # during closure, which dramatically reduces the size of + # the config sets. It also obviates the need to test predicates + # later during conflict resolution. + currentPosition = self._input.index + self._input.seek(self._startIndex) + predSucceeds = pt.getPredicate().eval(self.parser, self._outerContext) + self._input.seek(currentPosition) + if predSucceeds: + c = ATNConfig(state=pt.target, config=config) # no pred context + else: + newSemCtx = andContext(config.semanticContext, pt.getPredicate()) + c = ATNConfig(state=pt.target, semantic=newSemCtx, config=config) + else: + c = ATNConfig(state=pt.target, config=config) + + if ParserATNSimulator.debug: + print("config from pred transition=" + str(c)) + return c + + def predTransition(self, config:ATNConfig, pt:PredicateTransition, collectPredicates:bool, inContext:bool, fullCtx:bool): + if ParserATNSimulator.debug: + print("PRED (collectPredicates=" + str(collectPredicates) + ") " + str(pt.ruleIndex) + + ":" + str(pt.predIndex) + ", ctx dependent=" + str(pt.isCtxDependent)) + if self.parser is not None: + print("context surrounding pred is " + str(self.parser.getRuleInvocationStack())) + + c = None + if collectPredicates and (not pt.isCtxDependent or (pt.isCtxDependent and inContext)): + if fullCtx: + # In full context mode, we can evaluate predicates on-the-fly + # during closure, which dramatically reduces the size of + # the config sets. It also obviates the need to test predicates + # later during conflict resolution. + currentPosition = self._input.index + self._input.seek(self._startIndex) + predSucceeds = pt.getPredicate().eval(self.parser, self._outerContext) + self._input.seek(currentPosition) + if predSucceeds: + c = ATNConfig(state=pt.target, config=config) # no pred context + else: + newSemCtx = andContext(config.semanticContext, pt.getPredicate()) + c = ATNConfig(state=pt.target, semantic=newSemCtx, config=config) + else: + c = ATNConfig(state=pt.target, config=config) + + if ParserATNSimulator.debug: + print("config from pred transition=" + str(c)) + return c + + def ruleTransition(self, config:ATNConfig, t:RuleTransition): + if ParserATNSimulator.debug: + print("CALL rule " + self.getRuleName(t.target.ruleIndex) + ", ctx=" + str(config.context)) + returnState = t.followState + newContext = SingletonPredictionContext.create(config.context, returnState.stateNumber) + return ATNConfig(state=t.target, context=newContext, config=config ) + + def getConflictingAlts(self, configs:ATNConfigSet): + altsets = PredictionMode.getConflictingAltSubsets(configs) + return PredictionMode.getAlts(altsets) + + # Sam pointed out a problem with the previous definition, v3, of + # ambiguous states. If we have another state associated with conflicting + # alternatives, we should keep going. For example, the following grammar + # + # s : (ID | ID ID?) ';' ; + # + # When the ATN simulation reaches the state before ';', it has a DFA + # state that looks like: [12|1|[], 6|2|[], 12|2|[]]. Naturally + # 12|1|[] and 12|2|[] conflict, but we cannot stop processing this node + # because alternative to has another way to continue, via [6|2|[]]. + # The key is that we have a single state that has config's only associated + # with a single alternative, 2, and crucially the state transitions + # among the configurations are all non-epsilon transitions. That means + # we don't consider any conflicts that include alternative 2. So, we + # ignore the conflict between alts 1 and 2. We ignore a set of + # conflicting alts when there is an intersection with an alternative + # associated with a single alt state in the state→config-list map. + # + # It's also the case that we might have two conflicting configurations but + # also a 3rd nonconflicting configuration for a different alternative: + # [1|1|[], 1|2|[], 8|3|[]]. This can come about from grammar: + # + # a : A | A | A B ; + # + # After matching input A, we reach the stop state for rule A, state 1. + # State 8 is the state right before B. Clearly alternatives 1 and 2 + # conflict and no amount of further lookahead will separate the two. + # However, alternative 3 will be able to continue and so we do not + # stop working on this state. In the previous example, we're concerned + # with states associated with the conflicting alternatives. Here alt + # 3 is not associated with the conflicting configs, but since we can continue + # looking for input reasonably, I don't declare the state done. We + # ignore a set of conflicting alts when we have an alternative + # that we still need to pursue. + # + + def getConflictingAltsOrUniqueAlt(self, configs:ATNConfigSet): + conflictingAlts = None + if configs.uniqueAlt!= ATN.INVALID_ALT_NUMBER: + conflictingAlts = set() + conflictingAlts.add(configs.uniqueAlt) + else: + conflictingAlts = configs.conflictingAlts + return conflictingAlts + + def getTokenName(self, t:int): + if t==Token.EOF: + return "EOF" + if self.parser is not None and \ + self.parser.literalNames is not None and \ + t < len(self.parser.literalNames): + return self.parser.literalNames[t] + "<" + str(t) + ">" + else: + return str(t) + + def getLookaheadName(self, input:TokenStream): + return self.getTokenName(input.LA(1)) + + # Used for debugging in adaptivePredict around execATN but I cut + # it out for clarity now that alg. works well. We can leave this + # "dead" code for a bit. + # + def dumpDeadEndConfigs(self, nvae:NoViableAltException): + print("dead end configs: ") + for c in nvae.getDeadEndConfigs(): + trans = "no edges" + if len(c.state.transitions)>0: + t = c.state.transitions[0] + if isinstance(t, AtomTransition): + trans = "Atom "+ self.getTokenName(t.label) + elif isinstance(t, SetTransition): + neg = isinstance(t, NotSetTransition) + trans = ("~" if neg else "")+"Set "+ str(t.set) + print(c.toString(self.parser, True) + ":" + trans, file=sys.stderr) + + def noViableAlt(self, input:TokenStream, outerContext:ParserRuleContext, configs:ATNConfigSet, startIndex:int): + return NoViableAltException(self.parser, input, input.get(startIndex), input.LT(1), configs, outerContext) + + def getUniqueAlt(self, configs:ATNConfigSet): + alt = ATN.INVALID_ALT_NUMBER + for c in configs: + if alt == ATN.INVALID_ALT_NUMBER: + alt = c.alt # found first alt + elif c.alt!=alt: + return ATN.INVALID_ALT_NUMBER + return alt + + # + # Add an edge to the DFA, if possible. This method calls + # {@link #addDFAState} to ensure the {@code to} state is present in the + # DFA. If {@code from} is {@code null}, or if {@code t} is outside the + # range of edges that can be represented in the DFA tables, this method + # returns without adding the edge to the DFA. + # + #

      If {@code to} is {@code null}, this method returns {@code null}. + # Otherwise, this method returns the {@link DFAState} returned by calling + # {@link #addDFAState} for the {@code to} state.

      + # + # @param dfa The DFA + # @param from The source state for the edge + # @param t The input symbol + # @param to The target state for the edge + # + # @return If {@code to} is {@code null}, this method returns {@code null}; + # otherwise this method returns the result of calling {@link #addDFAState} + # on {@code to} + # + def addDFAEdge(self, dfa:DFA, from_:DFAState, t:int, to:DFAState): + if ParserATNSimulator.debug: + print("EDGE " + str(from_) + " -> " + str(to) + " upon " + self.getTokenName(t)) + + if to is None: + return None + + to = self.addDFAState(dfa, to) # used existing if possible not incoming + if from_ is None or t < -1 or t > self.atn.maxTokenType: + return to + + if from_.edges is None: + from_.edges = [None] * (self.atn.maxTokenType + 2) + from_.edges[t+1] = to # connect + + if ParserATNSimulator.debug: + names = None if self.parser is None else self.parser.literalNames + print("DFA=\n" + dfa.toString(names)) + + return to + + # + # Add state {@code D} to the DFA if it is not already present, and return + # the actual instance stored in the DFA. If a state equivalent to {@code D} + # is already in the DFA, the existing state is returned. Otherwise this + # method returns {@code D} after adding it to the DFA. + # + #

      If {@code D} is {@link #ERROR}, this method returns {@link #ERROR} and + # does not change the DFA.

      + # + # @param dfa The dfa + # @param D The DFA state to add + # @return The state stored in the DFA. This will be either the existing + # state if {@code D} is already in the DFA, or {@code D} itself if the + # state was not already present. + # + def addDFAState(self, dfa:DFA, D:DFAState): + if D is self.ERROR: + return D + + + existing = dfa.states.get(D, None) + if existing is not None: + return existing + + D.stateNumber = len(dfa.states) + if not D.configs.readonly: + D.configs.optimizeConfigs(self) + D.configs.setReadonly(True) + dfa.states[D] = D + if ParserATNSimulator.debug: + print("adding new DFA state: " + str(D)) + return D + + def reportAttemptingFullContext(self, dfa:DFA, conflictingAlts:set, configs:ATNConfigSet, startIndex:int, stopIndex:int): + if ParserATNSimulator.debug or ParserATNSimulator.retry_debug: + print("reportAttemptingFullContext decision=" + str(dfa.decision) + ":" + str(configs) + + ", input=" + self.parser.getTokenStream().getText(startIndex, stopIndex)) + if self.parser is not None: + self.parser.getErrorListenerDispatch().reportAttemptingFullContext(self.parser, dfa, startIndex, stopIndex, conflictingAlts, configs) + + def reportContextSensitivity(self, dfa:DFA, prediction:int, configs:ATNConfigSet, startIndex:int, stopIndex:int): + if ParserATNSimulator.debug or ParserATNSimulator.retry_debug: + print("reportContextSensitivity decision=" + str(dfa.decision) + ":" + str(configs) + + ", input=" + self.parser.getTokenStream().getText(startIndex, stopIndex)) + if self.parser is not None: + self.parser.getErrorListenerDispatch().reportContextSensitivity(self.parser, dfa, startIndex, stopIndex, prediction, configs) + + # If context sensitive parsing, we know it's ambiguity not conflict# + def reportAmbiguity(self, dfa:DFA, D:DFAState, startIndex:int, stopIndex:int, + exact:bool, ambigAlts:set, configs:ATNConfigSet ): + if ParserATNSimulator.debug or ParserATNSimulator.retry_debug: +# ParserATNPathFinder finder = new ParserATNPathFinder(parser, atn); +# int i = 1; +# for (Transition t : dfa.atnStartState.transitions) { +# print("ALT "+i+"="); +# print(startIndex+".."+stopIndex+", len(input)="+parser.getInputStream().size()); +# TraceTree path = finder.trace(t.target, parser.getContext(), (TokenStream)parser.getInputStream(), +# startIndex, stopIndex); +# if ( path!=null ) { +# print("path = "+path.toStringTree()); +# for (TraceTree leaf : path.leaves) { +# List states = path.getPathToNode(leaf); +# print("states="+states); +# } +# } +# i++; +# } + print("reportAmbiguity " + str(ambigAlts) + ":" + str(configs) + + ", input=" + self.parser.getTokenStream().getText(startIndex, stopIndex)) + if self.parser is not None: + self.parser.getErrorListenerDispatch().reportAmbiguity(self.parser, dfa, startIndex, stopIndex, exact, ambigAlts, configs) diff --git a/lib/python3.12/site-packages/antlr4/atn/PredictionMode.py b/lib/python3.12/site-packages/antlr4/atn/PredictionMode.py new file mode 100644 index 0000000000000000000000000000000000000000..8e5c73bb47f329d519f4e574ba3a36fc6c4ac29f --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/PredictionMode.py @@ -0,0 +1,499 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# +# +# This enumeration defines the prediction modes available in ANTLR 4 along with +# utility methods for analyzing configuration sets for conflicts and/or +# ambiguities. + + +from enum import Enum +from antlr4.atn.ATN import ATN +from antlr4.atn.ATNConfig import ATNConfig +from antlr4.atn.ATNConfigSet import ATNConfigSet +from antlr4.atn.ATNState import RuleStopState +from antlr4.atn.SemanticContext import SemanticContext + +PredictionMode = None + +class PredictionMode(Enum): + # + # The SLL(*) prediction mode. This prediction mode ignores the current + # parser context when making predictions. This is the fastest prediction + # mode, and provides correct results for many grammars. This prediction + # mode is more powerful than the prediction mode provided by ANTLR 3, but + # may result in syntax errors for grammar and input combinations which are + # not SLL. + # + #

      + # When using this prediction mode, the parser will either return a correct + # parse tree (i.e. the same parse tree that would be returned with the + # {@link #LL} prediction mode), or it will report a syntax error. If a + # syntax error is encountered when using the {@link #SLL} prediction mode, + # it may be due to either an actual syntax error in the input or indicate + # that the particular combination of grammar and input requires the more + # powerful {@link #LL} prediction abilities to complete successfully.

      + # + #

      + # This prediction mode does not provide any guarantees for prediction + # behavior for syntactically-incorrect inputs.

      + # + SLL = 0 + # + # The LL(*) prediction mode. This prediction mode allows the current parser + # context to be used for resolving SLL conflicts that occur during + # prediction. This is the fastest prediction mode that guarantees correct + # parse results for all combinations of grammars with syntactically correct + # inputs. + # + #

      + # When using this prediction mode, the parser will make correct decisions + # for all syntactically-correct grammar and input combinations. However, in + # cases where the grammar is truly ambiguous this prediction mode might not + # report a precise answer for exactly which alternatives are + # ambiguous.

      + # + #

      + # This prediction mode does not provide any guarantees for prediction + # behavior for syntactically-incorrect inputs.

      + # + LL = 1 + # + # The LL(*) prediction mode with exact ambiguity detection. In addition to + # the correctness guarantees provided by the {@link #LL} prediction mode, + # this prediction mode instructs the prediction algorithm to determine the + # complete and exact set of ambiguous alternatives for every ambiguous + # decision encountered while parsing. + # + #

      + # This prediction mode may be used for diagnosing ambiguities during + # grammar development. Due to the performance overhead of calculating sets + # of ambiguous alternatives, this prediction mode should be avoided when + # the exact results are not necessary.

      + # + #

      + # This prediction mode does not provide any guarantees for prediction + # behavior for syntactically-incorrect inputs.

      + # + LL_EXACT_AMBIG_DETECTION = 2 + + + # + # Computes the SLL prediction termination condition. + # + #

      + # This method computes the SLL prediction termination condition for both of + # the following cases.

      + # + #
        + #
      • The usual SLL+LL fallback upon SLL conflict
      • + #
      • Pure SLL without LL fallback
      • + #
      + # + #

      COMBINED SLL+LL PARSING

      + # + #

      When LL-fallback is enabled upon SLL conflict, correct predictions are + # ensured regardless of how the termination condition is computed by this + # method. Due to the substantially higher cost of LL prediction, the + # prediction should only fall back to LL when the additional lookahead + # cannot lead to a unique SLL prediction.

      + # + #

      Assuming combined SLL+LL parsing, an SLL configuration set with only + # conflicting subsets should fall back to full LL, even if the + # configuration sets don't resolve to the same alternative (e.g. + # {@code {1,2}} and {@code {3,4}}. If there is at least one non-conflicting + # configuration, SLL could continue with the hopes that more lookahead will + # resolve via one of those non-conflicting configurations.

      + # + #

      Here's the prediction termination rule them: SLL (for SLL+LL parsing) + # stops when it sees only conflicting configuration subsets. In contrast, + # full LL keeps going when there is uncertainty.

      + # + #

      HEURISTIC

      + # + #

      As a heuristic, we stop prediction when we see any conflicting subset + # unless we see a state that only has one alternative associated with it. + # The single-alt-state thing lets prediction continue upon rules like + # (otherwise, it would admit defeat too soon):

      + # + #

      {@code [12|1|[], 6|2|[], 12|2|[]]. s : (ID | ID ID?) ';' ;}

      + # + #

      When the ATN simulation reaches the state before {@code ';'}, it has a + # DFA state that looks like: {@code [12|1|[], 6|2|[], 12|2|[]]}. Naturally + # {@code 12|1|[]} and {@code 12|2|[]} conflict, but we cannot stop + # processing this node because alternative to has another way to continue, + # via {@code [6|2|[]]}.

      + # + #

      It also let's us continue for this rule:

      + # + #

      {@code [1|1|[], 1|2|[], 8|3|[]] a : A | A | A B ;}

      + # + #

      After matching input A, we reach the stop state for rule A, state 1. + # State 8 is the state right before B. Clearly alternatives 1 and 2 + # conflict and no amount of further lookahead will separate the two. + # However, alternative 3 will be able to continue and so we do not stop + # working on this state. In the previous example, we're concerned with + # states associated with the conflicting alternatives. Here alt 3 is not + # associated with the conflicting configs, but since we can continue + # looking for input reasonably, don't declare the state done.

      + # + #

      PURE SLL PARSING

      + # + #

      To handle pure SLL parsing, all we have to do is make sure that we + # combine stack contexts for configurations that differ only by semantic + # predicate. From there, we can do the usual SLL termination heuristic.

      + # + #

      PREDICATES IN SLL+LL PARSING

      + # + #

      SLL decisions don't evaluate predicates until after they reach DFA stop + # states because they need to create the DFA cache that works in all + # semantic situations. In contrast, full LL evaluates predicates collected + # during start state computation so it can ignore predicates thereafter. + # This means that SLL termination detection can totally ignore semantic + # predicates.

      + # + #

      Implementation-wise, {@link ATNConfigSet} combines stack contexts but not + # semantic predicate contexts so we might see two configurations like the + # following.

      + # + #

      {@code (s, 1, x, {}), (s, 1, x', {p})}

      + # + #

      Before testing these configurations against others, we have to merge + # {@code x} and {@code x'} (without modifying the existing configurations). + # For example, we test {@code (x+x')==x''} when looking for conflicts in + # the following configurations.

      + # + #

      {@code (s, 1, x, {}), (s, 1, x', {p}), (s, 2, x'', {})}

      + # + #

      If the configuration set has predicates (as indicated by + # {@link ATNConfigSet#hasSemanticContext}), this algorithm makes a copy of + # the configurations to strip out all of the predicates so that a standard + # {@link ATNConfigSet} will merge everything ignoring predicates.

      + # + @classmethod + def hasSLLConflictTerminatingPrediction(cls, mode:PredictionMode, configs:ATNConfigSet): + # Configs in rule stop states indicate reaching the end of the decision + # rule (local context) or end of start rule (full context). If all + # configs meet this condition, then none of the configurations is able + # to match additional input so we terminate prediction. + # + if cls.allConfigsInRuleStopStates(configs): + return True + + # pure SLL mode parsing + if mode == PredictionMode.SLL: + # Don't bother with combining configs from different semantic + # contexts if we can fail over to full LL; costs more time + # since we'll often fail over anyway. + if configs.hasSemanticContext: + # dup configs, tossing out semantic predicates + dup = ATNConfigSet() + for c in configs: + c = ATNConfig(config=c, semantic=SemanticContext.NONE) + dup.add(c) + configs = dup + # now we have combined contexts for configs with dissimilar preds + + # pure SLL or combined SLL+LL mode parsing + altsets = cls.getConflictingAltSubsets(configs) + return cls.hasConflictingAltSet(altsets) and not cls.hasStateAssociatedWithOneAlt(configs) + + # Checks if any configuration in {@code configs} is in a + # {@link RuleStopState}. Configurations meeting this condition have reached + # the end of the decision rule (local context) or end of start rule (full + # context). + # + # @param configs the configuration set to test + # @return {@code true} if any configuration in {@code configs} is in a + # {@link RuleStopState}, otherwise {@code false} + @classmethod + def hasConfigInRuleStopState(cls, configs:ATNConfigSet): + return any(isinstance(cfg.state, RuleStopState) for cfg in configs) + + # Checks if all configurations in {@code configs} are in a + # {@link RuleStopState}. Configurations meeting this condition have reached + # the end of the decision rule (local context) or end of start rule (full + # context). + # + # @param configs the configuration set to test + # @return {@code true} if all configurations in {@code configs} are in a + # {@link RuleStopState}, otherwise {@code false} + @classmethod + def allConfigsInRuleStopStates(cls, configs:ATNConfigSet): + return all(isinstance(cfg.state, RuleStopState) for cfg in configs) + + # + # Full LL prediction termination. + # + #

      Can we stop looking ahead during ATN simulation or is there some + # uncertainty as to which alternative we will ultimately pick, after + # consuming more input? Even if there are partial conflicts, we might know + # that everything is going to resolve to the same minimum alternative. That + # means we can stop since no more lookahead will change that fact. On the + # other hand, there might be multiple conflicts that resolve to different + # minimums. That means we need more look ahead to decide which of those + # alternatives we should predict.

      + # + #

      The basic idea is to split the set of configurations {@code C}, into + # conflicting subsets {@code (s, _, ctx, _)} and singleton subsets with + # non-conflicting configurations. Two configurations conflict if they have + # identical {@link ATNConfig#state} and {@link ATNConfig#context} values + # but different {@link ATNConfig#alt} value, e.g. {@code (s, i, ctx, _)} + # and {@code (s, j, ctx, _)} for {@code i!=j}.

      + # + #

      Reduce these configuration subsets to the set of possible alternatives. + # You can compute the alternative subsets in one pass as follows:

      + # + #

      {@code A_s,ctx = {i | (s, i, ctx, _)}} for each configuration in + # {@code C} holding {@code s} and {@code ctx} fixed.

      + # + #

      Or in pseudo-code, for each configuration {@code c} in {@code C}:

      + # + #
      +    # map[c] U= c.{@link ATNConfig#alt alt} # map hash/equals uses s and x, not
      +    # alt and not pred
      +    # 
      + # + #

      The values in {@code map} are the set of {@code A_s,ctx} sets.

      + # + #

      If {@code |A_s,ctx|=1} then there is no conflict associated with + # {@code s} and {@code ctx}.

      + # + #

      Reduce the subsets to singletons by choosing a minimum of each subset. If + # the union of these alternative subsets is a singleton, then no amount of + # more lookahead will help us. We will always pick that alternative. If, + # however, there is more than one alternative, then we are uncertain which + # alternative to predict and must continue looking for resolution. We may + # or may not discover an ambiguity in the future, even if there are no + # conflicting subsets this round.

      + # + #

      The biggest sin is to terminate early because it means we've made a + # decision but were uncertain as to the eventual outcome. We haven't used + # enough lookahead. On the other hand, announcing a conflict too late is no + # big deal; you will still have the conflict. It's just inefficient. It + # might even look until the end of file.

      + # + #

      No special consideration for semantic predicates is required because + # predicates are evaluated on-the-fly for full LL prediction, ensuring that + # no configuration contains a semantic context during the termination + # check.

      + # + #

      CONFLICTING CONFIGS

      + # + #

      Two configurations {@code (s, i, x)} and {@code (s, j, x')}, conflict + # when {@code i!=j} but {@code x=x'}. Because we merge all + # {@code (s, i, _)} configurations together, that means that there are at + # most {@code n} configurations associated with state {@code s} for + # {@code n} possible alternatives in the decision. The merged stacks + # complicate the comparison of configuration contexts {@code x} and + # {@code x'}. Sam checks to see if one is a subset of the other by calling + # merge and checking to see if the merged result is either {@code x} or + # {@code x'}. If the {@code x} associated with lowest alternative {@code i} + # is the superset, then {@code i} is the only possible prediction since the + # others resolve to {@code min(i)} as well. However, if {@code x} is + # associated with {@code j>i} then at least one stack configuration for + # {@code j} is not in conflict with alternative {@code i}. The algorithm + # should keep going, looking for more lookahead due to the uncertainty.

      + # + #

      For simplicity, I'm doing a equality check between {@code x} and + # {@code x'} that lets the algorithm continue to consume lookahead longer + # than necessary. The reason I like the equality is of course the + # simplicity but also because that is the test you need to detect the + # alternatives that are actually in conflict.

      + # + #

      CONTINUE/STOP RULE

      + # + #

      Continue if union of resolved alternative sets from non-conflicting and + # conflicting alternative subsets has more than one alternative. We are + # uncertain about which alternative to predict.

      + # + #

      The complete set of alternatives, {@code [i for (_,i,_)]}, tells us which + # alternatives are still in the running for the amount of input we've + # consumed at this point. The conflicting sets let us to strip away + # configurations that won't lead to more states because we resolve + # conflicts to the configuration with a minimum alternate for the + # conflicting set.

      + # + #

      CASES

      + # + #
        + # + #
      • no conflicts and more than 1 alternative in set => continue
      • + # + #
      • {@code (s, 1, x)}, {@code (s, 2, x)}, {@code (s, 3, z)}, + # {@code (s', 1, y)}, {@code (s', 2, y)} yields non-conflicting set + # {@code {3}} U conflicting sets {@code min({1,2})} U {@code min({1,2})} = + # {@code {1,3}} => continue + #
      • + # + #
      • {@code (s, 1, x)}, {@code (s, 2, x)}, {@code (s', 1, y)}, + # {@code (s', 2, y)}, {@code (s'', 1, z)} yields non-conflicting set + # {@code {1}} U conflicting sets {@code min({1,2})} U {@code min({1,2})} = + # {@code {1}} => stop and predict 1
      • + # + #
      • {@code (s, 1, x)}, {@code (s, 2, x)}, {@code (s', 1, y)}, + # {@code (s', 2, y)} yields conflicting, reduced sets {@code {1}} U + # {@code {1}} = {@code {1}} => stop and predict 1, can announce + # ambiguity {@code {1,2}}
      • + # + #
      • {@code (s, 1, x)}, {@code (s, 2, x)}, {@code (s', 2, y)}, + # {@code (s', 3, y)} yields conflicting, reduced sets {@code {1}} U + # {@code {2}} = {@code {1,2}} => continue
      • + # + #
      • {@code (s, 1, x)}, {@code (s, 2, x)}, {@code (s', 3, y)}, + # {@code (s', 4, y)} yields conflicting, reduced sets {@code {1}} U + # {@code {3}} = {@code {1,3}} => continue
      • + # + #
      + # + #

      EXACT AMBIGUITY DETECTION

      + # + #

      If all states report the same conflicting set of alternatives, then we + # know we have the exact ambiguity set.

      + # + #

      |A_i|>1 and + # A_i = A_j for all i, j.

      + # + #

      In other words, we continue examining lookahead until all {@code A_i} + # have more than one alternative and all {@code A_i} are the same. If + # {@code A={{1,2}, {1,3}}}, then regular LL prediction would terminate + # because the resolved set is {@code {1}}. To determine what the real + # ambiguity is, we have to know whether the ambiguity is between one and + # two or one and three so we keep going. We can only stop prediction when + # we need exact ambiguity detection when the sets look like + # {@code A={{1,2}}} or {@code {{1,2},{1,2}}}, etc...

      + # + @classmethod + def resolvesToJustOneViableAlt(cls, altsets:list): + return cls.getSingleViableAlt(altsets) + + # + # Determines if every alternative subset in {@code altsets} contains more + # than one alternative. + # + # @param altsets a collection of alternative subsets + # @return {@code true} if every {@link BitSet} in {@code altsets} has + # {@link BitSet#cardinality cardinality} > 1, otherwise {@code false} + # + @classmethod + def allSubsetsConflict(cls, altsets:list): + return not cls.hasNonConflictingAltSet(altsets) + + # + # Determines if any single alternative subset in {@code altsets} contains + # exactly one alternative. + # + # @param altsets a collection of alternative subsets + # @return {@code true} if {@code altsets} contains a {@link BitSet} with + # {@link BitSet#cardinality cardinality} 1, otherwise {@code false} + # + @classmethod + def hasNonConflictingAltSet(cls, altsets:list): + return any(len(alts) == 1 for alts in altsets) + + # + # Determines if any single alternative subset in {@code altsets} contains + # more than one alternative. + # + # @param altsets a collection of alternative subsets + # @return {@code true} if {@code altsets} contains a {@link BitSet} with + # {@link BitSet#cardinality cardinality} > 1, otherwise {@code false} + # + @classmethod + def hasConflictingAltSet(cls, altsets:list): + return any(len(alts) > 1 for alts in altsets) + + # + # Determines if every alternative subset in {@code altsets} is equivalent. + # + # @param altsets a collection of alternative subsets + # @return {@code true} if every member of {@code altsets} is equal to the + # others, otherwise {@code false} + # + @classmethod + def allSubsetsEqual(cls, altsets:list): + if not altsets: + return True + first = next(iter(altsets)) + return all(alts == first for alts in iter(altsets)) + + # + # Returns the unique alternative predicted by all alternative subsets in + # {@code altsets}. If no such alternative exists, this method returns + # {@link ATN#INVALID_ALT_NUMBER}. + # + # @param altsets a collection of alternative subsets + # + @classmethod + def getUniqueAlt(cls, altsets:list): + all = cls.getAlts(altsets) + if len(all)==1: + return next(iter(all)) + return ATN.INVALID_ALT_NUMBER + + # Gets the complete set of represented alternatives for a collection of + # alternative subsets. This method returns the union of each {@link BitSet} + # in {@code altsets}. + # + # @param altsets a collection of alternative subsets + # @return the set of represented alternatives in {@code altsets} + # + @classmethod + def getAlts(cls, altsets:list): + return set.union(*altsets) + + # + # This function gets the conflicting alt subsets from a configuration set. + # For each configuration {@code c} in {@code configs}: + # + #
      +    # map[c] U= c.{@link ATNConfig#alt alt} # map hash/equals uses s and x, not
      +    # alt and not pred
      +    # 
      + # + @classmethod + def getConflictingAltSubsets(cls, configs:ATNConfigSet): + configToAlts = dict() + for c in configs: + h = hash((c.state.stateNumber, c.context)) + alts = configToAlts.get(h, None) + if alts is None: + alts = set() + configToAlts[h] = alts + alts.add(c.alt) + return configToAlts.values() + + # + # Get a map from state to alt subset from a configuration set. For each + # configuration {@code c} in {@code configs}: + # + #
      +    # map[c.{@link ATNConfig#state state}] U= c.{@link ATNConfig#alt alt}
      +    # 
      + # + @classmethod + def getStateToAltMap(cls, configs:ATNConfigSet): + m = dict() + for c in configs: + alts = m.get(c.state, None) + if alts is None: + alts = set() + m[c.state] = alts + alts.add(c.alt) + return m + + @classmethod + def hasStateAssociatedWithOneAlt(cls, configs:ATNConfigSet): + return any(len(alts) == 1 for alts in cls.getStateToAltMap(configs).values()) + + @classmethod + def getSingleViableAlt(cls, altsets:list): + viableAlts = set() + for alts in altsets: + minAlt = min(alts) + viableAlts.add(minAlt) + if len(viableAlts)>1 : # more than 1 viable alt + return ATN.INVALID_ALT_NUMBER + return min(viableAlts) diff --git a/lib/python3.12/site-packages/antlr4/atn/SemanticContext.py b/lib/python3.12/site-packages/antlr4/atn/SemanticContext.py new file mode 100644 index 0000000000000000000000000000000000000000..8f4dc31088d35b73304432c46c54e31c1ab92700 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/SemanticContext.py @@ -0,0 +1,323 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# A tree structure used to record the semantic context in which +# an ATN configuration is valid. It's either a single predicate, +# a conjunction {@code p1&&p2}, or a sum of products {@code p1||p2}. +# +#

      I have scoped the {@link AND}, {@link OR}, and {@link Predicate} subclasses of +# {@link SemanticContext} within the scope of this outer class.

      +# +from antlr4.Recognizer import Recognizer +from antlr4.RuleContext import RuleContext +from io import StringIO + + +class SemanticContext(object): + # + # The default {@link SemanticContext}, which is semantically equivalent to + # a predicate of the form {@code {true}?}. + # + NONE = None + + # + # For context independent predicates, we evaluate them without a local + # context (i.e., null context). That way, we can evaluate them without + # having to create proper rule-specific context during prediction (as + # opposed to the parser, which creates them naturally). In a practical + # sense, this avoids a cast exception from RuleContext to myruleContext. + # + #

      For context dependent predicates, we must pass in a local context so that + # references such as $arg evaluate properly as _localctx.arg. We only + # capture context dependent predicates in the context in which we begin + # prediction, so we passed in the outer context here in case of context + # dependent predicate evaluation.

      + # + def eval(self, parser:Recognizer , outerContext:RuleContext ): + pass + + # + # Evaluate the precedence predicates for the context and reduce the result. + # + # @param parser The parser instance. + # @param outerContext The current parser context object. + # @return The simplified semantic context after precedence predicates are + # evaluated, which will be one of the following values. + #
        + #
      • {@link #NONE}: if the predicate simplifies to {@code true} after + # precedence predicates are evaluated.
      • + #
      • {@code null}: if the predicate simplifies to {@code false} after + # precedence predicates are evaluated.
      • + #
      • {@code this}: if the semantic context is not changed as a result of + # precedence predicate evaluation.
      • + #
      • A non-{@code null} {@link SemanticContext}: the new simplified + # semantic context after precedence predicates are evaluated.
      • + #
      + # + def evalPrecedence(self, parser:Recognizer, outerContext:RuleContext): + return self + +# need forward declaration +AND = None + +def andContext(a:SemanticContext, b:SemanticContext): + if a is None or a is SemanticContext.NONE: + return b + if b is None or b is SemanticContext.NONE: + return a + result = AND(a, b) + if len(result.opnds) == 1: + return result.opnds[0] + else: + return result + +# need forward declaration +OR = None + +def orContext(a:SemanticContext, b:SemanticContext): + if a is None: + return b + if b is None: + return a + if a is SemanticContext.NONE or b is SemanticContext.NONE: + return SemanticContext.NONE + result = OR(a, b) + if len(result.opnds) == 1: + return result.opnds[0] + else: + return result + +def filterPrecedencePredicates(collection:set): + return [context for context in collection if isinstance(context, PrecedencePredicate)] + + +class Predicate(SemanticContext): + __slots__ = ('ruleIndex', 'predIndex', 'isCtxDependent') + + def __init__(self, ruleIndex:int=-1, predIndex:int=-1, isCtxDependent:bool=False): + self.ruleIndex = ruleIndex + self.predIndex = predIndex + self.isCtxDependent = isCtxDependent # e.g., $i ref in pred + + def eval(self, parser:Recognizer , outerContext:RuleContext ): + localctx = outerContext if self.isCtxDependent else None + return parser.sempred(localctx, self.ruleIndex, self.predIndex) + + def __hash__(self): + return hash((self.ruleIndex, self.predIndex, self.isCtxDependent)) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, Predicate): + return False + return self.ruleIndex == other.ruleIndex and \ + self.predIndex == other.predIndex and \ + self.isCtxDependent == other.isCtxDependent + + def __str__(self): + return "{" + str(self.ruleIndex) + ":" + str(self.predIndex) + "}?" + + +class PrecedencePredicate(SemanticContext): + + def __init__(self, precedence:int=0): + self.precedence = precedence + + def eval(self, parser:Recognizer , outerContext:RuleContext ): + return parser.precpred(outerContext, self.precedence) + + def evalPrecedence(self, parser:Recognizer, outerContext:RuleContext): + if parser.precpred(outerContext, self.precedence): + return SemanticContext.NONE + else: + return None + + def __lt__(self, other): + return self.precedence < other.precedence + + def __hash__(self): + return 31 + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, PrecedencePredicate): + return False + else: + return self.precedence == other.precedence + +# A semantic context which is true whenever none of the contained contexts +# is false. +del AND +class AND(SemanticContext): + __slots__ = 'opnds' + + def __init__(self, a:SemanticContext, b:SemanticContext): + operands = set() + if isinstance( a, AND ): + operands.update(a.opnds) + else: + operands.add(a) + if isinstance( b, AND ): + operands.update(b.opnds) + else: + operands.add(b) + + precedencePredicates = filterPrecedencePredicates(operands) + if len(precedencePredicates)>0: + # interested in the transition with the lowest precedence + reduced = min(precedencePredicates) + operands.add(reduced) + + self.opnds = list(operands) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, AND): + return False + else: + return self.opnds == other.opnds + + def __hash__(self): + h = 0 + for o in self.opnds: + h = hash((h, o)) + return hash((h, "AND")) + + # + # {@inheritDoc} + # + #

      + # The evaluation of predicates by this context is short-circuiting, but + # unordered.

      + # + def eval(self, parser:Recognizer, outerContext:RuleContext): + return all(opnd.eval(parser, outerContext) for opnd in self.opnds) + + def evalPrecedence(self, parser:Recognizer, outerContext:RuleContext): + differs = False + operands = [] + for context in self.opnds: + evaluated = context.evalPrecedence(parser, outerContext) + differs |= evaluated is not context + if evaluated is None: + # The AND context is false if any element is false + return None + elif evaluated is not SemanticContext.NONE: + # Reduce the result by skipping true elements + operands.append(evaluated) + + if not differs: + return self + + if len(operands)==0: + # all elements were true, so the AND context is true + return SemanticContext.NONE + + result = None + for o in operands: + result = o if result is None else andContext(result, o) + + return result + + def __str__(self): + with StringIO() as buf: + first = True + for o in self.opnds: + if not first: + buf.write("&&") + buf.write(str(o)) + first = False + return buf.getvalue() + +# +# A semantic context which is true whenever at least one of the contained +# contexts is true. +del OR +class OR (SemanticContext): + __slots__ = 'opnds' + + def __init__(self, a:SemanticContext, b:SemanticContext): + operands = set() + if isinstance( a, OR ): + operands.update(a.opnds) + else: + operands.add(a) + if isinstance( b, OR ): + operands.update(b.opnds) + else: + operands.add(b) + + precedencePredicates = filterPrecedencePredicates(operands) + if len(precedencePredicates)>0: + # interested in the transition with the highest precedence + s = sorted(precedencePredicates) + reduced = s[-1] + operands.add(reduced) + + self.opnds = list(operands) + + def __eq__(self, other): + if self is other: + return True + elif not isinstance(other, OR): + return False + else: + return self.opnds == other.opnds + + def __hash__(self): + h = 0 + for o in self.opnds: + h = hash((h, o)) + return hash((h, "OR")) + + #

      + # The evaluation of predicates by this context is short-circuiting, but + # unordered.

      + # + def eval(self, parser:Recognizer, outerContext:RuleContext): + return any(opnd.eval(parser, outerContext) for opnd in self.opnds) + + def evalPrecedence(self, parser:Recognizer, outerContext:RuleContext): + differs = False + operands = [] + for context in self.opnds: + evaluated = context.evalPrecedence(parser, outerContext) + differs |= evaluated is not context + if evaluated is SemanticContext.NONE: + # The OR context is true if any element is true + return SemanticContext.NONE + elif evaluated is not None: + # Reduce the result by skipping false elements + operands.append(evaluated) + + if not differs: + return self + + if len(operands)==0: + # all elements were false, so the OR context is false + return None + + result = None + for o in operands: + result = o if result is None else orContext(result, o) + + return result + + def __str__(self): + with StringIO() as buf: + first = True + for o in self.opnds: + if not first: + buf.write("||") + buf.write(str(o)) + first = False + return buf.getvalue() + + +SemanticContext.NONE = Predicate() diff --git a/lib/python3.12/site-packages/antlr4/atn/Transition.py b/lib/python3.12/site-packages/antlr4/atn/Transition.py new file mode 100644 index 0000000000000000000000000000000000000000..2e4c9971763c34dbb2690660434c5c99d44193e1 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/Transition.py @@ -0,0 +1,268 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# An ATN transition between any two ATN states. Subclasses define +# atom, set, epsilon, action, predicate, rule transitions. +# +#

      This is a one way link. It emanates from a state (usually via a list of +# transitions) and has a target state.

      +# +#

      Since we never have to change the ATN transitions once we construct it, +# we can fix these transitions as specific classes. The DFA transitions +# on the other hand need to update the labels as it adds transitions to +# the states. We'll use the term Edge for the DFA to distinguish them from +# ATN transitions.

      +# +from antlr4.IntervalSet import IntervalSet +from antlr4.Token import Token + +# need forward declarations +from antlr4.atn.SemanticContext import Predicate, PrecedencePredicate + +ATNState = None +RuleStartState = None + +class Transition (object): + __slots__ = ('target','isEpsilon','label') + + # constants for serialization + EPSILON = 1 + RANGE = 2 + RULE = 3 + PREDICATE = 4 # e.g., {isType(input.LT(1))}? + ATOM = 5 + ACTION = 6 + SET = 7 # ~(A|B) or ~atom, wildcard, which convert to next 2 + NOT_SET = 8 + WILDCARD = 9 + PRECEDENCE = 10 + + serializationNames = [ + "INVALID", + "EPSILON", + "RANGE", + "RULE", + "PREDICATE", + "ATOM", + "ACTION", + "SET", + "NOT_SET", + "WILDCARD", + "PRECEDENCE" + ] + + serializationTypes = dict() + + def __init__(self, target:ATNState): + # The target of this transition. + if target is None: + raise Exception("target cannot be null.") + self.target = target + # Are we epsilon, action, sempred? + self.isEpsilon = False + self.label = None + + +# TODO: make all transitions sets? no, should remove set edges +class AtomTransition(Transition): + __slots__ = ('label_', 'serializationType') + + def __init__(self, target:ATNState, label:int): + super().__init__(target) + self.label_ = label # The token type or character value; or, signifies special label. + self.label = self.makeLabel() + self.serializationType = self.ATOM + + def makeLabel(self): + s = IntervalSet() + s.addOne(self.label_) + return s + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return self.label_ == symbol + + def __str__(self): + return str(self.label_) + +class RuleTransition(Transition): + __slots__ = ('ruleIndex', 'precedence', 'followState', 'serializationType') + + def __init__(self, ruleStart:RuleStartState, ruleIndex:int, precedence:int, followState:ATNState): + super().__init__(ruleStart) + self.ruleIndex = ruleIndex # ptr to the rule definition object for this rule ref + self.precedence = precedence + self.followState = followState # what node to begin computations following ref to rule + self.serializationType = self.RULE + self.isEpsilon = True + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return False + + +class EpsilonTransition(Transition): + __slots__ = ('serializationType', 'outermostPrecedenceReturn') + + def __init__(self, target, outermostPrecedenceReturn=-1): + super(EpsilonTransition, self).__init__(target) + self.serializationType = self.EPSILON + self.isEpsilon = True + self.outermostPrecedenceReturn = outermostPrecedenceReturn + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return False + + def __str__(self): + return "epsilon" + +class RangeTransition(Transition): + __slots__ = ('serializationType', 'start', 'stop') + + def __init__(self, target:ATNState, start:int, stop:int): + super().__init__(target) + self.serializationType = self.RANGE + self.start = start + self.stop = stop + self.label = self.makeLabel() + + def makeLabel(self): + s = IntervalSet() + s.addRange(range(self.start, self.stop + 1)) + return s + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return symbol >= self.start and symbol <= self.stop + + def __str__(self): + return "'" + chr(self.start) + "'..'" + chr(self.stop) + "'" + +class AbstractPredicateTransition(Transition): + + def __init__(self, target:ATNState): + super().__init__(target) + + +class PredicateTransition(AbstractPredicateTransition): + __slots__ = ('serializationType', 'ruleIndex', 'predIndex', 'isCtxDependent') + + def __init__(self, target:ATNState, ruleIndex:int, predIndex:int, isCtxDependent:bool): + super().__init__(target) + self.serializationType = self.PREDICATE + self.ruleIndex = ruleIndex + self.predIndex = predIndex + self.isCtxDependent = isCtxDependent # e.g., $i ref in pred + self.isEpsilon = True + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return False + + def getPredicate(self): + return Predicate(self.ruleIndex, self.predIndex, self.isCtxDependent) + + def __str__(self): + return "pred_" + str(self.ruleIndex) + ":" + str(self.predIndex) + +class ActionTransition(Transition): + __slots__ = ('serializationType', 'ruleIndex', 'actionIndex', 'isCtxDependent') + + def __init__(self, target:ATNState, ruleIndex:int, actionIndex:int=-1, isCtxDependent:bool=False): + super().__init__(target) + self.serializationType = self.ACTION + self.ruleIndex = ruleIndex + self.actionIndex = actionIndex + self.isCtxDependent = isCtxDependent # e.g., $i ref in pred + self.isEpsilon = True + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return False + + def __str__(self): + return "action_"+self.ruleIndex+":"+self.actionIndex + +# A transition containing a set of values. +class SetTransition(Transition): + __slots__ = 'serializationType' + + def __init__(self, target:ATNState, set:IntervalSet): + super().__init__(target) + self.serializationType = self.SET + if set is not None: + self.label = set + else: + self.label = IntervalSet() + self.label.addRange(range(Token.INVALID_TYPE, Token.INVALID_TYPE + 1)) + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return symbol in self.label + + def __str__(self): + return str(self.label) + +class NotSetTransition(SetTransition): + + def __init__(self, target:ATNState, set:IntervalSet): + super().__init__(target, set) + self.serializationType = self.NOT_SET + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return symbol >= minVocabSymbol \ + and symbol <= maxVocabSymbol \ + and not super(type(self), self).matches(symbol, minVocabSymbol, maxVocabSymbol) + + def __str__(self): + return '~' + super(type(self), self).__str__() + + +class WildcardTransition(Transition): + __slots__ = 'serializationType' + + def __init__(self, target:ATNState): + super().__init__(target) + self.serializationType = self.WILDCARD + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return symbol >= minVocabSymbol and symbol <= maxVocabSymbol + + def __str__(self): + return "." + + +class PrecedencePredicateTransition(AbstractPredicateTransition): + __slots__ = ('serializationType', 'precedence') + + def __init__(self, target:ATNState, precedence:int): + super().__init__(target) + self.serializationType = self.PRECEDENCE + self.precedence = precedence + self.isEpsilon = True + + def matches( self, symbol:int, minVocabSymbol:int, maxVocabSymbol:int): + return False + + + def getPredicate(self): + return PrecedencePredicate(self.precedence) + + def __str__(self): + return self.precedence + " >= _p" + + +Transition.serializationTypes = { + EpsilonTransition: Transition.EPSILON, + RangeTransition: Transition.RANGE, + RuleTransition: Transition.RULE, + PredicateTransition: Transition.PREDICATE, + AtomTransition: Transition.ATOM, + ActionTransition: Transition.ACTION, + SetTransition: Transition.SET, + NotSetTransition: Transition.NOT_SET, + WildcardTransition: Transition.WILDCARD, + PrecedencePredicateTransition: Transition.PRECEDENCE + } + +del ATNState +del RuleStartState + +from antlr4.atn.ATNState import * diff --git a/lib/python3.12/site-packages/antlr4/atn/__init__.py b/lib/python3.12/site-packages/antlr4/atn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..216c000dc5ffc8e53cc9c596e420c1e67604d1aa --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/atn/__init__.py @@ -0,0 +1 @@ +__author__ = 'ericvergnaud' diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATN.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATN.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f5d9714f586c931a4611f8dd7a4de77eb71be94 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATN.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNConfig.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNConfig.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a92441f21b7273063b7ecb72cb4d47ed3227d0b5 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNConfig.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNConfigSet.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNConfigSet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2eddcae68b9873be51600add6f61a3172eadd200 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNConfigSet.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNDeserializationOptions.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNDeserializationOptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6edf61d9e5bd69807985fac05c9c572a469d2eb3 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNDeserializationOptions.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNDeserializer.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNDeserializer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a5e9864b590ccaf9c65f5b780c6baa9ffbed21e Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNDeserializer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNSimulator.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNSimulator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8b5618732365b3a0dd5c9e481853df0a1b3dffb Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNSimulator.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNState.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNState.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61629345fc9a493a2fca97019a9f4b92d447a084 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNState.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNType.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNType.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10b09d04d37f941456190329dfdc37dd42181ed7 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ATNType.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerATNSimulator.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerATNSimulator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc0f13b21d11dc524e305b8a850f7a560e3c0738 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerATNSimulator.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerAction.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerAction.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3559468606efe47f1694f91c8ea178ffeb326c39 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerAction.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerActionExecutor.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerActionExecutor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8344bab8905ae8b1a871bca52d3401aa4bc93fe6 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/LexerActionExecutor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/ParserATNSimulator.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ParserATNSimulator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aac985361cf02c30dd109ba5fa3ed76075520cb3 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/ParserATNSimulator.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/PredictionMode.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/PredictionMode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25620f524550e0e7eed66b4385e7faaa44dde8df Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/PredictionMode.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/SemanticContext.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/SemanticContext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2dd3aea95f2b85f94cf6a4dbb5397cd6c0e14f27 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/SemanticContext.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/Transition.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/Transition.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cca3cfb115e5d9c6ec6f345797d1db10f893518 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/Transition.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/atn/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/atn/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a38c064001ec8c6af4682ee27c5948184f2e536 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/atn/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/dfa/DFA.py b/lib/python3.12/site-packages/antlr4/dfa/DFA.py new file mode 100644 index 0000000000000000000000000000000000000000..d80589a6834a48cc6bb718bf33f57156e32ec934 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/dfa/DFA.py @@ -0,0 +1,133 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +from antlr4.atn.ATNState import StarLoopEntryState + +from antlr4.atn.ATNConfigSet import ATNConfigSet +from antlr4.atn.ATNState import DecisionState +from antlr4.dfa.DFAState import DFAState +from antlr4.error.Errors import IllegalStateException + + +class DFA(object): + __slots__ = ('atnStartState', 'decision', '_states', 's0', 'precedenceDfa') + + def __init__(self, atnStartState:DecisionState, decision:int=0): + # From which ATN state did we create this DFA? + self.atnStartState = atnStartState + self.decision = decision + # A set of all DFA states. Use {@link Map} so we can get old state back + # ({@link Set} only allows you to see if it's there). + self._states = dict() + self.s0 = None + # {@code true} if this DFA is for a precedence decision; otherwise, + # {@code false}. This is the backing field for {@link #isPrecedenceDfa}, + # {@link #setPrecedenceDfa}. + self.precedenceDfa = False + + if isinstance(atnStartState, StarLoopEntryState): + if atnStartState.isPrecedenceDecision: + self.precedenceDfa = True + precedenceState = DFAState(configs=ATNConfigSet()) + precedenceState.edges = [] + precedenceState.isAcceptState = False + precedenceState.requiresFullContext = False + self.s0 = precedenceState + + + # Get the start state for a specific precedence value. + # + # @param precedence The current precedence. + # @return The start state corresponding to the specified precedence, or + # {@code null} if no start state exists for the specified precedence. + # + # @throws IllegalStateException if this is not a precedence DFA. + # @see #isPrecedenceDfa() + + def getPrecedenceStartState(self, precedence:int): + if not self.precedenceDfa: + raise IllegalStateException("Only precedence DFAs may contain a precedence start state.") + + # s0.edges is never null for a precedence DFA + if precedence < 0 or precedence >= len(self.s0.edges): + return None + return self.s0.edges[precedence] + + # Set the start state for a specific precedence value. + # + # @param precedence The current precedence. + # @param startState The start state corresponding to the specified + # precedence. + # + # @throws IllegalStateException if this is not a precedence DFA. + # @see #isPrecedenceDfa() + # + def setPrecedenceStartState(self, precedence:int, startState:DFAState): + if not self.precedenceDfa: + raise IllegalStateException("Only precedence DFAs may contain a precedence start state.") + + if precedence < 0: + return + + # synchronization on s0 here is ok. when the DFA is turned into a + # precedence DFA, s0 will be initialized once and not updated again + # s0.edges is never null for a precedence DFA + if precedence >= len(self.s0.edges): + ext = [None] * (precedence + 1 - len(self.s0.edges)) + self.s0.edges.extend(ext) + self.s0.edges[precedence] = startState + # + # Sets whether this is a precedence DFA. If the specified value differs + # from the current DFA configuration, the following actions are taken; + # otherwise no changes are made to the current DFA. + # + #
        + #
      • The {@link #states} map is cleared
      • + #
      • If {@code precedenceDfa} is {@code false}, the initial state + # {@link #s0} is set to {@code null}; otherwise, it is initialized to a new + # {@link DFAState} with an empty outgoing {@link DFAState#edges} array to + # store the start states for individual precedence values.
      • + #
      • The {@link #precedenceDfa} field is updated
      • + #
      + # + # @param precedenceDfa {@code true} if this is a precedence DFA; otherwise, + # {@code false} + + def setPrecedenceDfa(self, precedenceDfa:bool): + if self.precedenceDfa != precedenceDfa: + self._states = dict() + if precedenceDfa: + precedenceState = DFAState(configs=ATNConfigSet()) + precedenceState.edges = [] + precedenceState.isAcceptState = False + precedenceState.requiresFullContext = False + self.s0 = precedenceState + else: + self.s0 = None + self.precedenceDfa = precedenceDfa + + @property + def states(self): + return self._states + + # Return a list of all states in this DFA, ordered by state number. + def sortedStates(self): + return sorted(self._states.keys(), key=lambda state: state.stateNumber) + + def __str__(self): + return self.toString(None) + + def toString(self, literalNames:list=None, symbolicNames:list=None): + if self.s0 is None: + return "" + from antlr4.dfa.DFASerializer import DFASerializer + serializer = DFASerializer(self,literalNames,symbolicNames) + return str(serializer) + + def toLexerString(self): + if self.s0 is None: + return "" + from antlr4.dfa.DFASerializer import LexerDFASerializer + serializer = LexerDFASerializer(self) + return str(serializer) diff --git a/lib/python3.12/site-packages/antlr4/dfa/DFASerializer.py b/lib/python3.12/site-packages/antlr4/dfa/DFASerializer.py new file mode 100644 index 0000000000000000000000000000000000000000..bca0727b76dc54909be0bf60b6d636ec8f539927 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/dfa/DFASerializer.py @@ -0,0 +1,73 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# A DFA walker that knows how to dump them to serialized strings.#/ +from io import StringIO +from antlr4 import DFA +from antlr4.Utils import str_list +from antlr4.dfa.DFAState import DFAState + + +class DFASerializer(object): + __slots__ = ('dfa', 'literalNames', 'symbolicNames') + + def __init__(self, dfa:DFA, literalNames:list=None, symbolicNames:list=None): + self.dfa = dfa + self.literalNames = literalNames + self.symbolicNames = symbolicNames + + def __str__(self): + if self.dfa.s0 is None: + return None + with StringIO() as buf: + for s in self.dfa.sortedStates(): + n = 0 + if s.edges is not None: + n = len(s.edges) + for i in range(0, n): + t = s.edges[i] + if t is not None and t.stateNumber != 0x7FFFFFFF: + buf.write(self.getStateString(s)) + label = self.getEdgeLabel(i) + buf.write("-") + buf.write(label) + buf.write("->") + buf.write(self.getStateString(t)) + buf.write('\n') + output = buf.getvalue() + if len(output)==0: + return None + else: + return output + + def getEdgeLabel(self, i:int): + if i==0: + return "EOF" + if self.literalNames is not None and i<=len(self.literalNames): + return self.literalNames[i-1] + elif self.symbolicNames is not None and i<=len(self.symbolicNames): + return self.symbolicNames[i-1] + else: + return str(i-1) + + def getStateString(self, s:DFAState): + n = s.stateNumber + baseStateStr = ( ":" if s.isAcceptState else "") + "s" + str(n) + ( "^" if s.requiresFullContext else "") + if s.isAcceptState: + if s.predicates is not None: + return baseStateStr + "=>" + str_list(s.predicates) + else: + return baseStateStr + "=>" + str(s.prediction) + else: + return baseStateStr + +class LexerDFASerializer(DFASerializer): + + def __init__(self, dfa:DFA): + super().__init__(dfa, None) + + def getEdgeLabel(self, i:int): + return "'" + chr(i) + "'" diff --git a/lib/python3.12/site-packages/antlr4/dfa/DFAState.py b/lib/python3.12/site-packages/antlr4/dfa/DFAState.py new file mode 100644 index 0000000000000000000000000000000000000000..51955a448886ea1fa34f0f4dff7fb0976edd1975 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/dfa/DFAState.py @@ -0,0 +1,126 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + +# Map a predicate to a predicted alternative.#/ +from io import StringIO +from antlr4.atn.ATNConfigSet import ATNConfigSet +from antlr4.atn.SemanticContext import SemanticContext + + +class PredPrediction(object): + __slots__ = ('alt', 'pred') + + def __init__(self, pred:SemanticContext, alt:int): + self.alt = alt + self.pred = pred + + def __str__(self): + return "(" + str(self.pred) + ", " + str(self.alt) + ")" + +# A DFA state represents a set of possible ATN configurations. +# As Aho, Sethi, Ullman p. 117 says "The DFA uses its state +# to keep track of all possible states the ATN can be in after +# reading each input symbol. That is to say, after reading +# input a1a2..an, the DFA is in a state that represents the +# subset T of the states of the ATN that are reachable from the +# ATN's start state along some path labeled a1a2..an." +# In conventional NFA→DFA conversion, therefore, the subset T +# would be a bitset representing the set of states the +# ATN could be in. We need to track the alt predicted by each +# state as well, however. More importantly, we need to maintain +# a stack of states, tracking the closure operations as they +# jump from rule to rule, emulating rule invocations (method calls). +# I have to add a stack to simulate the proper lookahead sequences for +# the underlying LL grammar from which the ATN was derived. +# +#

      I use a set of ATNConfig objects not simple states. An ATNConfig +# is both a state (ala normal conversion) and a RuleContext describing +# the chain of rules (if any) followed to arrive at that state.

      +# +#

      A DFA state may have multiple references to a particular state, +# but with different ATN contexts (with same or different alts) +# meaning that state was reached via a different set of rule invocations.

      +#/ +class DFAState(object): + __slots__ = ( + 'stateNumber', 'configs', 'edges', 'isAcceptState', 'prediction', + 'lexerActionExecutor', 'requiresFullContext', 'predicates' + ) + + def __init__(self, stateNumber:int=-1, configs:ATNConfigSet=ATNConfigSet()): + self.stateNumber = stateNumber + self.configs = configs + # {@code edges[symbol]} points to target of symbol. Shift up by 1 so (-1) + # {@link Token#EOF} maps to {@code edges[0]}. + self.edges = None + self.isAcceptState = False + # if accept state, what ttype do we match or alt do we predict? + # This is set to {@link ATN#INVALID_ALT_NUMBER} when {@link #predicates}{@code !=null} or + # {@link #requiresFullContext}. + self.prediction = 0 + self.lexerActionExecutor = None + # Indicates that this state was created during SLL prediction that + # discovered a conflict between the configurations in the state. Future + # {@link ParserATNSimulator#execATN} invocations immediately jumped doing + # full context prediction if this field is true. + self.requiresFullContext = False + # During SLL parsing, this is a list of predicates associated with the + # ATN configurations of the DFA state. When we have predicates, + # {@link #requiresFullContext} is {@code false} since full context prediction evaluates predicates + # on-the-fly. If this is not null, then {@link #prediction} is + # {@link ATN#INVALID_ALT_NUMBER}. + # + #

      We only use these for non-{@link #requiresFullContext} but conflicting states. That + # means we know from the context (it's $ or we don't dip into outer + # context) that it's an ambiguity not a conflict.

      + # + #

      This list is computed by {@link ParserATNSimulator#predicateDFAState}.

      + self.predicates = None + + + + # Get the set of all alts mentioned by all ATN configurations in this + # DFA state. + def getAltSet(self): + if self.configs is not None: + return set(cfg.alt for cfg in self.configs) or None + return None + + def __hash__(self): + return hash(self.configs) + + # Two {@link DFAState} instances are equal if their ATN configuration sets + # are the same. This method is used to see if a state already exists. + # + #

      Because the number of alternatives and number of ATN configurations are + # finite, there is a finite number of DFA states that can be processed. + # This is necessary to show that the algorithm terminates.

      + # + #

      Cannot test the DFA state numbers here because in + # {@link ParserATNSimulator#addDFAState} we need to know if any other state + # exists that has this exact set of ATN configurations. The + # {@link #stateNumber} is irrelevant.

      + def __eq__(self, other): + # compare set of ATN configurations in this set with other + if self is other: + return True + elif not isinstance(other, DFAState): + return False + else: + return self.configs==other.configs + + def __str__(self): + with StringIO() as buf: + buf.write(str(self.stateNumber)) + buf.write(":") + buf.write(str(self.configs)) + if self.isAcceptState: + buf.write("=>") + if self.predicates is not None: + buf.write(str(self.predicates)) + else: + buf.write(str(self.prediction)) + return buf.getvalue() diff --git a/lib/python3.12/site-packages/antlr4/dfa/__init__.py b/lib/python3.12/site-packages/antlr4/dfa/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..216c000dc5ffc8e53cc9c596e420c1e67604d1aa --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/dfa/__init__.py @@ -0,0 +1 @@ +__author__ = 'ericvergnaud' diff --git a/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFA.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFA.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2ccd457f113d7031659896020019872650a622d Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFA.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFASerializer.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFASerializer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72ddca5d0372ee6658071559830a2d22811f5353 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFASerializer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFAState.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFAState.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9b3d7a6ae90e2b351f8b1f36bf8ae6e380f163e Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/DFAState.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/dfa/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ccb1f73a887de77958aa0feeca27c90be7a4f43 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/dfa/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/error/DiagnosticErrorListener.py b/lib/python3.12/site-packages/antlr4/error/DiagnosticErrorListener.py new file mode 100644 index 0000000000000000000000000000000000000000..32ac14b63579ce7c984c2e34f2b1c80bebe328ed --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/error/DiagnosticErrorListener.py @@ -0,0 +1,107 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + + +# +# This implementation of {@link ANTLRErrorListener} can be used to identify +# certain potential correctness and performance problems in grammars. "Reports" +# are made by calling {@link Parser#notifyErrorListeners} with the appropriate +# message. +# +#
        +#
      • Ambiguities: These are cases where more than one path through the +# grammar can match the input.
      • +#
      • Weak context sensitivity: These are cases where full-context +# prediction resolved an SLL conflict to a unique alternative which equaled the +# minimum alternative of the SLL conflict.
      • +#
      • Strong (forced) context sensitivity: These are cases where the +# full-context prediction resolved an SLL conflict to a unique alternative, +# and the minimum alternative of the SLL conflict was found to not be +# a truly viable alternative. Two-stage parsing cannot be used for inputs where +# this situation occurs.
      • +#
      + +from io import StringIO +from antlr4 import Parser, DFA +from antlr4.atn.ATNConfigSet import ATNConfigSet +from antlr4.error.ErrorListener import ErrorListener + +class DiagnosticErrorListener(ErrorListener): + + def __init__(self, exactOnly:bool=True): + # whether all ambiguities or only exact ambiguities are reported. + self.exactOnly = exactOnly + + def reportAmbiguity(self, recognizer:Parser, dfa:DFA, startIndex:int, + stopIndex:int, exact:bool, ambigAlts:set, configs:ATNConfigSet): + if self.exactOnly and not exact: + return + + with StringIO() as buf: + buf.write("reportAmbiguity d=") + buf.write(self.getDecisionDescription(recognizer, dfa)) + buf.write(": ambigAlts=") + buf.write(str(self.getConflictingAlts(ambigAlts, configs))) + buf.write(", input='") + buf.write(recognizer.getTokenStream().getText(startIndex, stopIndex)) + buf.write("'") + recognizer.notifyErrorListeners(buf.getvalue()) + + + def reportAttemptingFullContext(self, recognizer:Parser, dfa:DFA, startIndex:int, + stopIndex:int, conflictingAlts:set, configs:ATNConfigSet): + with StringIO() as buf: + buf.write("reportAttemptingFullContext d=") + buf.write(self.getDecisionDescription(recognizer, dfa)) + buf.write(", input='") + buf.write(recognizer.getTokenStream().getText(startIndex, stopIndex)) + buf.write("'") + recognizer.notifyErrorListeners(buf.getvalue()) + + def reportContextSensitivity(self, recognizer:Parser, dfa:DFA, startIndex:int, + stopIndex:int, prediction:int, configs:ATNConfigSet): + with StringIO() as buf: + buf.write("reportContextSensitivity d=") + buf.write(self.getDecisionDescription(recognizer, dfa)) + buf.write(", input='") + buf.write(recognizer.getTokenStream().getText(startIndex, stopIndex)) + buf.write("'") + recognizer.notifyErrorListeners(buf.getvalue()) + + def getDecisionDescription(self, recognizer:Parser, dfa:DFA): + decision = dfa.decision + ruleIndex = dfa.atnStartState.ruleIndex + + ruleNames = recognizer.ruleNames + if ruleIndex < 0 or ruleIndex >= len(ruleNames): + return str(decision) + + ruleName = ruleNames[ruleIndex] + if ruleName is None or len(ruleName)==0: + return str(decision) + + return str(decision) + " (" + ruleName + ")" + + # + # Computes the set of conflicting or ambiguous alternatives from a + # configuration set, if that information was not already provided by the + # parser. + # + # @param reportedAlts The set of conflicting or ambiguous alternatives, as + # reported by the parser. + # @param configs The conflicting or ambiguous configuration set. + # @return Returns {@code reportedAlts} if it is not {@code null}, otherwise + # returns the set of alternatives represented in {@code configs}. + # + def getConflictingAlts(self, reportedAlts:set, configs:ATNConfigSet): + if reportedAlts is not None: + return reportedAlts + + result = set() + for config in configs: + result.add(config.alt) + + return result diff --git a/lib/python3.12/site-packages/antlr4/error/ErrorListener.py b/lib/python3.12/site-packages/antlr4/error/ErrorListener.py new file mode 100644 index 0000000000000000000000000000000000000000..933264d431b9829f43a38d5f0f07c83bbad703a0 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/error/ErrorListener.py @@ -0,0 +1,72 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. + +# Provides an empty default implementation of {@link ANTLRErrorListener}. The +# default implementation of each method does nothing, but can be overridden as +# necessary. + + +import sys + +class ErrorListener(object): + + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + pass + + def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs): + pass + + def reportAttemptingFullContext(self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs): + pass + + def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs): + pass + +class ConsoleErrorListener(ErrorListener): + # + # Provides a default instance of {@link ConsoleErrorListener}. + # + INSTANCE = None + + # + # {@inheritDoc} + # + #

      + # This implementation prints messages to {@link System#err} containing the + # values of {@code line}, {@code charPositionInLine}, and {@code msg} using + # the following format.

      + # + #
      +    # line line:charPositionInLine msg
      +    # 
      + # + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + print("line " + str(line) + ":" + str(column) + " " + msg, file=sys.stderr) + +ConsoleErrorListener.INSTANCE = ConsoleErrorListener() + +class ProxyErrorListener(ErrorListener): + + def __init__(self, delegates): + super().__init__() + if delegates is None: + raise ReferenceError("delegates") + self.delegates = delegates + + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + for delegate in self.delegates: + delegate.syntaxError(recognizer, offendingSymbol, line, column, msg, e) + + def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs): + for delegate in self.delegates: + delegate.reportAmbiguity(recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs) + + def reportAttemptingFullContext(self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs): + for delegate in self.delegates: + delegate.reportAttemptingFullContext(recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs) + + def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs): + for delegate in self.delegates: + delegate.reportContextSensitivity(recognizer, dfa, startIndex, stopIndex, prediction, configs) diff --git a/lib/python3.12/site-packages/antlr4/error/ErrorStrategy.py b/lib/python3.12/site-packages/antlr4/error/ErrorStrategy.py new file mode 100644 index 0000000000000000000000000000000000000000..0f7caadb240445e6d997ad582a51836f95cab5c5 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/error/ErrorStrategy.py @@ -0,0 +1,709 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# +import sys +from antlr4.IntervalSet import IntervalSet + +from antlr4.Token import Token +from antlr4.atn.ATNState import ATNState +from antlr4.error.Errors import RecognitionException, NoViableAltException, InputMismatchException, \ + FailedPredicateException, ParseCancellationException + +# need forward declaration +Parser = None + +class ErrorStrategy(object): + + def reset(self, recognizer:Parser): + pass + + def recoverInline(self, recognizer:Parser): + pass + + def recover(self, recognizer:Parser, e:RecognitionException): + pass + + def sync(self, recognizer:Parser): + pass + + def inErrorRecoveryMode(self, recognizer:Parser): + pass + + def reportError(self, recognizer:Parser, e:RecognitionException): + pass + + +# This is the default implementation of {@link ANTLRErrorStrategy} used for +# error reporting and recovery in ANTLR parsers. +# +class DefaultErrorStrategy(ErrorStrategy): + + def __init__(self): + super().__init__() + # Indicates whether the error strategy is currently "recovering from an + # error". This is used to suppress reporting multiple error messages while + # attempting to recover from a detected syntax error. + # + # @see #inErrorRecoveryMode + # + self.errorRecoveryMode = False + + # The index into the input stream where the last error occurred. + # This is used to prevent infinite loops where an error is found + # but no token is consumed during recovery...another error is found, + # ad nauseum. This is a failsafe mechanism to guarantee that at least + # one token/tree node is consumed for two errors. + # + self.lastErrorIndex = -1 + self.lastErrorStates = None + self.nextTokensContext = None + self.nextTokenState = 0 + + #

      The default implementation simply calls {@link #endErrorCondition} to + # ensure that the handler is not in error recovery mode.

      + def reset(self, recognizer:Parser): + self.endErrorCondition(recognizer) + + # + # This method is called to enter error recovery mode when a recognition + # exception is reported. + # + # @param recognizer the parser instance + # + def beginErrorCondition(self, recognizer:Parser): + self.errorRecoveryMode = True + + def inErrorRecoveryMode(self, recognizer:Parser): + return self.errorRecoveryMode + + # + # This method is called to leave error recovery mode after recovering from + # a recognition exception. + # + # @param recognizer + # + def endErrorCondition(self, recognizer:Parser): + self.errorRecoveryMode = False + self.lastErrorStates = None + self.lastErrorIndex = -1 + + # + # {@inheritDoc} + # + #

      The default implementation simply calls {@link #endErrorCondition}.

      + # + def reportMatch(self, recognizer:Parser): + self.endErrorCondition(recognizer) + + # + # {@inheritDoc} + # + #

      The default implementation returns immediately if the handler is already + # in error recovery mode. Otherwise, it calls {@link #beginErrorCondition} + # and dispatches the reporting task based on the runtime type of {@code e} + # according to the following table.

      + # + #
        + #
      • {@link NoViableAltException}: Dispatches the call to + # {@link #reportNoViableAlternative}
      • + #
      • {@link InputMismatchException}: Dispatches the call to + # {@link #reportInputMismatch}
      • + #
      • {@link FailedPredicateException}: Dispatches the call to + # {@link #reportFailedPredicate}
      • + #
      • All other types: calls {@link Parser#notifyErrorListeners} to report + # the exception
      • + #
      + # + def reportError(self, recognizer:Parser, e:RecognitionException): + # if we've already reported an error and have not matched a token + # yet successfully, don't report any errors. + if self.inErrorRecoveryMode(recognizer): + return # don't report spurious errors + self.beginErrorCondition(recognizer) + if isinstance( e, NoViableAltException ): + self.reportNoViableAlternative(recognizer, e) + elif isinstance( e, InputMismatchException ): + self.reportInputMismatch(recognizer, e) + elif isinstance( e, FailedPredicateException ): + self.reportFailedPredicate(recognizer, e) + else: + print("unknown recognition error type: " + type(e).__name__) + recognizer.notifyErrorListeners(e.message, e.offendingToken, e) + + # + # {@inheritDoc} + # + #

      The default implementation resynchronizes the parser by consuming tokens + # until we find one in the resynchronization set--loosely the set of tokens + # that can follow the current rule.

      + # + def recover(self, recognizer:Parser, e:RecognitionException): + if self.lastErrorIndex==recognizer.getInputStream().index \ + and self.lastErrorStates is not None \ + and recognizer.state in self.lastErrorStates: + # uh oh, another error at same token index and previously-visited + # state in ATN; must be a case where LT(1) is in the recovery + # token set so nothing got consumed. Consume a single token + # at least to prevent an infinite loop; this is a failsafe. + recognizer.consume() + + self.lastErrorIndex = recognizer._input.index + if self.lastErrorStates is None: + self.lastErrorStates = [] + self.lastErrorStates.append(recognizer.state) + followSet = self.getErrorRecoverySet(recognizer) + self.consumeUntil(recognizer, followSet) + + # The default implementation of {@link ANTLRErrorStrategy#sync} makes sure + # that the current lookahead symbol is consistent with what were expecting + # at this point in the ATN. You can call this anytime but ANTLR only + # generates code to check before subrules/loops and each iteration. + # + #

      Implements Jim Idle's magic sync mechanism in closures and optional + # subrules. E.g.,

      + # + #
      +    # a : sync ( stuff sync )* ;
      +    # sync : {consume to what can follow sync} ;
      +    # 
      + # + # At the start of a sub rule upon error, {@link #sync} performs single + # token deletion, if possible. If it can't do that, it bails on the current + # rule and uses the default error recovery, which consumes until the + # resynchronization set of the current rule. + # + #

      If the sub rule is optional ({@code (...)?}, {@code (...)*}, or block + # with an empty alternative), then the expected set includes what follows + # the subrule.

      + # + #

      During loop iteration, it consumes until it sees a token that can start a + # sub rule or what follows loop. Yes, that is pretty aggressive. We opt to + # stay in the loop as long as possible.

      + # + #

      ORIGINS

      + # + #

      Previous versions of ANTLR did a poor job of their recovery within loops. + # A single mismatch token or missing token would force the parser to bail + # out of the entire rules surrounding the loop. So, for rule

      + # + #
      +    # classDef : 'class' ID '{' member* '}'
      +    # 
      + # + # input with an extra token between members would force the parser to + # consume until it found the next class definition rather than the next + # member definition of the current class. + # + #

      This functionality cost a little bit of effort because the parser has to + # compare token set at the start of the loop and at each iteration. If for + # some reason speed is suffering for you, you can turn off this + # functionality by simply overriding this method as a blank { }.

      + # + def sync(self, recognizer:Parser): + # If already recovering, don't try to sync + if self.inErrorRecoveryMode(recognizer): + return + + s = recognizer._interp.atn.states[recognizer.state] + la = recognizer.getTokenStream().LA(1) + # try cheaper subset first; might get lucky. seems to shave a wee bit off + nextTokens = recognizer.atn.nextTokens(s) + if la in nextTokens: + self.nextTokensContext = None + self.nextTokenState = ATNState.INVALID_STATE_NUMBER + return + elif Token.EPSILON in nextTokens: + if self.nextTokensContext is None: + # It's possible the next token won't match information tracked + # by sync is restricted for performance. + self.nextTokensContext = recognizer._ctx + self.nextTokensState = recognizer._stateNumber + return + + if s.stateType in [ATNState.BLOCK_START, ATNState.STAR_BLOCK_START, + ATNState.PLUS_BLOCK_START, ATNState.STAR_LOOP_ENTRY]: + # report error and recover if possible + if self.singleTokenDeletion(recognizer)is not None: + return + else: + raise InputMismatchException(recognizer) + + elif s.stateType in [ATNState.PLUS_LOOP_BACK, ATNState.STAR_LOOP_BACK]: + self.reportUnwantedToken(recognizer) + expecting = recognizer.getExpectedTokens() + whatFollowsLoopIterationOrRule = expecting.addSet(self.getErrorRecoverySet(recognizer)) + self.consumeUntil(recognizer, whatFollowsLoopIterationOrRule) + + else: + # do nothing if we can't identify the exact kind of ATN state + pass + + # This is called by {@link #reportError} when the exception is a + # {@link NoViableAltException}. + # + # @see #reportError + # + # @param recognizer the parser instance + # @param e the recognition exception + # + def reportNoViableAlternative(self, recognizer:Parser, e:NoViableAltException): + tokens = recognizer.getTokenStream() + if tokens is not None: + if e.startToken.type==Token.EOF: + input = "" + else: + input = tokens.getText(e.startToken, e.offendingToken) + else: + input = "" + msg = "no viable alternative at input " + self.escapeWSAndQuote(input) + recognizer.notifyErrorListeners(msg, e.offendingToken, e) + + # + # This is called by {@link #reportError} when the exception is an + # {@link InputMismatchException}. + # + # @see #reportError + # + # @param recognizer the parser instance + # @param e the recognition exception + # + def reportInputMismatch(self, recognizer:Parser, e:InputMismatchException): + msg = "mismatched input " + self.getTokenErrorDisplay(e.offendingToken) \ + + " expecting " + e.getExpectedTokens().toString(recognizer.literalNames, recognizer.symbolicNames) + recognizer.notifyErrorListeners(msg, e.offendingToken, e) + + # + # This is called by {@link #reportError} when the exception is a + # {@link FailedPredicateException}. + # + # @see #reportError + # + # @param recognizer the parser instance + # @param e the recognition exception + # + def reportFailedPredicate(self, recognizer, e): + ruleName = recognizer.ruleNames[recognizer._ctx.getRuleIndex()] + msg = "rule " + ruleName + " " + e.message + recognizer.notifyErrorListeners(msg, e.offendingToken, e) + + # This method is called to report a syntax error which requires the removal + # of a token from the input stream. At the time this method is called, the + # erroneous symbol is current {@code LT(1)} symbol and has not yet been + # removed from the input stream. When this method returns, + # {@code recognizer} is in error recovery mode. + # + #

      This method is called when {@link #singleTokenDeletion} identifies + # single-token deletion as a viable recovery strategy for a mismatched + # input error.

      + # + #

      The default implementation simply returns if the handler is already in + # error recovery mode. Otherwise, it calls {@link #beginErrorCondition} to + # enter error recovery mode, followed by calling + # {@link Parser#notifyErrorListeners}.

      + # + # @param recognizer the parser instance + # + def reportUnwantedToken(self, recognizer:Parser): + if self.inErrorRecoveryMode(recognizer): + return + + self.beginErrorCondition(recognizer) + t = recognizer.getCurrentToken() + tokenName = self.getTokenErrorDisplay(t) + expecting = self.getExpectedTokens(recognizer) + msg = "extraneous input " + tokenName + " expecting " \ + + expecting.toString(recognizer.literalNames, recognizer.symbolicNames) + recognizer.notifyErrorListeners(msg, t, None) + + # This method is called to report a syntax error which requires the + # insertion of a missing token into the input stream. At the time this + # method is called, the missing token has not yet been inserted. When this + # method returns, {@code recognizer} is in error recovery mode. + # + #

      This method is called when {@link #singleTokenInsertion} identifies + # single-token insertion as a viable recovery strategy for a mismatched + # input error.

      + # + #

      The default implementation simply returns if the handler is already in + # error recovery mode. Otherwise, it calls {@link #beginErrorCondition} to + # enter error recovery mode, followed by calling + # {@link Parser#notifyErrorListeners}.

      + # + # @param recognizer the parser instance + # + def reportMissingToken(self, recognizer:Parser): + if self.inErrorRecoveryMode(recognizer): + return + self.beginErrorCondition(recognizer) + t = recognizer.getCurrentToken() + expecting = self.getExpectedTokens(recognizer) + msg = "missing " + expecting.toString(recognizer.literalNames, recognizer.symbolicNames) \ + + " at " + self.getTokenErrorDisplay(t) + recognizer.notifyErrorListeners(msg, t, None) + + #

      The default implementation attempts to recover from the mismatched input + # by using single token insertion and deletion as described below. If the + # recovery attempt fails, this method throws an + # {@link InputMismatchException}.

      + # + #

      EXTRA TOKEN (single token deletion)

      + # + #

      {@code LA(1)} is not what we are looking for. If {@code LA(2)} has the + # right token, however, then assume {@code LA(1)} is some extra spurious + # token and delete it. Then consume and return the next token (which was + # the {@code LA(2)} token) as the successful result of the match operation.

      + # + #

      This recovery strategy is implemented by {@link #singleTokenDeletion}.

      + # + #

      MISSING TOKEN (single token insertion)

      + # + #

      If current token (at {@code LA(1)}) is consistent with what could come + # after the expected {@code LA(1)} token, then assume the token is missing + # and use the parser's {@link TokenFactory} to create it on the fly. The + # "insertion" is performed by returning the created token as the successful + # result of the match operation.

      + # + #

      This recovery strategy is implemented by {@link #singleTokenInsertion}.

      + # + #

      EXAMPLE

      + # + #

      For example, Input {@code i=(3;} is clearly missing the {@code ')'}. When + # the parser returns from the nested call to {@code expr}, it will have + # call chain:

      + # + #
      +    # stat → expr → atom
      +    # 
      + # + # and it will be trying to match the {@code ')'} at this point in the + # derivation: + # + #
      +    # => ID '=' '(' INT ')' ('+' atom)* ';'
      +    #                    ^
      +    # 
      + # + # The attempt to match {@code ')'} will fail when it sees {@code ';'} and + # call {@link #recoverInline}. To recover, it sees that {@code LA(1)==';'} + # is in the set of tokens that can follow the {@code ')'} token reference + # in rule {@code atom}. It can assume that you forgot the {@code ')'}. + # + def recoverInline(self, recognizer:Parser): + # SINGLE TOKEN DELETION + matchedSymbol = self.singleTokenDeletion(recognizer) + if matchedSymbol is not None: + # we have deleted the extra token. + # now, move past ttype token as if all were ok + recognizer.consume() + return matchedSymbol + + # SINGLE TOKEN INSERTION + if self.singleTokenInsertion(recognizer): + return self.getMissingSymbol(recognizer) + + # even that didn't work; must throw the exception + raise InputMismatchException(recognizer) + + # + # This method implements the single-token insertion inline error recovery + # strategy. It is called by {@link #recoverInline} if the single-token + # deletion strategy fails to recover from the mismatched input. If this + # method returns {@code true}, {@code recognizer} will be in error recovery + # mode. + # + #

      This method determines whether or not single-token insertion is viable by + # checking if the {@code LA(1)} input symbol could be successfully matched + # if it were instead the {@code LA(2)} symbol. If this method returns + # {@code true}, the caller is responsible for creating and inserting a + # token with the correct type to produce this behavior.

      + # + # @param recognizer the parser instance + # @return {@code true} if single-token insertion is a viable recovery + # strategy for the current mismatched input, otherwise {@code false} + # + def singleTokenInsertion(self, recognizer:Parser): + currentSymbolType = recognizer.getTokenStream().LA(1) + # if current token is consistent with what could come after current + # ATN state, then we know we're missing a token; error recovery + # is free to conjure up and insert the missing token + atn = recognizer._interp.atn + currentState = atn.states[recognizer.state] + next = currentState.transitions[0].target + expectingAtLL2 = atn.nextTokens(next, recognizer._ctx) + if currentSymbolType in expectingAtLL2: + self.reportMissingToken(recognizer) + return True + else: + return False + + # This method implements the single-token deletion inline error recovery + # strategy. It is called by {@link #recoverInline} to attempt to recover + # from mismatched input. If this method returns null, the parser and error + # handler state will not have changed. If this method returns non-null, + # {@code recognizer} will not be in error recovery mode since the + # returned token was a successful match. + # + #

      If the single-token deletion is successful, this method calls + # {@link #reportUnwantedToken} to report the error, followed by + # {@link Parser#consume} to actually "delete" the extraneous token. Then, + # before returning {@link #reportMatch} is called to signal a successful + # match.

      + # + # @param recognizer the parser instance + # @return the successfully matched {@link Token} instance if single-token + # deletion successfully recovers from the mismatched input, otherwise + # {@code null} + # + def singleTokenDeletion(self, recognizer:Parser): + nextTokenType = recognizer.getTokenStream().LA(2) + expecting = self.getExpectedTokens(recognizer) + if nextTokenType in expecting: + self.reportUnwantedToken(recognizer) + # print("recoverFromMismatchedToken deleting " \ + # + str(recognizer.getTokenStream().LT(1)) \ + # + " since " + str(recognizer.getTokenStream().LT(2)) \ + # + " is what we want", file=sys.stderr) + recognizer.consume() # simply delete extra token + # we want to return the token we're actually matching + matchedSymbol = recognizer.getCurrentToken() + self.reportMatch(recognizer) # we know current token is correct + return matchedSymbol + else: + return None + + # Conjure up a missing token during error recovery. + # + # The recognizer attempts to recover from single missing + # symbols. But, actions might refer to that missing symbol. + # For example, x=ID {f($x);}. The action clearly assumes + # that there has been an identifier matched previously and that + # $x points at that token. If that token is missing, but + # the next token in the stream is what we want we assume that + # this token is missing and we keep going. Because we + # have to return some token to replace the missing token, + # we have to conjure one up. This method gives the user control + # over the tokens returned for missing tokens. Mostly, + # you will want to create something special for identifier + # tokens. For literals such as '{' and ',', the default + # action in the parser or tree parser works. It simply creates + # a CommonToken of the appropriate type. The text will be the token. + # If you change what tokens must be created by the lexer, + # override this method to create the appropriate tokens. + # + def getMissingSymbol(self, recognizer:Parser): + currentSymbol = recognizer.getCurrentToken() + expecting = self.getExpectedTokens(recognizer) + expectedTokenType = expecting[0] # get any element + if expectedTokenType==Token.EOF: + tokenText = "" + else: + name = None + if expectedTokenType < len(recognizer.literalNames): + name = recognizer.literalNames[expectedTokenType] + if name is None and expectedTokenType < len(recognizer.symbolicNames): + name = recognizer.symbolicNames[expectedTokenType] + tokenText = "" + current = currentSymbol + lookback = recognizer.getTokenStream().LT(-1) + if current.type==Token.EOF and lookback is not None: + current = lookback + return recognizer.getTokenFactory().create(current.source, + expectedTokenType, tokenText, Token.DEFAULT_CHANNEL, + -1, -1, current.line, current.column) + + def getExpectedTokens(self, recognizer:Parser): + return recognizer.getExpectedTokens() + + # How should a token be displayed in an error message? The default + # is to display just the text, but during development you might + # want to have a lot of information spit out. Override in that case + # to use t.toString() (which, for CommonToken, dumps everything about + # the token). This is better than forcing you to override a method in + # your token objects because you don't have to go modify your lexer + # so that it creates a new Java type. + # + def getTokenErrorDisplay(self, t:Token): + if t is None: + return "" + s = t.text + if s is None: + if t.type==Token.EOF: + s = "" + else: + s = "<" + str(t.type) + ">" + return self.escapeWSAndQuote(s) + + def escapeWSAndQuote(self, s:str): + s = s.replace("\n","\\n") + s = s.replace("\r","\\r") + s = s.replace("\t","\\t") + return "'" + s + "'" + + # Compute the error recovery set for the current rule. During + # rule invocation, the parser pushes the set of tokens that can + # follow that rule reference on the stack; this amounts to + # computing FIRST of what follows the rule reference in the + # enclosing rule. See LinearApproximator.FIRST(). + # This local follow set only includes tokens + # from within the rule; i.e., the FIRST computation done by + # ANTLR stops at the end of a rule. + # + # EXAMPLE + # + # When you find a "no viable alt exception", the input is not + # consistent with any of the alternatives for rule r. The best + # thing to do is to consume tokens until you see something that + # can legally follow a call to r#or* any rule that called r. + # You don't want the exact set of viable next tokens because the + # input might just be missing a token--you might consume the + # rest of the input looking for one of the missing tokens. + # + # Consider grammar: + # + # a : '[' b ']' + # | '(' b ')' + # ; + # b : c '^' INT ; + # c : ID + # | INT + # ; + # + # At each rule invocation, the set of tokens that could follow + # that rule is pushed on a stack. Here are the various + # context-sensitive follow sets: + # + # FOLLOW(b1_in_a) = FIRST(']') = ']' + # FOLLOW(b2_in_a) = FIRST(')') = ')' + # FOLLOW(c_in_b) = FIRST('^') = '^' + # + # Upon erroneous input "[]", the call chain is + # + # a -> b -> c + # + # and, hence, the follow context stack is: + # + # depth follow set start of rule execution + # 0 a (from main()) + # 1 ']' b + # 2 '^' c + # + # Notice that ')' is not included, because b would have to have + # been called from a different context in rule a for ')' to be + # included. + # + # For error recovery, we cannot consider FOLLOW(c) + # (context-sensitive or otherwise). We need the combined set of + # all context-sensitive FOLLOW sets--the set of all tokens that + # could follow any reference in the call chain. We need to + # resync to one of those tokens. Note that FOLLOW(c)='^' and if + # we resync'd to that token, we'd consume until EOF. We need to + # sync to context-sensitive FOLLOWs for a, b, and c: {']','^'}. + # In this case, for input "[]", LA(1) is ']' and in the set, so we would + # not consume anything. After printing an error, rule c would + # return normally. Rule b would not find the required '^' though. + # At this point, it gets a mismatched token error and throws an + # exception (since LA(1) is not in the viable following token + # set). The rule exception handler tries to recover, but finds + # the same recovery set and doesn't consume anything. Rule b + # exits normally returning to rule a. Now it finds the ']' (and + # with the successful match exits errorRecovery mode). + # + # So, you can see that the parser walks up the call chain looking + # for the token that was a member of the recovery set. + # + # Errors are not generated in errorRecovery mode. + # + # ANTLR's error recovery mechanism is based upon original ideas: + # + # "Algorithms + Data Structures = Programs" by Niklaus Wirth + # + # and + # + # "A note on error recovery in recursive descent parsers": + # http:#portal.acm.org/citation.cfm?id=947902.947905 + # + # Later, Josef Grosch had some good ideas: + # + # "Efficient and Comfortable Error Recovery in Recursive Descent + # Parsers": + # ftp:#www.cocolab.com/products/cocktail/doca4.ps/ell.ps.zip + # + # Like Grosch I implement context-sensitive FOLLOW sets that are combined + # at run-time upon error to avoid overhead during parsing. + # + def getErrorRecoverySet(self, recognizer:Parser): + atn = recognizer._interp.atn + ctx = recognizer._ctx + recoverSet = IntervalSet() + while ctx is not None and ctx.invokingState>=0: + # compute what follows who invoked us + invokingState = atn.states[ctx.invokingState] + rt = invokingState.transitions[0] + follow = atn.nextTokens(rt.followState) + recoverSet.addSet(follow) + ctx = ctx.parentCtx + recoverSet.removeOne(Token.EPSILON) + return recoverSet + + # Consume tokens until one matches the given token set.# + def consumeUntil(self, recognizer:Parser, set_:set): + ttype = recognizer.getTokenStream().LA(1) + while ttype != Token.EOF and not ttype in set_: + recognizer.consume() + ttype = recognizer.getTokenStream().LA(1) + + +# +# This implementation of {@link ANTLRErrorStrategy} responds to syntax errors +# by immediately canceling the parse operation with a +# {@link ParseCancellationException}. The implementation ensures that the +# {@link ParserRuleContext#exception} field is set for all parse tree nodes +# that were not completed prior to encountering the error. +# +#

      +# This error strategy is useful in the following scenarios.

      +# +#
        +#
      • Two-stage parsing: This error strategy allows the first +# stage of two-stage parsing to immediately terminate if an error is +# encountered, and immediately fall back to the second stage. In addition to +# avoiding wasted work by attempting to recover from errors here, the empty +# implementation of {@link BailErrorStrategy#sync} improves the performance of +# the first stage.
      • +#
      • Silent validation: When syntax errors are not being +# reported or logged, and the parse result is simply ignored if errors occur, +# the {@link BailErrorStrategy} avoids wasting work on recovering from errors +# when the result will be ignored either way.
      • +#
      +# +#

      +# {@code myparser.setErrorHandler(new BailErrorStrategy());}

      +# +# @see Parser#setErrorHandler(ANTLRErrorStrategy) +# +class BailErrorStrategy(DefaultErrorStrategy): + # Instead of recovering from exception {@code e}, re-throw it wrapped + # in a {@link ParseCancellationException} so it is not caught by the + # rule function catches. Use {@link Exception#getCause()} to get the + # original {@link RecognitionException}. + # + def recover(self, recognizer:Parser, e:RecognitionException): + context = recognizer._ctx + while context is not None: + context.exception = e + context = context.parentCtx + raise ParseCancellationException(e) + + # Make sure we don't attempt to recover inline; if the parser + # successfully recovers, it won't throw an exception. + # + def recoverInline(self, recognizer:Parser): + self.recover(recognizer, InputMismatchException(recognizer)) + + # Make sure we don't attempt to recover from problems in subrules.# + def sync(self, recognizer:Parser): + pass + +del Parser \ No newline at end of file diff --git a/lib/python3.12/site-packages/antlr4/error/Errors.py b/lib/python3.12/site-packages/antlr4/error/Errors.py new file mode 100644 index 0000000000000000000000000000000000000000..e78ac05911d3c9569441fe376ff7d6c686c05c95 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/error/Errors.py @@ -0,0 +1,172 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# need forward declaration +Token = None +Lexer = None +Parser = None +TokenStream = None +ATNConfigSet = None +ParserRulecontext = None +PredicateTransition = None +BufferedTokenStream = None + +class UnsupportedOperationException(Exception): + + def __init__(self, msg:str): + super().__init__(msg) + +class IllegalStateException(Exception): + + def __init__(self, msg:str): + super().__init__(msg) + +class CancellationException(IllegalStateException): + + def __init__(self, msg:str): + super().__init__(msg) + +# The root of the ANTLR exception hierarchy. In general, ANTLR tracks just +# 3 kinds of errors: prediction errors, failed predicate errors, and +# mismatched input errors. In each case, the parser knows where it is +# in the input, where it is in the ATN, the rule invocation stack, +# and what kind of problem occurred. + +from antlr4.InputStream import InputStream +from antlr4.ParserRuleContext import ParserRuleContext +from antlr4.Recognizer import Recognizer + +class RecognitionException(Exception): + + + def __init__(self, message:str=None, recognizer:Recognizer=None, input:InputStream=None, ctx:ParserRulecontext=None): + super().__init__(message) + self.message = message + self.recognizer = recognizer + self.input = input + self.ctx = ctx + # The current {@link Token} when an error occurred. Since not all streams + # support accessing symbols by index, we have to track the {@link Token} + # instance itself. + self.offendingToken = None + # Get the ATN state number the parser was in at the time the error + # occurred. For {@link NoViableAltException} and + # {@link LexerNoViableAltException} exceptions, this is the + # {@link DecisionState} number. For others, it is the state whose outgoing + # edge we couldn't match. + self.offendingState = -1 + if recognizer is not None: + self.offendingState = recognizer.state + + #

      If the state number is not known, this method returns -1.

      + + # + # Gets the set of input symbols which could potentially follow the + # previously matched symbol at the time this exception was thrown. + # + #

      If the set of expected tokens is not known and could not be computed, + # this method returns {@code null}.

      + # + # @return The set of token types that could potentially follow the current + # state in the ATN, or {@code null} if the information is not available. + #/ + def getExpectedTokens(self): + if self.recognizer is not None: + return self.recognizer.atn.getExpectedTokens(self.offendingState, self.ctx) + else: + return None + + +class LexerNoViableAltException(RecognitionException): + + def __init__(self, lexer:Lexer, input:InputStream, startIndex:int, deadEndConfigs:ATNConfigSet): + super().__init__(message=None, recognizer=lexer, input=input, ctx=None) + self.startIndex = startIndex + self.deadEndConfigs = deadEndConfigs + + def __str__(self): + symbol = "" + if self.startIndex >= 0 and self.startIndex < self.input.size: + symbol = self.input.getText(self.startIndex, self.startIndex) + # TODO symbol = Utils.escapeWhitespace(symbol, false); + return "LexerNoViableAltException('" + symbol + "')" + +# Indicates that the parser could not decide which of two or more paths +# to take based upon the remaining input. It tracks the starting token +# of the offending input and also knows where the parser was +# in the various paths when the error. Reported by reportNoViableAlternative() +# +class NoViableAltException(RecognitionException): + + def __init__(self, recognizer:Parser, input:TokenStream=None, startToken:Token=None, + offendingToken:Token=None, deadEndConfigs:ATNConfigSet=None, ctx:ParserRuleContext=None): + if ctx is None: + ctx = recognizer._ctx + if offendingToken is None: + offendingToken = recognizer.getCurrentToken() + if startToken is None: + startToken = recognizer.getCurrentToken() + if input is None: + input = recognizer.getInputStream() + super().__init__(recognizer=recognizer, input=input, ctx=ctx) + # Which configurations did we try at input.index() that couldn't match input.LT(1)?# + self.deadEndConfigs = deadEndConfigs + # The token object at the start index; the input stream might + # not be buffering tokens so get a reference to it. (At the + # time the error occurred, of course the stream needs to keep a + # buffer all of the tokens but later we might not have access to those.) + self.startToken = startToken + self.offendingToken = offendingToken + +# This signifies any kind of mismatched input exceptions such as +# when the current input does not match the expected token. +# +class InputMismatchException(RecognitionException): + + def __init__(self, recognizer:Parser): + super().__init__(recognizer=recognizer, input=recognizer.getInputStream(), ctx=recognizer._ctx) + self.offendingToken = recognizer.getCurrentToken() + + +# A semantic predicate failed during validation. Validation of predicates +# occurs when normally parsing the alternative just like matching a token. +# Disambiguating predicate evaluation occurs when we test a predicate during +# prediction. + +class FailedPredicateException(RecognitionException): + + def __init__(self, recognizer:Parser, predicate:str=None, message:str=None): + super().__init__(message=self.formatMessage(predicate,message), recognizer=recognizer, + input=recognizer.getInputStream(), ctx=recognizer._ctx) + s = recognizer._interp.atn.states[recognizer.state] + trans = s.transitions[0] + from antlr4.atn.Transition import PredicateTransition + if isinstance(trans, PredicateTransition): + self.ruleIndex = trans.ruleIndex + self.predicateIndex = trans.predIndex + else: + self.ruleIndex = 0 + self.predicateIndex = 0 + self.predicate = predicate + self.offendingToken = recognizer.getCurrentToken() + + def formatMessage(self, predicate:str, message:str): + if message is not None: + return message + else: + return "failed predicate: {" + predicate + "}?" + +class ParseCancellationException(CancellationException): + + pass + +del Token +del Lexer +del Parser +del TokenStream +del ATNConfigSet +del ParserRulecontext +del PredicateTransition +del BufferedTokenStream diff --git a/lib/python3.12/site-packages/antlr4/error/__init__.py b/lib/python3.12/site-packages/antlr4/error/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..216c000dc5ffc8e53cc9c596e420c1e67604d1aa --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/error/__init__.py @@ -0,0 +1 @@ +__author__ = 'ericvergnaud' diff --git a/lib/python3.12/site-packages/antlr4/error/__pycache__/DiagnosticErrorListener.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/error/__pycache__/DiagnosticErrorListener.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65eb886f5d2caddd1b278688d53319c0832af7cc Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/error/__pycache__/DiagnosticErrorListener.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/error/__pycache__/ErrorListener.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/error/__pycache__/ErrorListener.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c2f49911a0d6ff9a7b336c7fa37758f4e96672f Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/error/__pycache__/ErrorListener.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/error/__pycache__/ErrorStrategy.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/error/__pycache__/ErrorStrategy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..078069c4b010241d42b0f864be1a0b9ee64df7ac Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/error/__pycache__/ErrorStrategy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/error/__pycache__/Errors.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/error/__pycache__/Errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98d89f4fe0d1dafc652e029336f865ad2cecfe98 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/error/__pycache__/Errors.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/error/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/error/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29bd07e94d65f785559528064e71cdb32150f430 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/error/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/Chunk.py b/lib/python3.12/site-packages/antlr4/tree/Chunk.py new file mode 100644 index 0000000000000000000000000000000000000000..081419a34f65463b370b848b141192bfe491befd --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/Chunk.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +class Chunk(object): + pass + +class TagChunk(Chunk): + __slots__ = ('tag', 'label') + + def __init__(self, tag:str, label:str=None): + self.tag = tag + self.label = label + + def __str__(self): + if self.label is None: + return self.tag + else: + return self.label + ":" + self.tag + +class TextChunk(Chunk): + __slots__ = 'text' + + def __init__(self, text:str): + self.text = text + + def __str__(self): + return "'" + self.text + "'" diff --git a/lib/python3.12/site-packages/antlr4/tree/ParseTreeMatch.py b/lib/python3.12/site-packages/antlr4/tree/ParseTreeMatch.py new file mode 100644 index 0000000000000000000000000000000000000000..c02bc0357d26b343a72307cda77ff62fe307a44a --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/ParseTreeMatch.py @@ -0,0 +1,118 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + + +# +# Represents the result of matching a {@link ParseTree} against a tree pattern. +# +from io import StringIO +from antlr4.tree.ParseTreePattern import ParseTreePattern +from antlr4.tree.Tree import ParseTree + + +class ParseTreeMatch(object): + __slots__ = ('tree', 'pattern', 'labels', 'mismatchedNode') + # + # Constructs a new instance of {@link ParseTreeMatch} from the specified + # parse tree and pattern. + # + # @param tree The parse tree to match against the pattern. + # @param pattern The parse tree pattern. + # @param labels A mapping from label names to collections of + # {@link ParseTree} objects located by the tree pattern matching process. + # @param mismatchedNode The first node which failed to match the tree + # pattern during the matching process. + # + # @exception IllegalArgumentException if {@code tree} is {@code null} + # @exception IllegalArgumentException if {@code pattern} is {@code null} + # @exception IllegalArgumentException if {@code labels} is {@code null} + # + def __init__(self, tree:ParseTree, pattern:ParseTreePattern, labels:dict, mismatchedNode:ParseTree): + if tree is None: + raise Exception("tree cannot be null") + if pattern is None: + raise Exception("pattern cannot be null") + if labels is None: + raise Exception("labels cannot be null") + self.tree = tree + self.pattern = pattern + self.labels = labels + self.mismatchedNode = mismatchedNode + + # + # Get the last node associated with a specific {@code label}. + # + #

      For example, for pattern {@code }, {@code get("id")} returns the + # node matched for that {@code ID}. If more than one node + # matched the specified label, only the last is returned. If there is + # no node associated with the label, this returns {@code null}.

      + # + #

      Pattern tags like {@code } and {@code } without labels are + # considered to be labeled with {@code ID} and {@code expr}, respectively.

      + # + # @param label The label to check. + # + # @return The last {@link ParseTree} to match a tag with the specified + # label, or {@code null} if no parse tree matched a tag with the label. + # + def get(self, label:str): + parseTrees = self.labels.get(label, None) + if parseTrees is None or len(parseTrees)==0: + return None + else: + return parseTrees[len(parseTrees)-1] + + # + # Return all nodes matching a rule or token tag with the specified label. + # + #

      If the {@code label} is the name of a parser rule or token in the + # grammar, the resulting list will contain both the parse trees matching + # rule or tags explicitly labeled with the label and the complete set of + # parse trees matching the labeled and unlabeled tags in the pattern for + # the parser rule or token. For example, if {@code label} is {@code "foo"}, + # the result will contain all of the following.

      + # + #
        + #
      • Parse tree nodes matching tags of the form {@code } and + # {@code }.
      • + #
      • Parse tree nodes matching tags of the form {@code }.
      • + #
      • Parse tree nodes matching tags of the form {@code }.
      • + #
      + # + # @param label The label. + # + # @return A collection of all {@link ParseTree} nodes matching tags with + # the specified {@code label}. If no nodes matched the label, an empty list + # is returned. + # + def getAll(self, label:str): + nodes = self.labels.get(label, None) + if nodes is None: + return list() + else: + return nodes + + + # + # Gets a value indicating whether the match operation succeeded. + # + # @return {@code true} if the match operation succeeded; otherwise, + # {@code false}. + # + def succeeded(self): + return self.mismatchedNode is None + + # + # {@inheritDoc} + # + def __str__(self): + with StringIO() as buf: + buf.write("Match ") + buf.write("succeeded" if self.succeeded() else "failed") + buf.write("; found ") + buf.write(str(len(self.labels))) + buf.write(" labels") + return buf.getvalue() diff --git a/lib/python3.12/site-packages/antlr4/tree/ParseTreePattern.py b/lib/python3.12/site-packages/antlr4/tree/ParseTreePattern.py new file mode 100644 index 0000000000000000000000000000000000000000..37fd0bf09f478d47f927b3fdf7d7a32da1c0b795 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/ParseTreePattern.py @@ -0,0 +1,72 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# A pattern like {@code = ;} converted to a {@link ParseTree} by +# {@link ParseTreePatternMatcher#compile(String, int)}. +# +from antlr4.tree.ParseTreePatternMatcher import ParseTreePatternMatcher +from antlr4.tree.Tree import ParseTree +from antlr4.xpath.XPath import XPath + + +class ParseTreePattern(object): + __slots__ = ('matcher', 'patternRuleIndex', 'pattern', 'patternTree') + + # Construct a new instance of the {@link ParseTreePattern} class. + # + # @param matcher The {@link ParseTreePatternMatcher} which created this + # tree pattern. + # @param pattern The tree pattern in concrete syntax form. + # @param patternRuleIndex The parser rule which serves as the root of the + # tree pattern. + # @param patternTree The tree pattern in {@link ParseTree} form. + # + def __init__(self, matcher:ParseTreePatternMatcher, pattern:str, patternRuleIndex:int , patternTree:ParseTree): + self.matcher = matcher + self.patternRuleIndex = patternRuleIndex + self.pattern = pattern + self.patternTree = patternTree + + # + # Match a specific parse tree against this tree pattern. + # + # @param tree The parse tree to match against this tree pattern. + # @return A {@link ParseTreeMatch} object describing the result of the + # match operation. The {@link ParseTreeMatch#succeeded()} method can be + # used to determine whether or not the match was successful. + # + def match(self, tree:ParseTree): + return self.matcher.match(tree, self) + + # + # Determine whether or not a parse tree matches this tree pattern. + # + # @param tree The parse tree to match against this tree pattern. + # @return {@code true} if {@code tree} is a match for the current tree + # pattern; otherwise, {@code false}. + # + def matches(self, tree:ParseTree): + return self.matcher.match(tree, self).succeeded() + + # Find all nodes using XPath and then try to match those subtrees against + # this tree pattern. + # + # @param tree The {@link ParseTree} to match against this pattern. + # @param xpath An expression matching the nodes + # + # @return A collection of {@link ParseTreeMatch} objects describing the + # successful matches. Unsuccessful matches are omitted from the result, + # regardless of the reason for the failure. + # + def findAll(self, tree:ParseTree, xpath:str): + subtrees = XPath.findAll(tree, xpath, self.matcher.parser) + matches = list() + for t in subtrees: + match = self.match(t) + if match.succeeded(): + matches.append(match) + return matches diff --git a/lib/python3.12/site-packages/antlr4/tree/ParseTreePatternMatcher.py b/lib/python3.12/site-packages/antlr4/tree/ParseTreePatternMatcher.py new file mode 100644 index 0000000000000000000000000000000000000000..62fd197b0d143393fa187ead9b0c576112b486be --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/ParseTreePatternMatcher.py @@ -0,0 +1,374 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# A tree pattern matching mechanism for ANTLR {@link ParseTree}s. +# +#

      Patterns are strings of source input text with special tags representing +# token or rule references such as:

      +# +#

      {@code = ;}

      +# +#

      Given a pattern start rule such as {@code statement}, this object constructs +# a {@link ParseTree} with placeholders for the {@code ID} and {@code expr} +# subtree. Then the {@link #match} routines can compare an actual +# {@link ParseTree} from a parse with this pattern. Tag {@code } matches +# any {@code ID} token and tag {@code } references the result of the +# {@code expr} rule (generally an instance of {@code ExprContext}.

      +# +#

      Pattern {@code x = 0;} is a similar pattern that matches the same pattern +# except that it requires the identifier to be {@code x} and the expression to +# be {@code 0}.

      +# +#

      The {@link #matches} routines return {@code true} or {@code false} based +# upon a match for the tree rooted at the parameter sent in. The +# {@link #match} routines return a {@link ParseTreeMatch} object that +# contains the parse tree, the parse tree pattern, and a map from tag name to +# matched nodes (more below). A subtree that fails to match, returns with +# {@link ParseTreeMatch#mismatchedNode} set to the first tree node that did not +# match.

      +# +#

      For efficiency, you can compile a tree pattern in string form to a +# {@link ParseTreePattern} object.

      +# +#

      See {@code TestParseTreeMatcher} for lots of examples. +# {@link ParseTreePattern} has two static helper methods: +# {@link ParseTreePattern#findAll} and {@link ParseTreePattern#match} that +# are easy to use but not super efficient because they create new +# {@link ParseTreePatternMatcher} objects each time and have to compile the +# pattern in string form before using it.

      +# +#

      The lexer and parser that you pass into the {@link ParseTreePatternMatcher} +# constructor are used to parse the pattern in string form. The lexer converts +# the {@code = ;} into a sequence of four tokens (assuming lexer +# throws out whitespace or puts it on a hidden channel). Be aware that the +# input stream is reset for the lexer (but not the parser; a +# {@link ParserInterpreter} is created to parse the input.). Any user-defined +# fields you have put into the lexer might get changed when this mechanism asks +# it to scan the pattern string.

      +# +#

      Normally a parser does not accept token {@code } as a valid +# {@code expr} but, from the parser passed in, we create a special version of +# the underlying grammar representation (an {@link ATN}) that allows imaginary +# tokens representing rules ({@code }) to match entire rules. We call +# these bypass alternatives.

      +# +#

      Delimiters are {@code <} and {@code >}, with {@code \} as the escape string +# by default, but you can set them to whatever you want using +# {@link #setDelimiters}. You must escape both start and stop strings +# {@code \<} and {@code \>}.

      +# +from antlr4.CommonTokenStream import CommonTokenStream +from antlr4.InputStream import InputStream +from antlr4.ParserRuleContext import ParserRuleContext +from antlr4.Lexer import Lexer +from antlr4.ListTokenSource import ListTokenSource +from antlr4.Token import Token +from antlr4.error.ErrorStrategy import BailErrorStrategy +from antlr4.error.Errors import RecognitionException, ParseCancellationException +from antlr4.tree.Chunk import TagChunk, TextChunk +from antlr4.tree.RuleTagToken import RuleTagToken +from antlr4.tree.TokenTagToken import TokenTagToken +from antlr4.tree.Tree import ParseTree, TerminalNode, RuleNode + +# need forward declaration +Parser = None +ParseTreePattern = None + +class CannotInvokeStartRule(Exception): + + def __init__(self, e:Exception): + super().__init__(e) + +class StartRuleDoesNotConsumeFullPattern(Exception): + + pass + + +class ParseTreePatternMatcher(object): + __slots__ = ('lexer', 'parser', 'start', 'stop', 'escape') + + # Constructs a {@link ParseTreePatternMatcher} or from a {@link Lexer} and + # {@link Parser} object. The lexer input stream is altered for tokenizing + # the tree patterns. The parser is used as a convenient mechanism to get + # the grammar name, plus token, rule names. + def __init__(self, lexer:Lexer, parser:Parser): + self.lexer = lexer + self.parser = parser + self.start = "<" + self.stop = ">" + self.escape = "\\" # e.g., \< and \> must escape BOTH! + + # Set the delimiters used for marking rule and token tags within concrete + # syntax used by the tree pattern parser. + # + # @param start The start delimiter. + # @param stop The stop delimiter. + # @param escapeLeft The escape sequence to use for escaping a start or stop delimiter. + # + # @exception IllegalArgumentException if {@code start} is {@code null} or empty. + # @exception IllegalArgumentException if {@code stop} is {@code null} or empty. + # + def setDelimiters(self, start:str, stop:str, escapeLeft:str): + if start is None or len(start)==0: + raise Exception("start cannot be null or empty") + if stop is None or len(stop)==0: + raise Exception("stop cannot be null or empty") + self.start = start + self.stop = stop + self.escape = escapeLeft + + # Does {@code pattern} matched as rule {@code patternRuleIndex} match {@code tree}?# + def matchesRuleIndex(self, tree:ParseTree, pattern:str, patternRuleIndex:int): + p = self.compileTreePattern(pattern, patternRuleIndex) + return self.matches(tree, p) + + # Does {@code pattern} matched as rule patternRuleIndex match tree? Pass in a + # compiled pattern instead of a string representation of a tree pattern. + # + def matchesPattern(self, tree:ParseTree, pattern:ParseTreePattern): + mismatchedNode = self.matchImpl(tree, pattern.patternTree, dict()) + return mismatchedNode is None + + # + # Compare {@code pattern} matched as rule {@code patternRuleIndex} against + # {@code tree} and return a {@link ParseTreeMatch} object that contains the + # matched elements, or the node at which the match failed. + # + def matchRuleIndex(self, tree:ParseTree, pattern:str, patternRuleIndex:int): + p = self.compileTreePattern(pattern, patternRuleIndex) + return self.matchPattern(tree, p) + + # + # Compare {@code pattern} matched against {@code tree} and return a + # {@link ParseTreeMatch} object that contains the matched elements, or the + # node at which the match failed. Pass in a compiled pattern instead of a + # string representation of a tree pattern. + # + def matchPattern(self, tree:ParseTree, pattern:ParseTreePattern): + labels = dict() + mismatchedNode = self.matchImpl(tree, pattern.patternTree, labels) + from antlr4.tree.ParseTreeMatch import ParseTreeMatch + return ParseTreeMatch(tree, pattern, labels, mismatchedNode) + + # + # For repeated use of a tree pattern, compile it to a + # {@link ParseTreePattern} using this method. + # + def compileTreePattern(self, pattern:str, patternRuleIndex:int): + tokenList = self.tokenize(pattern) + tokenSrc = ListTokenSource(tokenList) + tokens = CommonTokenStream(tokenSrc) + from antlr4.ParserInterpreter import ParserInterpreter + parserInterp = ParserInterpreter(self.parser.grammarFileName, self.parser.tokenNames, + self.parser.ruleNames, self.parser.getATNWithBypassAlts(),tokens) + tree = None + try: + parserInterp.setErrorHandler(BailErrorStrategy()) + tree = parserInterp.parse(patternRuleIndex) + except ParseCancellationException as e: + raise e.cause + except RecognitionException as e: + raise e + except Exception as e: + raise CannotInvokeStartRule(e) + + # Make sure tree pattern compilation checks for a complete parse + if tokens.LA(1)!=Token.EOF: + raise StartRuleDoesNotConsumeFullPattern() + + from antlr4.tree.ParseTreePattern import ParseTreePattern + return ParseTreePattern(self, pattern, patternRuleIndex, tree) + + # + # Recursively walk {@code tree} against {@code patternTree}, filling + # {@code match.}{@link ParseTreeMatch#labels labels}. + # + # @return the first node encountered in {@code tree} which does not match + # a corresponding node in {@code patternTree}, or {@code null} if the match + # was successful. The specific node returned depends on the matching + # algorithm used by the implementation, and may be overridden. + # + def matchImpl(self, tree:ParseTree, patternTree:ParseTree, labels:dict): + if tree is None: + raise Exception("tree cannot be null") + if patternTree is None: + raise Exception("patternTree cannot be null") + + # x and , x and y, or x and x; or could be mismatched types + if isinstance(tree, TerminalNode) and isinstance(patternTree, TerminalNode ): + mismatchedNode = None + # both are tokens and they have same type + if tree.symbol.type == patternTree.symbol.type: + if isinstance( patternTree.symbol, TokenTagToken ): # x and + tokenTagToken = patternTree.symbol + # track label->list-of-nodes for both token name and label (if any) + self.map(labels, tokenTagToken.tokenName, tree) + if tokenTagToken.label is not None: + self.map(labels, tokenTagToken.label, tree) + elif tree.getText()==patternTree.getText(): + # x and x + pass + else: + # x and y + if mismatchedNode is None: + mismatchedNode = tree + else: + if mismatchedNode is None: + mismatchedNode = tree + + return mismatchedNode + + if isinstance(tree, ParserRuleContext) and isinstance(patternTree, ParserRuleContext): + mismatchedNode = None + # (expr ...) and + ruleTagToken = self.getRuleTagToken(patternTree) + if ruleTagToken is not None: + m = None + if tree.ruleContext.ruleIndex == patternTree.ruleContext.ruleIndex: + # track label->list-of-nodes for both rule name and label (if any) + self.map(labels, ruleTagToken.ruleName, tree) + if ruleTagToken.label is not None: + self.map(labels, ruleTagToken.label, tree) + else: + if mismatchedNode is None: + mismatchedNode = tree + + return mismatchedNode + + # (expr ...) and (expr ...) + if tree.getChildCount()!=patternTree.getChildCount(): + if mismatchedNode is None: + mismatchedNode = tree + return mismatchedNode + + n = tree.getChildCount() + for i in range(0, n): + childMatch = self.matchImpl(tree.getChild(i), patternTree.getChild(i), labels) + if childMatch is not None: + return childMatch + + return mismatchedNode + + # if nodes aren't both tokens or both rule nodes, can't match + return tree + + def map(self, labels, label, tree): + v = labels.get(label, None) + if v is None: + v = list() + labels[label] = v + v.append(tree) + + # Is {@code t} {@code (expr )} subtree?# + def getRuleTagToken(self, tree:ParseTree): + if isinstance( tree, RuleNode ): + if tree.getChildCount()==1 and isinstance(tree.getChild(0), TerminalNode ): + c = tree.getChild(0) + if isinstance( c.symbol, RuleTagToken ): + return c.symbol + return None + + def tokenize(self, pattern:str): + # split pattern into chunks: sea (raw input) and islands (, ) + chunks = self.split(pattern) + + # create token stream from text and tags + tokens = list() + for chunk in chunks: + if isinstance( chunk, TagChunk ): + # add special rule token or conjure up new token from name + if chunk.tag[0].isupper(): + ttype = self.parser.getTokenType(chunk.tag) + if ttype==Token.INVALID_TYPE: + raise Exception("Unknown token " + str(chunk.tag) + " in pattern: " + pattern) + tokens.append(TokenTagToken(chunk.tag, ttype, chunk.label)) + elif chunk.tag[0].islower(): + ruleIndex = self.parser.getRuleIndex(chunk.tag) + if ruleIndex==-1: + raise Exception("Unknown rule " + str(chunk.tag) + " in pattern: " + pattern) + ruleImaginaryTokenType = self.parser.getATNWithBypassAlts().ruleToTokenType[ruleIndex] + tokens.append(RuleTagToken(chunk.tag, ruleImaginaryTokenType, chunk.label)) + else: + raise Exception("invalid tag: " + str(chunk.tag) + " in pattern: " + pattern) + else: + self.lexer.setInputStream(InputStream(chunk.text)) + t = self.lexer.nextToken() + while t.type!=Token.EOF: + tokens.append(t) + t = self.lexer.nextToken() + return tokens + + # Split {@code = ;} into 4 chunks for tokenizing by {@link #tokenize}.# + def split(self, pattern:str): + p = 0 + n = len(pattern) + chunks = list() + # find all start and stop indexes first, then collect + starts = list() + stops = list() + while p < n : + if p == pattern.find(self.escape + self.start, p): + p += len(self.escape) + len(self.start) + elif p == pattern.find(self.escape + self.stop, p): + p += len(self.escape) + len(self.stop) + elif p == pattern.find(self.start, p): + starts.append(p) + p += len(self.start) + elif p == pattern.find(self.stop, p): + stops.append(p) + p += len(self.stop) + else: + p += 1 + + nt = len(starts) + + if nt > len(stops): + raise Exception("unterminated tag in pattern: " + pattern) + if nt < len(stops): + raise Exception("missing start tag in pattern: " + pattern) + + for i in range(0, nt): + if starts[i] >= stops[i]: + raise Exception("tag delimiters out of order in pattern: " + pattern) + + # collect into chunks now + if nt==0: + chunks.append(TextChunk(pattern)) + + if nt>0 and starts[0]>0: # copy text up to first tag into chunks + text = pattern[0:starts[0]] + chunks.add(TextChunk(text)) + + for i in range(0, nt): + # copy inside of + tag = pattern[starts[i] + len(self.start) : stops[i]] + ruleOrToken = tag + label = None + colon = tag.find(':') + if colon >= 0: + label = tag[0:colon] + ruleOrToken = tag[colon+1 : len(tag)] + chunks.append(TagChunk(label, ruleOrToken)) + if i+1 < len(starts): + # copy from end of to start of next + text = pattern[stops[i] + len(self.stop) : starts[i + 1]] + chunks.append(TextChunk(text)) + + if nt > 0 : + afterLastTag = stops[nt - 1] + len(self.stop) + if afterLastTag < n : # copy text from end of last tag to end + text = pattern[afterLastTag : n] + chunks.append(TextChunk(text)) + + # strip out the escape sequences from text chunks but not tags + for i in range(0, len(chunks)): + c = chunks[i] + if isinstance( c, TextChunk ): + unescaped = c.text.replace(self.escape, "") + if len(unescaped) < len(c.text): + chunks[i] = TextChunk(unescaped) + return chunks diff --git a/lib/python3.12/site-packages/antlr4/tree/RuleTagToken.py b/lib/python3.12/site-packages/antlr4/tree/RuleTagToken.py new file mode 100644 index 0000000000000000000000000000000000000000..a198f7da13643d538ce96aeeb6a8ff4f757f1ecd --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/RuleTagToken.py @@ -0,0 +1,50 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# A {@link Token} object representing an entire subtree matched by a parser +# rule; e.g., {@code }. These tokens are created for {@link TagChunk} +# chunks where the tag corresponds to a parser rule. +# +from antlr4.Token import Token + + +class RuleTagToken(Token): + __slots__ = ('label', 'ruleName') + # + # Constructs a new instance of {@link RuleTagToken} with the specified rule + # name, bypass token type, and label. + # + # @param ruleName The name of the parser rule this rule tag matches. + # @param bypassTokenType The bypass token type assigned to the parser rule. + # @param label The label associated with the rule tag, or {@code null} if + # the rule tag is unlabeled. + # + # @exception IllegalArgumentException if {@code ruleName} is {@code null} + # or empty. + + def __init__(self, ruleName:str, bypassTokenType:int, label:str=None): + if ruleName is None or len(ruleName)==0: + raise Exception("ruleName cannot be null or empty.") + self.source = None + self.type = bypassTokenType # token type of the token + self.channel = Token.DEFAULT_CHANNEL # The parser ignores everything not on DEFAULT_CHANNEL + self.start = -1 # optional; return -1 if not implemented. + self.stop = -1 # optional; return -1 if not implemented. + self.tokenIndex = -1 # from 0..n-1 of the token object in the input stream + self.line = 0 # line=1..n of the 1st character + self.column = -1 # beginning of the line at which it occurs, 0..n-1 + self.label = label + self._text = self.getText() # text of the token. + + self.ruleName = ruleName + + + def getText(self): + if self.label is None: + return "<" + self.ruleName + ">" + else: + return "<" + self.label + ":" + self.ruleName + ">" diff --git a/lib/python3.12/site-packages/antlr4/tree/TokenTagToken.py b/lib/python3.12/site-packages/antlr4/tree/TokenTagToken.py new file mode 100644 index 0000000000000000000000000000000000000000..b7beeb87684c06606e17053f0f74fcae36876959 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/TokenTagToken.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# A {@link Token} object representing a token of a particular type; e.g., +# {@code }. These tokens are created for {@link TagChunk} chunks where the +# tag corresponds to a lexer rule or token type. +# +from antlr4.Token import CommonToken + + +class TokenTagToken(CommonToken): + __slots__ = ('tokenName', 'label') + # Constructs a new instance of {@link TokenTagToken} with the specified + # token name, type, and label. + # + # @param tokenName The token name. + # @param type The token type. + # @param label The label associated with the token tag, or {@code null} if + # the token tag is unlabeled. + # + def __init__(self, tokenName:str, type:int, label:str=None): + super().__init__(type=type) + self.tokenName = tokenName + self.label = label + self._text = self.getText() + + # + # {@inheritDoc} + # + #

      The implementation for {@link TokenTagToken} returns the token tag + # formatted with {@code <} and {@code >} delimiters.

      + # + def getText(self): + if self.label is None: + return "<" + self.tokenName + ">" + else: + return "<" + self.label + ":" + self.tokenName + ">" + + #

      The implementation for {@link TokenTagToken} returns a string of the form + # {@code tokenName:type}.

      + # + def __str__(self): + return self.tokenName + ":" + str(self.type) diff --git a/lib/python3.12/site-packages/antlr4/tree/Tree.py b/lib/python3.12/site-packages/antlr4/tree/Tree.py new file mode 100644 index 0000000000000000000000000000000000000000..812acc96bbee97860bc8a914feedcd0584def050 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/Tree.py @@ -0,0 +1,191 @@ +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +#/ + + +# The basic notion of a tree has a parent, a payload, and a list of children. +# It is the most abstract interface for all the trees used by ANTLR. +#/ +from antlr4.Token import Token + +INVALID_INTERVAL = (-1, -2) + +class Tree(object): + pass + +class SyntaxTree(Tree): + pass + +class ParseTree(SyntaxTree): + pass + +class RuleNode(ParseTree): + pass + +class TerminalNode(ParseTree): + pass + +class ErrorNode(TerminalNode): + pass + +class ParseTreeVisitor(object): + def visit(self, tree): + return tree.accept(self) + + def visitChildren(self, node): + result = self.defaultResult() + n = node.getChildCount() + for i in range(n): + if not self.shouldVisitNextChild(node, result): + return result + + c = node.getChild(i) + childResult = c.accept(self) + result = self.aggregateResult(result, childResult) + + return result + + def visitTerminal(self, node): + return self.defaultResult() + + def visitErrorNode(self, node): + return self.defaultResult() + + def defaultResult(self): + return None + + def aggregateResult(self, aggregate, nextResult): + return nextResult + + def shouldVisitNextChild(self, node, currentResult): + return True + +ParserRuleContext = None + +class ParseTreeListener(object): + + def visitTerminal(self, node:TerminalNode): + pass + + def visitErrorNode(self, node:ErrorNode): + pass + + def enterEveryRule(self, ctx:ParserRuleContext): + pass + + def exitEveryRule(self, ctx:ParserRuleContext): + pass + +del ParserRuleContext + +class TerminalNodeImpl(TerminalNode): + __slots__ = ('parentCtx', 'symbol') + + def __init__(self, symbol:Token): + self.parentCtx = None + self.symbol = symbol + def __setattr__(self, key, value): + super().__setattr__(key, value) + + def getChild(self, i:int): + return None + + def getSymbol(self): + return self.symbol + + def getParent(self): + return self.parentCtx + + def getPayload(self): + return self.symbol + + def getSourceInterval(self): + if self.symbol is None: + return INVALID_INTERVAL + tokenIndex = self.symbol.tokenIndex + return (tokenIndex, tokenIndex) + + def getChildCount(self): + return 0 + + def accept(self, visitor:ParseTreeVisitor): + return visitor.visitTerminal(self) + + def getText(self): + return self.symbol.text + + def __str__(self): + if self.symbol.type == Token.EOF: + return "" + else: + return self.symbol.text + +# Represents a token that was consumed during resynchronization +# rather than during a valid match operation. For example, +# we will create this kind of a node during single token insertion +# and deletion as well as during "consume until error recovery set" +# upon no viable alternative exceptions. + +class ErrorNodeImpl(TerminalNodeImpl,ErrorNode): + + def __init__(self, token:Token): + super().__init__(token) + + def accept(self, visitor:ParseTreeVisitor): + return visitor.visitErrorNode(self) + + +class ParseTreeWalker(object): + + DEFAULT = None + + def walk(self, listener:ParseTreeListener, t:ParseTree): + """ + Performs a walk on the given parse tree starting at the root and going down recursively + with depth-first search. On each node, {@link ParseTreeWalker#enterRule} is called before + recursively walking down into child nodes, then + {@link ParseTreeWalker#exitRule} is called after the recursive call to wind up. + @param listener The listener used by the walker to process grammar rules + @param t The parse tree to be walked on + """ + if isinstance(t, ErrorNode): + listener.visitErrorNode(t) + return + elif isinstance(t, TerminalNode): + listener.visitTerminal(t) + return + self.enterRule(listener, t) + for child in t.getChildren(): + self.walk(listener, child) + self.exitRule(listener, t) + + # + # The discovery of a rule node, involves sending two events: the generic + # {@link ParseTreeListener#enterEveryRule} and a + # {@link RuleContext}-specific event. First we trigger the generic and then + # the rule specific. We to them in reverse order upon finishing the node. + # + def enterRule(self, listener:ParseTreeListener, r:RuleNode): + """ + Enters a grammar rule by first triggering the generic event {@link ParseTreeListener#enterEveryRule} + then by triggering the event specific to the given parse tree node + @param listener The listener responding to the trigger events + @param r The grammar rule containing the rule context + """ + ctx = r.getRuleContext() + listener.enterEveryRule(ctx) + ctx.enterRule(listener) + + def exitRule(self, listener:ParseTreeListener, r:RuleNode): + """ + Exits a grammar rule by first triggering the event specific to the given parse tree node + then by triggering the generic event {@link ParseTreeListener#exitEveryRule} + @param listener The listener responding to the trigger events + @param r The grammar rule containing the rule context + """ + ctx = r.getRuleContext() + ctx.exitRule(listener) + listener.exitEveryRule(ctx) + +ParseTreeWalker.DEFAULT = ParseTreeWalker() diff --git a/lib/python3.12/site-packages/antlr4/tree/Trees.py b/lib/python3.12/site-packages/antlr4/tree/Trees.py new file mode 100644 index 0000000000000000000000000000000000000000..686b8cb287b3058c2e0b33dfb1567320299f214e --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/tree/Trees.py @@ -0,0 +1,111 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + + +# A set of utility routines useful for all kinds of ANTLR trees.# +from io import StringIO +from antlr4.Token import Token +from antlr4.Utils import escapeWhitespace +from antlr4.tree.Tree import RuleNode, ErrorNode, TerminalNode, Tree, ParseTree + +# need forward declaration +Parser = None + +class Trees(object): + + # Print out a whole tree in LISP form. {@link #getNodeText} is used on the + # node payloads to get the text for the nodes. Detect + # parse trees and extract data appropriately. + @classmethod + def toStringTree(cls, t:Tree, ruleNames:list=None, recog:Parser=None): + if recog is not None: + ruleNames = recog.ruleNames + s = escapeWhitespace(cls.getNodeText(t, ruleNames), False) + if t.getChildCount()==0: + return s + with StringIO() as buf: + buf.write("(") + buf.write(s) + buf.write(' ') + for i in range(0, t.getChildCount()): + if i > 0: + buf.write(' ') + buf.write(cls.toStringTree(t.getChild(i), ruleNames)) + buf.write(")") + return buf.getvalue() + + @classmethod + def getNodeText(cls, t:Tree, ruleNames:list=None, recog:Parser=None): + if recog is not None: + ruleNames = recog.ruleNames + if ruleNames is not None: + if isinstance(t, RuleNode): + if t.getAltNumber()!=0: # should use ATN.INVALID_ALT_NUMBER but won't compile + return ruleNames[t.getRuleIndex()]+":"+str(t.getAltNumber()) + return ruleNames[t.getRuleIndex()] + elif isinstance( t, ErrorNode): + return str(t) + elif isinstance(t, TerminalNode): + if t.symbol is not None: + return t.symbol.text + # no recog for rule names + payload = t.getPayload() + if isinstance(payload, Token ): + return payload.text + return str(t.getPayload()) + + + # Return ordered list of all children of this node + @classmethod + def getChildren(cls, t:Tree): + return [ t.getChild(i) for i in range(0, t.getChildCount()) ] + + # Return a list of all ancestors of this node. The first node of + # list is the root and the last is the parent of this node. + # + @classmethod + def getAncestors(cls, t:Tree): + ancestors = [] + t = t.getParent() + while t is not None: + ancestors.insert(0, t) # insert at start + t = t.getParent() + return ancestors + + @classmethod + def findAllTokenNodes(cls, t:ParseTree, ttype:int): + return cls.findAllNodes(t, ttype, True) + + @classmethod + def findAllRuleNodes(cls, t:ParseTree, ruleIndex:int): + return cls.findAllNodes(t, ruleIndex, False) + + @classmethod + def findAllNodes(cls, t:ParseTree, index:int, findTokens:bool): + nodes = [] + cls._findAllNodes(t, index, findTokens, nodes) + return nodes + + @classmethod + def _findAllNodes(cls, t:ParseTree, index:int, findTokens:bool, nodes:list): + from antlr4.ParserRuleContext import ParserRuleContext + # check this node (the root) first + if findTokens and isinstance(t, TerminalNode): + if t.symbol.type==index: + nodes.append(t) + elif not findTokens and isinstance(t, ParserRuleContext): + if t.ruleIndex == index: + nodes.append(t) + # check children + for i in range(0, t.getChildCount()): + cls._findAllNodes(t.getChild(i), index, findTokens, nodes) + + @classmethod + def descendants(cls, t:ParseTree): + nodes = [t] + for i in range(0, t.getChildCount()): + nodes.extend(cls.descendants(t.getChild(i))) + return nodes diff --git a/lib/python3.12/site-packages/antlr4/tree/__init__.py b/lib/python3.12/site-packages/antlr4/tree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/Chunk.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/Chunk.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69403b088e2b49e91d369a8346a58ffa32bf20b7 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/Chunk.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreeMatch.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreeMatch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b54a223391df40ff5339a6bf91f0ecdf7d9d0342 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreeMatch.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreePattern.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreePattern.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c83520a269309d32bae649c8dbded349c55b4ace Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreePattern.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreePatternMatcher.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreePatternMatcher.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2778e9bfb45b181fff14574ff14ea1501bc4fcd5 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/ParseTreePatternMatcher.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/RuleTagToken.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/RuleTagToken.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c1f68a13755d6a8501b33dbfc39c598ffb3c8b8 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/RuleTagToken.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/TokenTagToken.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/TokenTagToken.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65610c472ba6787c8cfb75c8789eac093f563c06 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/TokenTagToken.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/Tree.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/Tree.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b03a403dc7f8d2c884dfe09b2caca14a66e7daa1 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/Tree.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/Trees.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/Trees.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34eb0b3d9c3302989b3548b2b1ab32f015d400c9 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/Trees.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/tree/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/tree/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0b13d083063719f9c7fd753af282e4f519b187b Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/tree/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/xpath/XPath.py b/lib/python3.12/site-packages/antlr4/xpath/XPath.py new file mode 100644 index 0000000000000000000000000000000000000000..24029e7cf2bfa9798538211bc7599b49dbb70191 --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/xpath/XPath.py @@ -0,0 +1,352 @@ +# +# Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. +# Use of this file is governed by the BSD 3-clause license that +# can be found in the LICENSE.txt file in the project root. +# + +# +# Represent a subset of XPath XML path syntax for use in identifying nodes in +# parse trees. +# +#

      +# Split path into words and separators {@code /} and {@code //} via ANTLR +# itself then walk path elements from left to right. At each separator-word +# pair, find set of nodes. Next stage uses those as work list.

      +# +#

      +# The basic interface is +# {@link XPath#findAll ParseTree.findAll}{@code (tree, pathString, parser)}. +# But that is just shorthand for:

      +# +#
      +# {@link XPath} p = new {@link XPath#XPath XPath}(parser, pathString);
      +# return p.{@link #evaluate evaluate}(tree);
      +# 
      +# +#

      +# See {@code org.antlr.v4.test.TestXPath} for descriptions. In short, this +# allows operators:

      +# +#
      +#
      /
      root
      +#
      //
      anywhere
      +#
      !
      invert; this must appear directly after root or anywhere +# operator
      +#
      +# +#

      +# and path elements:

      +# +#
      +#
      ID
      token name
      +#
      'string'
      any string literal token from the grammar
      +#
      expr
      rule name
      +#
      *
      wildcard matching any node
      +#
      +# +#

      +# Whitespace is not allowed.

      +# +from antlr4 import CommonTokenStream, DFA, PredictionContextCache, Lexer, LexerATNSimulator, ParserRuleContext, TerminalNode +from antlr4.InputStream import InputStream +from antlr4.Parser import Parser +from antlr4.RuleContext import RuleContext +from antlr4.Token import Token +from antlr4.atn.ATNDeserializer import ATNDeserializer +from antlr4.error.ErrorListener import ErrorListener +from antlr4.error.Errors import LexerNoViableAltException +from antlr4.tree.Tree import ParseTree +from antlr4.tree.Trees import Trees +from io import StringIO + + +def serializedATN(): + with StringIO() as buf: + buf.write("\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\2\n") + buf.write("\64\b\1\4\2\t\2\4\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t") + buf.write("\7\4\b\t\b\4\t\t\t\3\2\3\2\3\2\3\3\3\3\3\4\3\4\3\5\3\5") + buf.write("\3\6\3\6\7\6\37\n\6\f\6\16\6\"\13\6\3\6\3\6\3\7\3\7\5") + buf.write("\7(\n\7\3\b\3\b\3\t\3\t\7\t.\n\t\f\t\16\t\61\13\t\3\t") + buf.write("\3\t\3/\2\n\3\5\5\6\7\7\t\b\13\t\r\2\17\2\21\n\3\2\4\7") + buf.write("\2\62;aa\u00b9\u00b9\u0302\u0371\u2041\u2042\17\2C\\c") + buf.write("|\u00c2\u00d8\u00da\u00f8\u00fa\u0301\u0372\u037f\u0381") + buf.write("\u2001\u200e\u200f\u2072\u2191\u2c02\u2ff1\u3003\ud801") + buf.write("\uf902\ufdd1\ufdf2\uffff\64\2\3\3\2\2\2\2\5\3\2\2\2\2") + buf.write("\7\3\2\2\2\2\t\3\2\2\2\2\13\3\2\2\2\2\21\3\2\2\2\3\23") + buf.write("\3\2\2\2\5\26\3\2\2\2\7\30\3\2\2\2\t\32\3\2\2\2\13\34") + buf.write("\3\2\2\2\r\'\3\2\2\2\17)\3\2\2\2\21+\3\2\2\2\23\24\7\61") + buf.write("\2\2\24\25\7\61\2\2\25\4\3\2\2\2\26\27\7\61\2\2\27\6\3") + buf.write("\2\2\2\30\31\7,\2\2\31\b\3\2\2\2\32\33\7#\2\2\33\n\3\2") + buf.write("\2\2\34 \5\17\b\2\35\37\5\r\7\2\36\35\3\2\2\2\37\"\3\2") + buf.write("\2\2 \36\3\2\2\2 !\3\2\2\2!#\3\2\2\2\" \3\2\2\2#$\b\6") + buf.write("\2\2$\f\3\2\2\2%(\5\17\b\2&(\t\2\2\2\'%\3\2\2\2\'&\3\2") + buf.write("\2\2(\16\3\2\2\2)*\t\3\2\2*\20\3\2\2\2+/\7)\2\2,.\13\2") + buf.write("\2\2-,\3\2\2\2.\61\3\2\2\2/\60\3\2\2\2/-\3\2\2\2\60\62") + buf.write("\3\2\2\2\61/\3\2\2\2\62\63\7)\2\2\63\22\3\2\2\2\6\2 \'") + buf.write("/\3\3\6\2") + return buf.getvalue() + + +class XPathLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + + TOKEN_REF = 1 + RULE_REF = 2 + ANYWHERE = 3 + ROOT = 4 + WILDCARD = 5 + BANG = 6 + ID = 7 + STRING = 8 + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "'//'", "'/'", "'*'", "'!'" ] + + symbolicNames = [ "", + "TOKEN_REF", "RULE_REF", "ANYWHERE", "ROOT", "WILDCARD", "BANG", + "ID", "STRING" ] + + ruleNames = [ "ANYWHERE", "ROOT", "WILDCARD", "BANG", "ID", "NameChar", + "NameStartChar", "STRING" ] + + grammarFileName = "XPathLexer.g4" + + def __init__(self, input=None): + super().__init__(input) + self.checkVersion("4.9.1") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + + def action(self, localctx:RuleContext, ruleIndex:int, actionIndex:int): + if self._actions is None: + actions = dict() + actions[4] = self.ID_action + self._actions = actions + _action = self._actions.get(ruleIndex, None) + if _action is not None: + _action(localctx, actionIndex) + else: + raise Exception("No registered action for: %d" % ruleIndex) + + def ID_action(self, localctx:RuleContext , actionIndex:int): + if actionIndex == 0: + char = self.text[0] + if char.isupper(): + self.type = XPathLexer.TOKEN_REF + else: + self.type = XPathLexer.RULE_REF + +class XPath(object): + + WILDCARD = "*" # word not operator/separator + NOT = "!" # word for invert operator + + def __init__(self, parser:Parser, path:str): + self.parser = parser + self.path = path + self.elements = self.split(path) + + def split(self, path:str): + input = InputStream(path) + lexer = XPathLexer(input) + def recover(self, e): + raise e + lexer.recover = recover + lexer.removeErrorListeners() + lexer.addErrorListener(ErrorListener()) # XPathErrorListener does no more + tokenStream = CommonTokenStream(lexer) + try: + tokenStream.fill() + except LexerNoViableAltException as e: + pos = lexer.column + msg = "Invalid tokens or characters at index %d in path '%s'" % (pos, path) + raise Exception(msg, e) + + tokens = iter(tokenStream.tokens) + elements = list() + for el in tokens: + invert = False + anywhere = False + # Check for path separators, if none assume root + if el.type in [XPathLexer.ROOT, XPathLexer.ANYWHERE]: + anywhere = el.type == XPathLexer.ANYWHERE + next_el = next(tokens, None) + if not next_el: + raise Exception('Missing element after %s' % el.getText()) + else: + el = next_el + # Check for bangs + if el.type == XPathLexer.BANG: + invert = True + next_el = next(tokens, None) + if not next_el: + raise Exception('Missing element after %s' % el.getText()) + else: + el = next_el + # Add searched element + if el.type in [XPathLexer.TOKEN_REF, XPathLexer.RULE_REF, XPathLexer.WILDCARD, XPathLexer.STRING]: + element = self.getXPathElement(el, anywhere) + element.invert = invert + elements.append(element) + elif el.type==Token.EOF: + break + else: + raise Exception("Unknown path element %s" % lexer.symbolicNames[el.type]) + return elements + + # + # Convert word like {@code#} or {@code ID} or {@code expr} to a path + # element. {@code anywhere} is {@code true} if {@code //} precedes the + # word. + # + def getXPathElement(self, wordToken:Token, anywhere:bool): + if wordToken.type==Token.EOF: + raise Exception("Missing path element at end of path") + + word = wordToken.text + if wordToken.type==XPathLexer.WILDCARD : + return XPathWildcardAnywhereElement() if anywhere else XPathWildcardElement() + + elif wordToken.type in [XPathLexer.TOKEN_REF, XPathLexer.STRING]: + tsource = self.parser.getTokenStream().tokenSource + + ttype = Token.INVALID_TYPE + if wordToken.type == XPathLexer.TOKEN_REF: + if word in tsource.ruleNames: + ttype = tsource.ruleNames.index(word) + 1 + else: + if word in tsource.literalNames: + ttype = tsource.literalNames.index(word) + + if ttype == Token.INVALID_TYPE: + raise Exception("%s at index %d isn't a valid token name" % (word, wordToken.tokenIndex)) + return XPathTokenAnywhereElement(word, ttype) if anywhere else XPathTokenElement(word, ttype) + + else: + ruleIndex = self.parser.ruleNames.index(word) if word in self.parser.ruleNames else -1 + + if ruleIndex == -1: + raise Exception("%s at index %d isn't a valid rule name" % (word, wordToken.tokenIndex)) + return XPathRuleAnywhereElement(word, ruleIndex) if anywhere else XPathRuleElement(word, ruleIndex) + + + @staticmethod + def findAll(tree:ParseTree, xpath:str, parser:Parser): + p = XPath(parser, xpath) + return p.evaluate(tree) + + # + # Return a list of all nodes starting at {@code t} as root that satisfy the + # path. The root {@code /} is relative to the node passed to + # {@link #evaluate}. + # + def evaluate(self, t:ParseTree): + dummyRoot = ParserRuleContext() + dummyRoot.children = [t] # don't set t's parent. + + work = [dummyRoot] + for element in self.elements: + work_next = list() + for node in work: + if not isinstance(node, TerminalNode) and node.children: + # only try to match next element if it has children + # e.g., //func/*/stat might have a token node for which + # we can't go looking for stat nodes. + matching = element.evaluate(node) + + # See issue antlr#370 - Prevents XPath from returning the + # same node multiple times + matching = filter(lambda m: m not in work_next, matching) + + work_next.extend(matching) + work = work_next + + return work + + +class XPathElement(object): + + def __init__(self, nodeName:str): + self.nodeName = nodeName + self.invert = False + + def __str__(self): + return type(self).__name__ + "[" + ("!" if self.invert else "") + self.nodeName + "]" + + + +# +# Either {@code ID} at start of path or {@code ...//ID} in middle of path. +# +class XPathRuleAnywhereElement(XPathElement): + + def __init__(self, ruleName:str, ruleIndex:int): + super().__init__(ruleName) + self.ruleIndex = ruleIndex + + def evaluate(self, t:ParseTree): + # return all ParserRuleContext descendants of t that match ruleIndex (or do not match if inverted) + return filter(lambda c: isinstance(c, ParserRuleContext) and (self.invert ^ (c.getRuleIndex() == self.ruleIndex)), Trees.descendants(t)) + +class XPathRuleElement(XPathElement): + + def __init__(self, ruleName:str, ruleIndex:int): + super().__init__(ruleName) + self.ruleIndex = ruleIndex + + def evaluate(self, t:ParseTree): + # return all ParserRuleContext children of t that match ruleIndex (or do not match if inverted) + return filter(lambda c: isinstance(c, ParserRuleContext) and (self.invert ^ (c.getRuleIndex() == self.ruleIndex)), Trees.getChildren(t)) + +class XPathTokenAnywhereElement(XPathElement): + + def __init__(self, ruleName:str, tokenType:int): + super().__init__(ruleName) + self.tokenType = tokenType + + def evaluate(self, t:ParseTree): + # return all TerminalNode descendants of t that match tokenType (or do not match if inverted) + return filter(lambda c: isinstance(c, TerminalNode) and (self.invert ^ (c.symbol.type == self.tokenType)), Trees.descendants(t)) + +class XPathTokenElement(XPathElement): + + def __init__(self, ruleName:str, tokenType:int): + super().__init__(ruleName) + self.tokenType = tokenType + + def evaluate(self, t:ParseTree): + # return all TerminalNode children of t that match tokenType (or do not match if inverted) + return filter(lambda c: isinstance(c, TerminalNode) and (self.invert ^ (c.symbol.type == self.tokenType)), Trees.getChildren(t)) + + +class XPathWildcardAnywhereElement(XPathElement): + + def __init__(self): + super().__init__(XPath.WILDCARD) + + def evaluate(self, t:ParseTree): + if self.invert: + return list() # !* is weird but valid (empty) + else: + return Trees.descendants(t) + + +class XPathWildcardElement(XPathElement): + + def __init__(self): + super().__init__(XPath.WILDCARD) + + + def evaluate(self, t:ParseTree): + if self.invert: + return list() # !* is weird but valid (empty) + else: + return Trees.getChildren(t) diff --git a/lib/python3.12/site-packages/antlr4/xpath/__init__.py b/lib/python3.12/site-packages/antlr4/xpath/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..216c000dc5ffc8e53cc9c596e420c1e67604d1aa --- /dev/null +++ b/lib/python3.12/site-packages/antlr4/xpath/__init__.py @@ -0,0 +1 @@ +__author__ = 'ericvergnaud' diff --git a/lib/python3.12/site-packages/antlr4/xpath/__pycache__/XPath.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/xpath/__pycache__/XPath.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ece67a311cfe4fbfadab875fcfd21096fb357e76 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/xpath/__pycache__/XPath.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/antlr4/xpath/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/antlr4/xpath/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a103fb7402469bd05b6d006467ea0ed3de555b54 Binary files /dev/null and b/lib/python3.12/site-packages/antlr4/xpath/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__init__.py b/lib/python3.12/site-packages/attr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6e0650bc4bf53806420d7ef5f881ecd2bd77ea --- /dev/null +++ b/lib/python3.12/site-packages/attr/__init__.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: MIT + +""" +Classes Without Boilerplate +""" + +from functools import partial +from typing import Callable, Literal, Protocol + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Converter, + Factory, + _Nothing, + attrib, + attrs, + evolve, + fields, + fields_dict, + make_class, + validate, +) +from ._next_gen import define, field, frozen, mutable +from ._version_info import VersionInfo + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + + +class AttrsInstance(Protocol): + pass + + +NothingType = Literal[_Nothing.NOTHING] + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "Converter", + "Factory", + "NothingType", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "get_run_validators", + "has", + "ib", + "make_class", + "mutable", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + + +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + if name not in ("__version__", "__version_info__"): + msg = f"module {mod_name} has no attribute {name}" + raise AttributeError(msg) + + from importlib.metadata import metadata + + meta = metadata("attrs") + + if name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + + return meta["version"] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/lib/python3.12/site-packages/attr/__init__.pyi b/lib/python3.12/site-packages/attr/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..8d78fa19abe1a529af98889eb40017c32d5b62a2 --- /dev/null +++ b/lib/python3.12/site-packages/attr/__init__.pyi @@ -0,0 +1,389 @@ +import enum +import sys + +from typing import ( + Any, + Callable, + Generic, + Literal, + Mapping, + Protocol, + Sequence, + TypeVar, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ +from ._version_info import VersionInfo +from attrs import ( + define as define, + field as field, + mutable as mutable, + frozen as frozen, + _EqOrderType, + _ValidatorType, + _ConverterType, + _ReprArgType, + _OnSetAttrType, + _OnSetAttrArgType, + _FieldTransformer, + _ValidatorArgType, +) + +if sys.version_info >= (3, 10): + from typing import TypeGuard, TypeAlias +else: + from typing_extensions import TypeGuard, TypeAlias + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_FilterType = Callable[["Attribute[_T]", _T], bool] + +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass + +_A = TypeVar("_A", bound=type[AttrsInstance]) + +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING +NothingType: TypeAlias = Literal[_Nothing.NOTHING] + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. + +@overload +def Factory(factory: Callable[[], _T]) -> _T: ... +@overload +def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], +) -> _T: ... +@overload +def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], +) -> _T: ... + +In = TypeVar("In") +Out = TypeVar("Out") + +class Converter(Generic[In, Out]): + @overload + def __init__(self, converter: Callable[[In], Out]) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance, Attribute], Out], + *, + takes_self: Literal[True], + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, Attribute], Out], + *, + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance], Out], + *, + takes_self: Literal[True], + ) -> None: ... + +class Attribute(Generic[_T]): + name: str + default: _T | None + validator: _ValidatorType[_T] | None + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: bool | None + init: bool + converter: Converter | None + metadata: dict[Any, Any] + type: type[_T] | None + kw_only: bool + on_setattr: _OnSetAttrType + alias: str | None + + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: object = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: _C, + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> _C: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> Callable[[_C], _C]: ... +def fields(cls: type[AttrsInstance]) -> Any: ... +def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... +def validate(inst: AttrsInstance) -> None: ... +def resolve_types( + cls: _A, + globalns: dict[str, Any] | None = ..., + localns: dict[str, Any] | None = ..., + attribs: list[Attribute[Any]] | None = ..., + include_extras: bool = ..., +) -> _A: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: list[str] | tuple[str, ...] | dict[str, Any], + bases: tuple[type, ...] = ..., + class_body: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + collect_by_mro: bool = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! +def asdict( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + dict_factory: type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Callable[[type, Attribute[Any], Any], Any] | None = ..., + tuple_keys: bool | None = ..., +) -> dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + tuple_factory: type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> tuple[Any, ...]: ... +def has(cls: type) -> TypeGuard[type[AttrsInstance]]: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/lib/python3.12/site-packages/attr/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e8da84d2a5c875bbb841f5b4f0ef0604dc8434c Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/_cmp.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/_cmp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b16bd49364caeec4e7fe54991d94c2d629e7629 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/_cmp.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/_compat.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/_compat.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93882c3670cf096e7ed5263f9d5a69d8e798f2d8 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/_compat.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/_config.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac3be2a86231bec6104052c4c8bb94f36bf0641a Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/_config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/_funcs.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/_funcs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f56c0213fa474b21071ca5978b3587702115c19 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/_funcs.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/_next_gen.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/_next_gen.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39bb48431fc89f07899854cd122d69ddde2d5663 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/_next_gen.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/_version_info.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/_version_info.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48c5657bc69d0e9120dea95c60162e965b5d67d1 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/_version_info.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/converters.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/converters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04c4d3bcf3c2843a5f5c6ed0128db755125a9833 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/converters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/exceptions.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81a08d0b9475e38bebd1216bf0656e9878a0d0cb Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/exceptions.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/filters.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99569bc779162d5cacce9c4fa53862c883346fd5 Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/filters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/setters.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/setters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83d1d4c608379d99f43571eed93e4821656a986b Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/setters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/__pycache__/validators.cpython-312.pyc b/lib/python3.12/site-packages/attr/__pycache__/validators.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01c4d4dd44abb1fd02500753fcc12778e901776d Binary files /dev/null and b/lib/python3.12/site-packages/attr/__pycache__/validators.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attr/_cmp.py b/lib/python3.12/site-packages/attr/_cmp.py new file mode 100644 index 0000000000000000000000000000000000000000..09bab491f83ef4d15129f34b5f5a9e69bb34d63c --- /dev/null +++ b/lib/python3.12/site-packages/attr/_cmp.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: MIT + + +import functools +import types + +from ._make import __ne__ + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + Args: + eq (typing.Callable | None): + Callable used to evaluate equality of two objects. + + lt (typing.Callable | None): + Callable used to evaluate whether one object is less than another + object. + + le (typing.Callable | None): + Callable used to evaluate whether one object is less than or equal + to another object. + + gt (typing.Callable | None): + Callable used to evaluate whether one object is greater than + another object. + + ge (typing.Callable | None): + Callable used to evaluate whether one object is greater than or + equal to another object. + + require_same_type (bool): + When `True`, equality and ordering methods will return + `NotImplemented` if objects are not of the same type. + + class_name (str | None): Name of class. Defaults to "Comparable". + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = __ne__ + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = types.new_class( + class_name, (object,), {}, lambda ns: ns.update(body) + ) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + msg = "eq must be define is order to complete ordering from lt, le, gt, ge." + raise ValueError(msg) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + return all(func(self, other) for func in self._requirements) + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/lib/python3.12/site-packages/attr/_cmp.pyi b/lib/python3.12/site-packages/attr/_cmp.pyi new file mode 100644 index 0000000000000000000000000000000000000000..cc7893b04520afa719b1412c7646c3c1b39bf94b --- /dev/null +++ b/lib/python3.12/site-packages/attr/_cmp.pyi @@ -0,0 +1,13 @@ +from typing import Any, Callable + +_CompareWithType = Callable[[Any, Any], bool] + +def cmp_using( + eq: _CompareWithType | None = ..., + lt: _CompareWithType | None = ..., + le: _CompareWithType | None = ..., + gt: _CompareWithType | None = ..., + ge: _CompareWithType | None = ..., + require_same_type: bool = ..., + class_name: str = ..., +) -> type: ... diff --git a/lib/python3.12/site-packages/attr/_compat.py b/lib/python3.12/site-packages/attr/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..bc68ed9eaf9853bcc6065e7691209a787bf2eccc --- /dev/null +++ b/lib/python3.12/site-packages/attr/_compat.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: MIT + +import inspect +import platform +import sys +import threading + +from collections.abc import Mapping, Sequence # noqa: F401 +from typing import _GenericAlias + + +PYPY = platform.python_implementation() == "PyPy" +PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) +PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) +PY_3_13_PLUS = sys.version_info[:2] >= (3, 13) +PY_3_14_PLUS = sys.version_info[:2] >= (3, 14) + + +if PY_3_14_PLUS: + import annotationlib + + # We request forward-ref annotations to not break in the presence of + # forward references. + + def _get_annotations(cls): + return annotationlib.get_annotations( + cls, format=annotationlib.Format.FORWARDREF + ) + +else: + + def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + return cls.__dict__.get("__annotations__", {}) + + +class _AnnotationExtractor: + """ + Extract type annotations from a callable, returning None whenever there + is none. + """ + + __slots__ = ["sig"] + + def __init__(self, callable): + try: + self.sig = inspect.signature(callable) + except (ValueError, TypeError): # inspect failed + self.sig = None + + def get_first_param_type(self): + """ + Return the type annotation of the first argument if it's not empty. + """ + if not self.sig: + return None + + params = list(self.sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + return params[0].annotation + + return None + + def get_return_type(self): + """ + Return the return type if it's not empty. + """ + if ( + self.sig + and self.sig.return_annotation is not inspect.Signature.empty + ): + return self.sig.return_annotation + + return None + + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/lib/python3.12/site-packages/attr/_config.py b/lib/python3.12/site-packages/attr/_config.py new file mode 100644 index 0000000000000000000000000000000000000000..4b257726fb1e8b95583ecc3eee8d153336dc4089 --- /dev/null +++ b/lib/python3.12/site-packages/attr/_config.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT + +__all__ = ["get_run_validators", "set_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` + instead. + """ + if not isinstance(run, bool): + msg = "'run' must be bool." + raise TypeError(msg) + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` + instead. + """ + return _run_validators diff --git a/lib/python3.12/site-packages/attr/_funcs.py b/lib/python3.12/site-packages/attr/_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..1adb50021373d9c09fcb9db0641bbc03248d54a3 --- /dev/null +++ b/lib/python3.12/site-packages/attr/_funcs.py @@ -0,0 +1,497 @@ +# SPDX-License-Identifier: MIT + + +import copy + +from ._compat import get_generic_base +from ._make import _OBJ_SETATTR, NOTHING, fields +from .exceptions import AttrsAttributeNotFoundError + + +_ATOMIC_TYPES = frozenset( + { + type(None), + bool, + int, + float, + str, + complex, + bytes, + type(...), + type, + range, + property, + } +) + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the *attrs* attribute values of *inst* as a dict. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + dict_factory (~typing.Callable): + A callable to produce dictionaries from. For example, to produce + ordered dictionaries instead of normal Python dictionaries, pass in + ``collections.OrderedDict``. + + retain_collection_types (bool): + Do not convert to `list` when encountering an attribute whose type + is `tuple` or `set`. Only meaningful if *recurse* is `True`. + + value_serializer (typing.Callable | None): + A hook that is called for every attribute or dict key/value. It + receives the current instance, field and value and must return the + (updated) value. The hook is run *after* the optional *filter* has + been applied. + + Returns: + Return type of *dict_factory*. + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 + If a dict has a collection for a key, it is serialized as a tuple. + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + value_type = type(v) + if value_type in _ATOMIC_TYPES: + rv[a.name] = v + elif has(value_type): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif issubclass(value_type, (tuple, list, set, frozenset)): + cf = value_type if retain_collection_types is True else list + items = [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + try: + rv[a.name] = cf(items) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv[a.name] = cf(*items) + elif issubclass(value_type, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in v.items() + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + val_type = type(val) + if val_type in _ATOMIC_TYPES: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + elif getattr(val_type, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif issubclass(val_type, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif issubclass(val_type, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in val.items() + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the *attrs* attribute values of *inst* as a tuple. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): + Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + tuple_factory (~typing.Callable): + A callable to produce tuples from. For example, to produce lists + instead of tuples. + + retain_collection_types (bool): + Do not convert to `list` or `dict` when encountering an attribute + which type is `tuple`, `dict` or `set`. Only meaningful if + *recurse* is `True`. + + Returns: + Return type of *tuple_factory* + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + value_type = type(v) + if recurse is True: + if value_type in _ATOMIC_TYPES: + rv.append(v) + elif has(value_type): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif issubclass(value_type, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + items = [ + ( + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + ) + for j in v + ] + try: + rv.append(cf(items)) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv.append(cf(*items)) + elif issubclass(value_type, dict): + df = value_type if retain is True else dict + rv.append( + df( + ( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk + ), + ( + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv + ), + ) + for kk, vv in v.items() + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with *attrs* attributes. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + Returns: + bool: + """ + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + Args: + inst: Instance of a class with *attrs* attributes. + + changes: Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + attrs.exceptions.AttrsAttributeNotFoundError: + If *attr_name* couldn't be found on *cls*. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. deprecated:: 17.1.0 + Use `attrs.evolve` instead if you can. This function will not be + removed du to the slightly different approach compared to + `attrs.evolve`, though. + """ + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in changes.items(): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + msg = f"{k} is not an attrs attribute on {new.__class__}." + raise AttrsAttributeNotFoundError(msg) + _OBJ_SETATTR(new, k, v) + return new + + +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in :class:`Attribute`'s + *type* field. In other words, you don't need to resolve your types if you + only use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, for example, if the name only + exists inside a method, you may pass *globalns* or *localns* to specify + other dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + Args: + cls (type): Class to resolve. + + globalns (dict | None): Dictionary containing global variables. + + localns (dict | None): Dictionary containing local variables. + + attribs (list | None): + List of attribs for the given class. This is necessary when calling + from inside a ``field_transformer`` since *cls* is not an *attrs* + class yet. + + include_extras (bool): + Resolve more accurately, if possible. Pass ``include_extras`` to + ``typing.get_hints``, if supported by the typing module. On + supported Python versions (3.9+), this resolves the types more + accurately. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class and you didn't pass any attribs. + + NameError: If types cannot be resolved because of missing variables. + + Returns: + *cls* so you can use this function also as a class decorator. Please + note that you have to apply it **after** `attrs.define`. That means the + decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + kwargs = { + "globalns": globalns, + "localns": localns, + "include_extras": include_extras, + } + + hints = typing.get_type_hints(cls, **kwargs) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _OBJ_SETATTR(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls diff --git a/lib/python3.12/site-packages/attr/_make.py b/lib/python3.12/site-packages/attr/_make.py new file mode 100644 index 0000000000000000000000000000000000000000..d24d9ba98575c3c4beb3186ca0335d2175663c7f --- /dev/null +++ b/lib/python3.12/site-packages/attr/_make.py @@ -0,0 +1,3362 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import abc +import contextlib +import copy +import enum +import inspect +import itertools +import linecache +import sys +import types +import unicodedata +import weakref + +from collections.abc import Callable, Mapping +from functools import cached_property +from typing import Any, NamedTuple, TypeVar + +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters +from ._compat import ( + PY_3_10_PLUS, + PY_3_11_PLUS, + PY_3_13_PLUS, + _AnnotationExtractor, + _get_annotations, + get_generic_base, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + UnannotatedAttributeError, +) + + +# This is used at least twice, so cache it here. +_OBJ_SETATTR = object.__setattr__ +_INIT_FACTORY_PAT = "__attr_factory_%s" +_CLASSVAR_PREFIXES = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_HASH_CACHE_FIELD = "_attrs_cached_hash" + +_EMPTY_METADATA_SINGLETON = types.MappingProxyType({}) + +# Unique object for unequivocal getattr() defaults. +_SENTINEL = object() + +_DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate) + + +class _Nothing(enum.Enum): + """ + Sentinel to indicate the lack of a value when `None` is ambiguous. + + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. + """ + + NOTHING = enum.auto() + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + +NOTHING = _Nothing.NOTHING +""" +Sentinel to indicate the lack of a value when `None` is ambiguous. + +When using in 3rd party code, use `attrs.NothingType` for type annotations. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since `None` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=None, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new field / attribute on a class. + + Identical to `attrs.field`, except it's not keyword-only. + + Consider using `attrs.field` in new code (``attr.ib`` will *never* go away, + though). + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attr.s` (or similar)! + + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is `None` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 + *converter* as a replacement for the deprecated *convert* to achieve + consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* + .. versionchanged:: 25.4.0 + *kw_only* can now be None, and its default is also changed from False to + None. + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if factory is not None: + if default is not NOTHING: + msg = ( + "The `default` and `factory` arguments are mutually exclusive." + ) + raise ValueError(msg) + if not callable(factory): + msg = "The `factory` argument must be a callable." + raise ValueError(msg) + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + alias=alias, + ) + + +def _compile_and_eval( + script: str, + globs: dict[str, Any] | None, + locs: Mapping[str, object] | None = None, + filename: str = "", +) -> None: + """ + Evaluate the script with the given global (globs) and local (locs) + variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _linecache_and_compile( + script: str, + filename: str, + globs: dict[str, Any] | None, + locals: Mapping[str, object] | None = None, +) -> dict[str, Any]: + """ + Cache the script with _linecache_, compile it and return the _locals_. + """ + + locs = {} if locals is None else locals + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + + filename = f"{base_filename[:-1]}-{count}>" + count += 1 + + _compile_and_eval(script, globs, locs, filename) + + return locs + + +def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type: + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = f"{cls_name}Attributes" + body = {} + for i, attr_name in enumerate(attr_names): + + def getter(self, i=i): + return self[i] + + body[attr_name] = property(getter) + return type(attr_class_name, (tuple,), body) + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +class _Attributes(NamedTuple): + attrs: type + base_attrs: list[Attribute] + base_attrs_map: dict[str, type] + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_CLASSVAR_PREFIXES) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + """ + return attrib_name in cls.__dict__ + + +def _collect_base_attrs( + cls, taken_attr_names +) -> tuple[list[Attribute], dict[str, type]]: + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, +) -> _Attributes: + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + If *collect_by_mro* is True, collect them in the correct MRO order, + otherwise use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = list(these.items()) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if a.__class__ is not _CountingAttr: + a = attrib(a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if unannotated: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + ), + key=lambda e: e[1].counter, + ) + + fca = Attribute.from_counting_attr + no = ClassProps.KeywordOnly.NO + own_attrs = [ + fca( + attr_name, + ca, + kw_only is not no, + anns.get(attr_name), + ) + for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + if kw_only is ClassProps.KeywordOnly.FORCE: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = base_attrs + own_attrs + + if field_transformer is not None: + attrs = tuple(field_transformer(cls, attrs)) + + # Check attr order after executing the field_transformer. + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" + raise ValueError(msg) + + if had_default is False and a.default is not NOTHING: + had_default = True + + # Resolve default field alias after executing field_transformer. + # This allows field_transformer to differentiate between explicit vs + # default aliases and supply their own defaults. + for a in attrs: + if not a.alias: + # Evolve is very slow, so we hold our nose and do it dirty. + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map) + + +def _make_cached_property_getattr(cached_properties, original_getattr, cls): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper(_cls):", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " try:", + " return super().__getattribute__(item)", + " except AttributeError:", + " if not hasattr(super(), '__getattr__'):", + " raise", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) + + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper(_cls)", + ] + ) + + unique_filename = _generate_unique_filename(cls, "getattr") + + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _OBJ_SETATTR.__get__, + "original_getattr": original_getattr, + } + + return _linecache_and_compile( + "\n".join(lines), unique_filename, glob, locals={"_cls": cls} + )["__getattr__"] + + +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + "__suppress_context__", + "__notes__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + if isinstance(self, BaseException) and name in ("__notes__",): + BaseException.__delattr__(self, name) + return + + raise FrozenInstanceError + + +def evolve(*args, **changes): + """ + Create a new instance, based on the first positional argument with + *changes* applied. + + .. tip:: + + On Python 3.13 and later, you can also use `copy.replace` instead. + + Args: + + inst: + Instance of a class with *attrs* attributes. *inst* must be passed + as a positional argument. + + changes: + Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + TypeError: + If *attr_name* couldn't be found in the class ``__init__``. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. + .. versionchanged:: 24.1.0 + *inst* can't be passed as a keyword argument anymore. + """ + try: + (inst,) = args + except ValueError: + msg = ( + f"evolve() takes 1 positional argument, but {len(args)} were given" + ) + raise TypeError(msg) from None + + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = a.alias + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +class _ClassBuilder: + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_add_method_dunders", + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_custom_setattr", + "_has_post_init", + "_has_pre_init", + "_is_exc", + "_on_setattr", + "_pre_init_has_args", + "_repr_added", + "_script_snippets", + "_slots", + "_weakref_slot", + "_wrote_own_setattr", + ) + + def __init__( + self, + cls: type, + these, + auto_attribs: bool, + props: ClassProps, + has_custom_setattr: bool, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + props.kw_only, + props.collected_fields_by_mro, + props.field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if props.is_slotted else {} + self._attrs = attrs + self._base_names = {a.name for a in base_attrs} + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = props.is_slotted + self._frozen = props.is_frozen + self._weakref_slot = props.has_weakref_slot + self._cache_hash = ( + props.hashability is ClassProps.Hashability.HASHABLE_CACHED + ) + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._pre_init_has_args = False + if self._has_pre_init: + # Check if the pre init method has more arguments than just `self` + # We want to pass arguments if pre init expects arguments + pre_init_func = cls.__attrs_pre_init__ + pre_init_signature = inspect.signature(pre_init_func) + self._pre_init_has_args = len(pre_init_signature.parameters) > 1 + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = props.is_exception + self._on_setattr = props.on_setattr_hook + + self._has_custom_setattr = has_custom_setattr + self._wrote_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + self._cls_dict["__attrs_props__"] = props + + if props.is_frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._wrote_own_setattr = True + elif self._on_setattr in ( + _DEFAULT_ON_SETATTR, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False + for a in attrs: + if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: + break + if ( + ( + self._on_setattr == _DEFAULT_ON_SETATTR + and not (has_validator or has_converter) + ) + or (self._on_setattr == setters.validate and not has_validator) + or (self._on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. + self._on_setattr = None + + if props.added_pickling: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + # tuples of script, globs, hook + self._script_snippets: list[ + tuple[str, dict, Callable[[dict, dict], Any]] + ] = [] + self._repr_added = False + + # We want to only do this check once; in 99.9% of cases these + # exist. + if not hasattr(self._cls, "__module__") or not hasattr( + self._cls, "__qualname__" + ): + self._add_method_dunders = self._add_method_dunders_safe + else: + self._add_method_dunders = self._add_method_dunders_unsafe + + def __repr__(self): + return f"<_ClassBuilder(cls={self._cls.__name__})>" + + def _eval_snippets(self) -> None: + """ + Evaluate any registered snippets in one go. + """ + script = "\n".join([snippet[0] for snippet in self._script_snippets]) + globs = {} + for _, snippet_globs, _ in self._script_snippets: + globs.update(snippet_globs) + + locs = _linecache_and_compile( + script, + _generate_unique_filename(self._cls, "methods"), + globs, + ) + + for _, _, hook in self._script_snippets: + hook(self._cls_dict, locs) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + self._eval_snippets() + if self._slots is True: + cls = self._create_slots_class() + self._cls.__attrs_base_of_slotted__ = weakref.ref(cls) + else: + cls = self._patch_original_class() + if PY_3_10_PLUS: + cls = abc.update_abstractmethods(cls) + + # The method gets only called if it's not inherited from a base class. + # _has_own_attribute does NOT work properly for classmethods. + if ( + getattr(cls, "__attrs_init_subclass__", None) + and "__attrs_init_subclass__" not in cls.__dict__ + ): + cls.__attrs_init_subclass__() + + return cls + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _SENTINEL) is not _SENTINEL + ): + # An AttributeError can happen if a base class defines a + # class variable and we want to set an attribute with the + # same name by using only a type annotation. + with contextlib.suppress(AttributeError): + delattr(cls, name) + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._wrote_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = _OBJ_SETATTR + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in self._cls_dict.items() + if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") + } + + # 3.14.0rc2+ + if hasattr(sys, "_clear_type_descriptors"): + sys._clear_type_descriptors(self._cls) + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._wrote_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = _OBJ_SETATTR + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = {} + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + cached_properties = { + name: cached_prop.func + for name, cached_prop in cd.items() + if isinstance(cached_prop, cached_property) + } + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + # Add cached properties to names for slotting. + names += (name,) + # Clear out function from class to avoid clashing. + del cd[name] + additional_closure_functions_to_update.append(func) + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overridden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in existing_slots.items() + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_HASH_CACHE_FIELD) + + cd["__slots__"] = tuple(slot_names) + + cd["__qualname__"] = self._cls.__qualname__ + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # . + # If a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # noqa: PERF203 + # ValueError: Cell is empty + pass + else: + if match: + cell.cell_contents = cls + return cls + + def add_repr(self, ns): + script, globs = _make_repr_script(self._attrs, ns) + + def _attach_repr(cls_dict, globs): + cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"]) + + self._script_snippets.append((script, globs, _attach_repr)) + self._repr_added = True + return self + + def add_str(self): + if not self._repr_added: + msg = "__str__ can only be generated if a __repr__ exists." + raise ValueError(msg) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return {name: getattr(self, name) for name in state_attr_names} + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _OBJ_SETATTR.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_HASH_CACHE_FIELD, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + script, globs = _make_hash_script( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + + def attach_hash(cls_dict: dict, locs: dict) -> None: + cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"]) + + self._script_snippets.append((script, globs, attach_hash)) + + return self + + def add_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=False, + ) + + def _attach_init(cls_dict, globs): + init = globs["__init__"] + init.__annotations__ = annotations + cls_dict["__init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_init)) + + return self + + def add_replace(self): + self._cls_dict["__replace__"] = self._add_method_dunders( + lambda self, **changes: evolve(self, **changes) + ) + return self + + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + + def add_attrs_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=True, + ) + + def _attach_attrs_init(cls_dict, globs): + init = globs["__attrs_init__"] + init.__annotations__ = annotations + cls_dict["__attrs_init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_attrs_init)) + + return self + + def add_eq(self): + cd = self._cls_dict + + script, globs = _make_eq_script(self._attrs) + + def _attach_eq(cls_dict, globs): + cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"]) + + self._script_snippets.append((script, globs, _attach_eq)) + + cd["__ne__"] = __ne__ + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + msg = "Can't combine custom __setattr__ with on_setattr hooks." + raise ValueError(msg) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _OBJ_SETATTR(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._wrote_own_setattr = True + + return self + + def _add_method_dunders_unsafe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method*. + """ + method.__module__ = self._cls.__module__ + + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + method.__doc__ = ( + f"Method generated by attrs for class {self._cls.__qualname__}." + ) + + return method + + def _add_method_dunders_safe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + with contextlib.suppress(AttributeError): + method.__module__ = self._cls.__module__ + + with contextlib.suppress(AttributeError): + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + with contextlib.suppress(AttributeError): + method.__doc__ = f"Method generated by attrs for class {self._cls.__qualname__}." + + return method + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + unsafe_hash=None, + force_kw_only=True, +): + r""" + A class decorator that adds :term:`dunder methods` according to the + specified attributes using `attr.ib` or the *these* argument. + + Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will + *never* go away, though). + + Args: + repr_ns (str): + When using nested classes, there was no way in Python 2 to + automatically detect that. This argument allows to set a custom + name for a more meaningful ``repr`` output. This argument is + pointless in Python 3 and is therefore deprecated. + + .. caution:: + Refer to `attrs.define` for the rest of the parameters, but note that they + can have different defaults. + + Notably, leaving *on_setattr* as `None` will **not** add any hooks. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports `None` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. deprecated:: 24.1.0 *repr_ns* + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionchanged:: 25.4.0 + *kw_only* now only applies to attributes defined in the current class, + and respects attribute-level ``kw_only=False`` settings. + .. versionadded:: 25.4.0 *force_kw_only* + """ + if repr_ns is not None: + import warnings + + warnings.warn( + DeprecationWarning( + "The `repr_ns` argument is deprecated and will be removed in or after August 2025." + ), + stacklevel=2, + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + nonlocal hash + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + msg = "Can't freeze a class with a custom __setattr__." + raise ValueError(msg) + + eq = not is_exc and _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + + Hashability = ClassProps.Hashability + + if is_exc: + hashability = Hashability.LEAVE_ALONE + elif hash is True: + hashability = ( + Hashability.HASHABLE_CACHED + if cache_hash + else Hashability.HASHABLE + ) + elif hash is False: + hashability = Hashability.LEAVE_ALONE + elif hash is None: + if auto_detect is True and _has_own_attribute(cls, "__hash__"): + hashability = Hashability.LEAVE_ALONE + elif eq is True and is_frozen is True: + hashability = ( + Hashability.HASHABLE_CACHED + if cache_hash + else Hashability.HASHABLE + ) + elif eq is False: + hashability = Hashability.LEAVE_ALONE + else: + hashability = Hashability.UNHASHABLE + else: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + KeywordOnly = ClassProps.KeywordOnly + if kw_only: + kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES + else: + kwo = KeywordOnly.NO + + props = ClassProps( + is_exception=is_exc, + is_frozen=is_frozen, + is_slotted=slots, + collected_fields_by_mro=collect_by_mro, + added_init=_determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ), + added_repr=_determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ), + added_eq=eq, + added_ordering=not is_exc + and _determine_whether_to_implement( + cls, + order_, + auto_detect, + ("__lt__", "__le__", "__gt__", "__ge__"), + ), + hashability=hashability, + added_match_args=match_args, + kw_only=kwo, + has_weakref_slot=weakref_slot, + added_str=str, + added_pickling=_determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + on_setattr_hook=on_setattr, + field_transformer=field_transformer, + ) + + if not props.is_hashable and cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) + + builder = _ClassBuilder( + cls, + these, + auto_attribs=auto_attribs, + props=props, + has_custom_setattr=has_own_setattr, + ) + + if props.added_repr: + builder.add_repr(repr_ns) + + if props.added_str: + builder.add_str() + + if props.added_eq: + builder.add_eq() + if props.added_ordering: + builder.add_order() + + if not frozen: + builder.add_setattr() + + if props.is_hashable: + builder.add_hash() + elif props.hashability is Hashability.UNHASHABLE: + builder.make_unhashable() + + if props.added_init: + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, init must be True." + raise TypeError(msg) + + if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): + builder.add_replace() + + if ( + PY_3_10_PLUS + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): + builder.add_match_args() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ is _frozen_setattrs + + +def _generate_unique_filename(cls: type, func_name: str) -> str: + """ + Create a "filename" suitable for a function being generated. + """ + return ( + f"" + ) + + +def _make_hash_script( + cls: type, attrs: list[Attribute], frozen: bool, cache_hash: bool +) -> tuple[str, dict]: + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + type_hash = hash(_generate_unique_filename(cls, "hash")) + # If eq is custom generated, we need to include the functions in globs + globs = {} + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + hash_def += ", *" + + hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + f" {type_hash},", + ] + ) + + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + globs[cmp_name] = a.eq_key + method_lines.append( + indent + f" {cmp_name}(self.{a.name})," + ) + else: + method_lines.append(indent + f" self.{a.name},") + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + f"if self.{_HASH_CACHE_FIELD} is None:") + if frozen: + append_hash_computation_lines( + f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ", tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + f"self.{_HASH_CACHE_FIELD} = ", tab * 2 + ) + method_lines.append(tab + f"return self.{_HASH_CACHE_FIELD}") + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return script, globs + + +def _add_hash(cls: type, attrs: list[Attribute]): + """ + Add a hash method to *cls*. + """ + script, globs = _make_hash_script( + cls, attrs, frozen=False, cache_hash=False + ) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__hash__") + ) + cls.__hash__ = globs["__hash__"] + return cls + + +def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + +def _make_eq_script(attrs: list) -> tuple[str, dict]: + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + globs = {} + if attrs: + lines.append(" return (") + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + f" {cmp_name}(self.{a.name}) == {cmp_name}(other.{a.name})" + ) + else: + lines.append(f" self.{a.name} == other.{a.name}") + if a is not attrs[-1]: + lines[-1] = f"{lines[-1]} and" + lines.append(" )") + else: + lines.append(" return True") + + script = "\n".join(lines) + + return script, globs + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_eq_script(attrs) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__eq__") + ) + cls.__eq__ = globs["__eq__"] + cls.__ne__ = __ne__ + + return cls + + +def _make_repr_script(attrs, ns) -> tuple[str, dict]: + """ + Create the source and globs for a __repr__ and return it. + """ + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) + + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] + + return "\n".join(lines), globs + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_repr_script(attrs, ns) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__repr__") + ) + cls.__repr__ = globs["__repr__"] + return cls + + +def fields(cls): + """ + Return the tuple of *attrs* attributes for a class. + + The tuple also allows accessing the fields by their names (see below for + examples). + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + tuple (with name accessors) of `attrs.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. + """ + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + + attrs = getattr(cls, "__attrs_attrs__", None) + + if attrs is None: + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of *attrs* attributes for a class, whose keys + are the attribute names. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + dict[str, attrs.Attribute]: Dict of attribute name to definition + + .. versionadded:: 18.1.0 + """ + if not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return {a.name: a for a in attrs} + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + Args: + inst: Instance of a class with *attrs* attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + cls = base_attr_map.get(a_name) + return cls and "__slots__" in cls.__dict__ + + +def _make_init_script( + cls, + attrs, + pre_init, + pre_init_has_args, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + cls_on_setattr, + attrs_init, +) -> tuple[str, dict, dict]: + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = True + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: + needs_cached_setattr = True + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + pre_init_has_args, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + "__attrs_init__" if attrs_init else "__init__", + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__ + + return script, globs, annotations + + +def _setattr(attr_name: str, value_var: str, has_on_setattr: bool) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return f"_setattr('{attr_name}', {value_var})" + + +def _setattr_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return f"_setattr('{attr_name}', {converter._fmt_converter_call(attr_name, value_var)})" + + +def _assign(attr_name: str, value: str, has_on_setattr: bool) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return f"self.{attr_name} = {value}" + + +def _assign_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True, converter) + + return f"self.{attr_name} = {converter._fmt_converter_call(attr_name, value_var)}" + + +def _determine_setters( + frozen: bool, slots: bool, base_attr_map: dict[str, type] +): + """ + Determine the correct setter functions based on whether a class is frozen + and/or slotted. + """ + if frozen is True: + if slots is True: + return (), _setattr, _setattr_with_converter + + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + + def fmt_setter( + attr_name: str, value_var: str, has_on_setattr: bool + ) -> str: + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return f"_inst_dict['{attr_name}'] = {value_var}" + + def fmt_setter_with_converter( + attr_name: str, + value_var: str, + has_on_setattr: bool, + converter: Converter, + ) -> str: + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr, converter + ) + + return f"_inst_dict['{attr_name}'] = {converter._fmt_converter_call(attr_name, value_var)}" + + return ( + ("_inst_dict = self.__dict__",), + fmt_setter, + fmt_setter_with_converter, + ) + + # Not frozen -- we can just assign directly. + return (), _assign, _assign_with_converter + + +def _attrs_to_init_script( + attrs: list[Attribute], + is_frozen: bool, + is_slotted: bool, + call_pre_init: bool, + pre_init_has_args: bool, + call_post_init: bool, + does_cache_hash: bool, + base_attr_map: dict[str, type], + is_exc: bool, + needs_cached_setattr: bool, + has_cls_on_setattr: bool, + method_name: str, +) -> tuple[str, dict, dict]: + """ + Return a script of an initializer for *attrs*, a dict of globals, and + annotations for the initializer. + + The globals are required by the generated script. + """ + lines = ["self.__attrs_pre_init__()"] if call_pre_init else [] + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. Note _setattr will be used again below if + # does_cache_hash is True. + "_setattr = _cached_setattr_get(self)" + ) + + extra_lines, fmt_setter, fmt_setter_with_converter = _determine_setters( + is_frozen, is_slotted, base_attr_map + ) + lines.extend(extra_lines) + + args = [] # Parameters in the definition of __init__ + pre_init_args = [] # Parameters in the call to __attrs_pre_init__ + kw_only_args = [] # Used for both 'args' and 'pre_init_args' above + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_cls_on_setattr + ) + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias + + has_factory = isinstance(a.default, Factory) + maybe_self = "self" if has_factory and a.default.takes_self else "" + + if a.converter is not None and not isinstance(a.converter, Converter): + converter = Converter(a.converter) + else: + converter = a.converter + + if a.init is False: + if has_factory: + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + elif converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = f"{arg_name}=attr_dict['{attr_name}'].default" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + pre_init_args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = f"{arg_name}=NOTHING" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + pre_init_args.append(arg_name) + lines.append(f"if {arg_name} is not NOTHING:") + + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + pre_init_args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and converter is None: + annotations[arg_name] = a.type + elif converter is not None and converter._first_param_type: + # Use the type from the converter if present. + annotations[arg_name] = converter._first_param_type + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if call_post_init: + lines.append("self.__attrs_post_init__()") + + # Because this is set only after __attrs_post_init__ is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to field + # values during post-init combined with post-init accessing the hash code + # would result in silent bugs. + if does_cache_hash: + if is_frozen: + if is_slotted: + init_hash_cache = f"_setattr('{_HASH_CACHE_FIELD}', None)" + else: + init_hash_cache = f"_inst_dict['{_HASH_CACHE_FIELD}'] = None" + else: + init_hash_cache = f"self.{_HASH_CACHE_FIELD} = None" + lines.append(init_hash_cache) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) + + lines.append(f"BaseException.__init__(self, {vals})") + + args = ", ".join(args) + pre_init_args = ", ".join(pre_init_args) + if kw_only_args: + # leading comma & kw_only args + args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" + pre_init_kw_only_args = ", ".join( + [ + f"{kw_arg_name}={kw_arg_name}" + # We need to remove the defaults from the kw_only_args. + for kw_arg_name in (kwa.split("=")[0] for kwa in kw_only_args) + ] + ) + pre_init_args += ", " if pre_init_args else "" + pre_init_args += pre_init_kw_only_args + + if call_pre_init and pre_init_has_args: + # If pre init method has arguments, pass the values given to __init__. + lines[0] = f"self.__attrs_pre_init__({pre_init_args})" + + # Python <3.12 doesn't allow backslashes in f-strings. + NL = "\n " + return ( + f"""def {method_name}(self, {args}): + {NL.join(lines) if lines else "pass"} +""", + names_for_globals, + annotations, + ) + + +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + +class Attribute: + """ + *Read-only* representation of an attribute. + + .. warning:: + + You should never instantiate this class yourself. + + The class has *all* arguments of `attr.ib` (except for ``factory`` which is + only syntactic sugar for ``default=Factory(...)`` plus the following: + + - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The + callables that are used for comparing and ordering objects by this + attribute, respectively. These are set by passing a callable to + `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also + :ref:`comparison customization `. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The :ref:`field transformer ` hook receives a list of + them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* + + For the full version history of the fields, see `attr.ib`. + """ + + # These slots must NOT be reordered because we use them later for + # instantiation. + __slots__ = ( # noqa: RUF023 + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + "alias", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + alias=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _OBJ_SETATTR.__get__(self) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + types.MappingProxyType(dict(metadata)) # Shallow copy + if metadata + else _EMPTY_METADATA_SINGLETON + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) + + def __setattr__(self, name, value): + raise FrozenInstanceError + + @classmethod + def from_counting_attr( + cls, name: str, ca: _CountingAttr, kw_only: bool, type=None + ): + # The 'kw_only' argument is the class-level setting, and is used if the + # attribute itself does not explicitly set 'kw_only'. + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + msg = f"Type annotation and type argument cannot both be present for '{name}'." + raise ValueError(msg) + return cls( + name, + ca._default, + ca._validator, + ca.repr, + None, + ca.hash, + ca.init, + False, + ca.metadata, + type, + ca.converter, + kw_only if ca.kw_only is None else ca.kw_only, + ca.eq, + ca.eq_key, + ca.order, + ca.order_key, + ca.on_setattr, + ca.alias, + ) + + # Don't use attrs.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attrs.evolve` but that function does not work + with :class:`attrs.Attribute`. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _OBJ_SETATTR.__get__(self) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + ( + types.MappingProxyType(dict(value)) + if value + else _EMPTY_METADATA_SINGLETON + ), + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr: + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "_default", + "_validator", + "alias", + "converter", + "counter", + "eq", + "eq_key", + "hash", + "init", + "kw_only", + "metadata", + "on_setattr", + "order", + "order_key", + "repr", + "type", + ) + __attrs_attrs__ = ( + *tuple( + Attribute( + name=name, + alias=_default_init_alias_for(name), + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + "alias", + ) + ), + Attribute( + name="metadata", + alias="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + alias, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + self.alias = alias + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + Raises: + DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class ClassProps: + """ + Effective class properties as derived from parameters to `attr.s()` or + `define()` decorators. + + This is the same data structure that *attrs* uses internally to decide how + to construct the final class. + + Warning: + + This feature is currently **experimental** and is not covered by our + strict backwards-compatibility guarantees. + + + Attributes: + is_exception (bool): + Whether the class is treated as an exception class. + + is_slotted (bool): + Whether the class is `slotted `. + + has_weakref_slot (bool): + Whether the class has a slot for weak references. + + is_frozen (bool): + Whether the class is frozen. + + kw_only (KeywordOnly): + Whether / how the class enforces keyword-only arguments on the + ``__init__`` method. + + collected_fields_by_mro (bool): + Whether the class fields were collected by method resolution order. + That is, correctly but unlike `dataclasses`. + + added_init (bool): + Whether the class has an *attrs*-generated ``__init__`` method. + + added_repr (bool): + Whether the class has an *attrs*-generated ``__repr__`` method. + + added_eq (bool): + Whether the class has *attrs*-generated equality methods. + + added_ordering (bool): + Whether the class has *attrs*-generated ordering methods. + + hashability (Hashability): How `hashable ` the class is. + + added_match_args (bool): + Whether the class supports positional `match ` over its + fields. + + added_str (bool): + Whether the class has an *attrs*-generated ``__str__`` method. + + added_pickling (bool): + Whether the class has *attrs*-generated ``__getstate__`` and + ``__setstate__`` methods for `pickle`. + + on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None): + The class's ``__setattr__`` hook. + + field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None): + The class's `field transformers `. + + .. versionadded:: 25.4.0 + """ + + class Hashability(enum.Enum): + """ + The hashability of a class. + + .. versionadded:: 25.4.0 + """ + + HASHABLE = "hashable" + """Write a ``__hash__``.""" + HASHABLE_CACHED = "hashable_cache" + """Write a ``__hash__`` and cache the hash.""" + UNHASHABLE = "unhashable" + """Set ``__hash__`` to ``None``.""" + LEAVE_ALONE = "leave_alone" + """Don't touch ``__hash__``.""" + + class KeywordOnly(enum.Enum): + """ + How attributes should be treated regarding keyword-only parameters. + + .. versionadded:: 25.4.0 + """ + + NO = "no" + """Attributes are not keyword-only.""" + YES = "yes" + """Attributes in current class without kw_only=False are keyword-only.""" + FORCE = "force" + """All attributes are keyword-only.""" + + __slots__ = ( # noqa: RUF023 -- order matters for __init__ + "is_exception", + "is_slotted", + "has_weakref_slot", + "is_frozen", + "kw_only", + "collected_fields_by_mro", + "added_init", + "added_repr", + "added_eq", + "added_ordering", + "hashability", + "added_match_args", + "added_str", + "added_pickling", + "on_setattr_hook", + "field_transformer", + ) + + def __init__( + self, + is_exception, + is_slotted, + has_weakref_slot, + is_frozen, + kw_only, + collected_fields_by_mro, + added_init, + added_repr, + added_eq, + added_ordering, + hashability, + added_match_args, + added_str, + added_pickling, + on_setattr_hook, + field_transformer, + ): + self.is_exception = is_exception + self.is_slotted = is_slotted + self.has_weakref_slot = has_weakref_slot + self.is_frozen = is_frozen + self.kw_only = kw_only + self.collected_fields_by_mro = collected_fields_by_mro + self.added_init = added_init + self.added_repr = added_repr + self.added_eq = added_eq + self.added_ordering = added_ordering + self.hashability = hashability + self.added_match_args = added_match_args + self.added_str = added_str + self.added_pickling = added_pickling + self.on_setattr_hook = on_setattr_hook + self.field_transformer = field_transformer + + @property + def is_hashable(self): + return ( + self.hashability is ClassProps.Hashability.HASHABLE + or self.hashability is ClassProps.Hashability.HASHABLE_CACHED + ) + + +_cas = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in ClassProps.__slots__ +] + +ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas) + + +class Factory: + """ + Stores a factory callable. + + If passed as the default value to `attrs.field`, the factory is used to + generate a new value. + + Args: + factory (typing.Callable): + A callable that takes either none or exactly one mandatory + positional argument depending on *takes_self*. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +class Converter: + """ + Stores a converter callable. + + Allows for the wrapped converter to take additional arguments. The + arguments are passed in the order they are documented. + + Args: + converter (Callable): A callable that converts the passed value. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. (default: `False`) + + takes_field (bool): + Pass the field definition (an :class:`Attribute`) into the + converter as a positional argument. (default: `False`) + + .. versionadded:: 24.1.0 + """ + + __slots__ = ( + "__call__", + "_first_param_type", + "_global_name", + "converter", + "takes_field", + "takes_self", + ) + + def __init__(self, converter, *, takes_self=False, takes_field=False): + self.converter = converter + self.takes_self = takes_self + self.takes_field = takes_field + + ex = _AnnotationExtractor(converter) + self._first_param_type = ex.get_first_param_type() + + if not (self.takes_self or self.takes_field): + self.__call__ = lambda value, _, __: self.converter(value) + elif self.takes_self and not self.takes_field: + self.__call__ = lambda value, instance, __: self.converter( + value, instance + ) + elif not self.takes_self and self.takes_field: + self.__call__ = lambda value, __, field: self.converter( + value, field + ) + else: + self.__call__ = lambda value, instance, field: self.converter( + value, instance, field + ) + + rt = ex.get_return_type() + if rt is not None: + self.__call__.__annotations__["return"] = rt + + @staticmethod + def _get_global_name(attr_name: str) -> str: + """ + Return the name that a converter for an attribute name *attr_name* + would have. + """ + return f"__attr_converter_{attr_name}" + + def _fmt_converter_call(self, attr_name: str, value_var: str) -> str: + """ + Return a string that calls the converter for an attribute name + *attr_name* and the value in variable named *value_var* according to + `self.takes_self` and `self.takes_field`. + """ + if not (self.takes_self or self.takes_field): + return f"{self._get_global_name(attr_name)}({value_var})" + + if self.takes_self and self.takes_field: + return f"{self._get_global_name(attr_name)}({value_var}, self, attr_dict['{attr_name}'])" + + if self.takes_self: + return f"{self._get_global_name(attr_name)}({value_var}, self)" + + return f"{self._get_global_name(attr_name)}({value_var}, attr_dict['{attr_name}'])" + + def __getstate__(self): + """ + Return a dict containing only converter and takes_self -- the rest gets + computed when loading. + """ + return { + "converter": self.converter, + "takes_self": self.takes_self, + "takes_field": self.takes_field, + } + + def __setstate__(self, state): + """ + Load instance from state. + """ + self.__init__(**state) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in ("converter", "takes_self", "takes_field") +] + +Converter = _add_hash( + _add_eq(_add_repr(Converter, attrs=_f), attrs=_f), attrs=_f +) + + +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): + r""" + A quick way to create a new class called *name* with *attrs*. + + .. note:: + + ``make_class()`` is a thin wrapper around `attr.s`, not `attrs.define` + which means that it doesn't come with some of the improved defaults. + + For example, if you want the same ``on_setattr`` behavior as in + `attrs.define`, you have to pass the hooks yourself: ``make_class(..., + on_setattr=setters.pipe(setters.convert, setters.validate)`` + + .. warning:: + + It is *your* duty to ensure that the class name and the attribute names + are valid identifiers. ``make_class()`` will *not* validate them for + you. + + Args: + name (str): The name for the new class. + + attrs (list | dict): + A list of names or a dictionary of mappings of names to `attr.ib`\ + s / `attrs.field`\ s. + + The order is deduced from the order of the names or attributes + inside *attrs*. Otherwise the order of the definition of the + attributes is used. + + bases (tuple[type, ...]): Classes that the new class will subclass. + + class_body (dict): + An optional dictionary of class attributes for the new class. + + attributes_arguments: Passed unmodified to `attr.s`. + + Returns: + type: A new class with *attrs*. + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* + .. versionchanged:: 25.2.0 Class names can now be unicode. + """ + # Class identifiers are converted into the normal form NFKC while parsing + name = unicodedata.normalize("NFKC", name) + + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = {a: attrib() for a in attrs} + else: + msg = "attrs argument must be a dict or a list." + raise TypeError(msg) + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if class_body is not None: + body.update(class_body) + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = types.new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + with contextlib.suppress(AttributeError, ValueError): + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + cls = _attrs(these=cls_dict, **attributes_arguments)(type_) + # Only add type annotations now or "_attrs()" will complain: + cls.__annotations__ = { + k: v.type for k, v in cls_dict.items() if v.type is not None + } + return cls + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, unsafe_hash=True) +class _AndValidator: + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if they + have any. + + converters (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + return_instance = any(isinstance(c, Converter) for c in converters) + + if return_instance: + + def pipe_converter(val, inst, field): + for c in converters: + val = ( + c(val, inst, field) if isinstance(c, Converter) else c(val) + ) + + return val + + else: + + def pipe_converter(val): + for c in converters: + val = c(val) + + return val + + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = TypeVar("A") + pipe_converter.__annotations__.update({"val": A, "return": A}) + else: + # Get parameter type from first converter. + t = _AnnotationExtractor(converters[0]).get_first_param_type() + if t: + pipe_converter.__annotations__["val"] = t + + last = converters[-1] + if not PY_3_11_PLUS and isinstance(last, Converter): + last = last.__call__ + + # Get return type from last converter. + rt = _AnnotationExtractor(last).get_return_type() + if rt: + pipe_converter.__annotations__["return"] = rt + + if return_instance: + return Converter(pipe_converter, takes_self=True, takes_field=True) + return pipe_converter diff --git a/lib/python3.12/site-packages/attr/_next_gen.py b/lib/python3.12/site-packages/attr/_next_gen.py new file mode 100644 index 0000000000000000000000000000000000000000..4ccd0da2446dc126ce936b054581a527e247cabc --- /dev/null +++ b/lib/python3.12/site-packages/attr/_next_gen.py @@ -0,0 +1,674 @@ +# SPDX-License-Identifier: MIT + +""" +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. +""" + +from functools import partial + +from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple +from ._make import ( + _DEFAULT_ON_SETATTR, + NOTHING, + _frozen_setattrs, + attrib, + attrs, +) +from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + unsafe_hash=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + force_kw_only=False, +): + r""" + A class decorator that adds :term:`dunder methods` according to + :term:`fields ` specified using :doc:`type annotations `, + `field()` calls, or the *these* argument. + + Since *attrs* patches or replaces an existing class, you cannot use + `object.__init_subclass__` with *attrs* classes, because it runs too early. + As a replacement, you can define ``__attrs_init_subclass__`` on your class. + It will be called by *attrs* classes that subclass it after they're + created. See also :ref:`init-subclass`. + + Args: + slots (bool): + Create a :term:`slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, + so we encourage you to read the :term:`glossary entry `. + + auto_detect (bool): + Instead of setting the *init*, *repr*, *eq*, and *hash* arguments + explicitly, assume they are set to True **unless any** of the + involved methods for one of the arguments is implemented in the + *current* class (meaning, it is *not* inherited from some base + class). + + So, for example by implementing ``__eq__`` on a class yourself, + *attrs* will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a + sensible ``__ne__`` by default, so it *should* be enough to only + implement ``__eq__`` in most cases). + + Passing :data:`True` or :data:`False` to *init*, *repr*, *eq*, or *hash* + overrides whatever *auto_detect* would determine. + + auto_exc (bool): + If the class subclasses `BaseException` (which implicitly includes + any subclass of any exception), the following happens to behave + like a well-behaved Python exception class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids [#]_ , + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the + ``args`` attribute, + - the value of *str* is ignored leaving ``__str__`` to base + classes. + + .. [#] + Note that *attrs* will *not* remove existing implementations of + ``__hash__`` or the equality methods. It just won't add own + ones. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + A callable that is run whenever the user attempts to set an + attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same + arguments as validators: the instance, the attribute that is being + modified, and the new value. + + If no exception is raised, the attribute is set to the return value + of the callable. + + If a list of callables is passed, they're automatically wrapped in + an `attrs.setters.pipe`. + + If left None, the default behavior is to run converters and + validators whenever an attribute is set. + + init (bool): + Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name, + unless an alias is set on the attribute. + + .. seealso:: + `init` shows advanced ways to customize the generated + ``__init__`` method, including executing code before and after. + + repr(bool): + Create a ``__repr__`` method with a human readable representation + of *attrs* attributes. + + str (bool): + Create a ``__str__`` method that is identical to ``__repr__``. This + is usually not necessary except for `Exception`\ s. + + eq (bool | None): + If True or None (default), add ``__eq__`` and ``__ne__`` methods + that check two instances for equality. + + .. seealso:: + `comparison` describes how to customize the comparison behavior + going as far comparing NumPy arrays. + + order (bool | None): + If True, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` + methods that behave like *eq* above and allow instances to be + ordered. + + They compare the instances as if they were tuples of their *attrs* + attributes if and only if the types of both classes are + *identical*. + + If `None` mirror value of *eq*. + + .. seealso:: `comparison` + + unsafe_hash (bool | None): + If None (default), the ``__hash__`` method is generated according + how *eq* and *frozen* are set. + + 1. If *both* are True, *attrs* will generate a ``__hash__`` for + you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set + to None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning + the ``__hash__`` method of the base class will be used. If the + base class is `object`, this means it will fall back to id-based + hashing. + + Although not recommended, you can decide for yourself and force + *attrs* to create one (for example, if the class is immutable even + though you didn't freeze it programmatically) by passing True or + not. Both of these cases are rather special and should be used + carefully. + + .. seealso:: + + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ behavior + `_ for more + details. + + hash (bool | None): + Deprecated alias for *unsafe_hash*. *unsafe_hash* takes precedence. + + cache_hash (bool): + Ensure that the object's hash code is computed only once and stored + on the object. If this is set to True, hashing must be either + explicitly or implicitly enabled for this class. If the hash code + is cached, avoid any reassignments of fields involved in hash code + computation or mutations of the objects those fields point to after + object creation. If such changes occur, the behavior of the + object's hash code is undefined. + + frozen (bool): + Make instances immutable after initialization. If someone attempts + to modify a frozen instance, `attrs.exceptions.FrozenInstanceError` + is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` + method on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other + words: ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You + can circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + kw_only (bool): + Make attributes keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). Attributes that + explicitly set ``kw_only=False`` are not affected; base class + attributes are also not affected. + + Also see *force_kw_only*. + + weakref_slot (bool): + Make instances weak-referenceable. This has no effect unless + *slots* is True. + + field_transformer (~typing.Callable | None): + A function that is called with the original class object and all + fields right before *attrs* finalizes the class. You can use this, + for example, to automatically add converters or validators to + fields based on their types. + + .. seealso:: `transform-fields` + + match_args (bool): + If True (default), set ``__match_args__`` on the class to support + :pep:`634` (*Structural Pattern Matching*). It is a tuple of all + non-keyword-only ``__init__`` parameter names on Python 3.10 and + later. Ignored on older Python versions. + + collect_by_mro (bool): + If True, *attrs* collects attributes from base classes correctly + according to the `method resolution order + `_. If False, *attrs* + will mimic the (wrong) behavior of `dataclasses` and :pep:`681`. + + See also `issue #428 + `_. + + force_kw_only (bool): + A back-compat flag for restoring pre-25.4.0 behavior. If True and + ``kw_only=True``, all attributes are made keyword-only, including + base class attributes, and those set to ``kw_only=False`` at the + attribute level. Defaults to False. + + See also `issue #980 + `_. + + getstate_setstate (bool | None): + .. note:: + + This is usually only interesting for slotted classes and you + should probably just set *auto_detect* to True. + + If True, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left None, it's True by default for slotted classes + and False for dict classes. + + If *auto_detect* is True, and *getstate_setstate* is left None, and + **either** ``__getstate__`` or ``__setstate__`` is detected + directly on the class (meaning: not inherited), it is set to False + (this is usually what you want). + + auto_attribs (bool | None): + If True, look at type annotations to determine which attributes to + use, like `dataclasses`. If False, it will only look for explicit + :func:`field` class attributes, like classic *attrs*. + + If left None, it will guess: + + 1. If any attributes are annotated and no unannotated + `attrs.field`\ s are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attrs.field`\ s. + + If *attrs* decides to look at type annotations, **all** fields + **must** be annotated. If *attrs* encounters a field that is set to + a :func:`field` / `attr.ib` but lacks a type annotation, an + `attrs.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = field(...)`` if you don't want to set a + type. + + .. warning:: + + For features that use the attribute name to create decorators + (for example, :ref:`validators `), you still *must* + assign :func:`field` / `attr.ib` to them. Otherwise Python will + either not find the name or try to use the default value to + call, for example, ``validator`` on it. + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `field()` are **ignored**. + + these (dict[str, object]): + A dictionary of name to the (private) return value of `field()` + mappings. This is useful to avoid the definition of your attributes + within the class body because you can't (for example, if you want + to add ``__repr__`` methods to Django models) or don't want to. + + If *these* is not `None`, *attrs* will *not* search the class body + for attributes and will *not* remove any attributes from it. + + The order is deduced from the order of the attributes inside + *these*. + + Arguably, this is a rather obscure feature. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionadded:: 24.3.0 + Unless already present, a ``__replace__`` method is automatically + created for `copy.replace` (Python 3.13+ only). + .. versionchanged:: 25.4.0 + *kw_only* now only applies to attributes defined in the current class, + and respects attribute-level ``kw_only=False`` settings. + .. versionadded:: 25.4.0 + Added *force_kw_only* to go back to the previous *kw_only* behavior. + + .. note:: + + The main differences to the classic `attr.s` are: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - Converters and validators run when attributes are set by default -- + if *frozen* is `False`. + - *slots=True* + + Usually, this has only upsides and few visible effects in everyday + programming. But it *can* lead to some surprising behaviors, so + please make sure to read :term:`slotted classes`. + + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *force_kw_only=False* + - Some options that were only relevant on Python 2 or were kept around + for backwards-compatibility have been removed. + + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + unsafe_hash=unsafe_hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + match_args=match_args, + force_kw_only=force_kw_only, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes convert & validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = _DEFAULT_ON_SETATTR + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)." + raise ValueError(msg) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=None, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new :term:`field` / :term:`attribute` on a class. + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attrs.define` (or similar)! + + Args: + default: + A value that is used if an *attrs*-generated ``__init__`` is used + and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `attrs.Factory`, its callable will + be used to construct a new value (useful for mutable data types + like lists or dicts). + + If a default is not set (or set manually to `attrs.NOTHING`), a + value *must* be supplied when instantiating; otherwise a + `TypeError` will be raised. + + .. seealso:: `defaults` + + factory (~typing.Callable): + Syntactic sugar for ``default=attr.Factory(factory)``. + + validator (~typing.Callable | list[~typing.Callable]): + Callable that is called by *attrs*-generated ``__init__`` methods + after the instance has been initialized. They receive the + initialized instance, the :func:`~attrs.Attribute`, and the passed + value. + + The return value is *not* inspected so the validator has to throw + an exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. + + The validator can also be set using decorator notation as shown + below. + + .. seealso:: :ref:`validators` + + repr (bool | ~typing.Callable): + Include this attribute in the generated ``__repr__`` method. If + True, include the attribute; if False, omit it. By default, the + built-in ``repr()`` function is used. To override how the attribute + value is formatted, pass a ``callable`` that takes a single value + and returns a string. Note that the resulting string is used as-is, + which means it will be used directly *instead* of calling + ``repr()`` (the default). + + eq (bool | ~typing.Callable): + If True (default), include this attribute in the generated + ``__eq__`` and ``__ne__`` methods that check two instances for + equality. To override how the attribute value is compared, pass a + callable that takes a single value and returns the value to be + compared. + + .. seealso:: `comparison` + + order (bool | ~typing.Callable): + If True (default), include this attributes in the generated + ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To + override how the attribute value is ordered, pass a callable that + takes a single value and returns the value to be ordered. + + .. seealso:: `comparison` + + hash (bool | None): + Include this attribute in the generated ``__hash__`` method. If + None (default), mirror *eq*'s value. This is the correct behavior + according the Python spec. Setting this value to anything else + than None is *discouraged*. + + .. seealso:: `hashing` + + init (bool): + Include this attribute in the generated ``__init__`` method. + + It is possible to set this to False and set a default value. In + that case this attributed is unconditionally initialized with the + specified default value or factory. + + .. seealso:: `init` + + converter (typing.Callable | Converter): + A callable that is called by *attrs*-generated ``__init__`` methods + to convert attribute's value to the desired format. + + If a vanilla callable is passed, it is given the passed-in value as + the only positional argument. It is possible to receive additional + arguments by wrapping the callable in a `Converter`. + + Either way, the returned value will be used as the new value of the + attribute. The value is converted before being passed to the + validator, if any. + + .. seealso:: :ref:`converters` + + metadata (dict | None): + An arbitrary mapping, to be used by third-party code. + + .. seealso:: `extending-metadata`. + + type (type): + The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). + This argument is provided for backwards-compatibility and for usage + with `make_class`. Regardless of the approach used, the type will + be stored on ``Attribute.type``. + + Please note that *attrs* doesn't do anything with this metadata by + itself. You can use it as part of your own code or for `static type + checking `. + + kw_only (bool | None): + Make this attribute keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). If None (default), + mirror the setting from `attrs.define`. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + Allows to overwrite the *on_setattr* setting from `attr.s`. If left + None, the *on_setattr* value from `attr.s` is used. Set to + `attrs.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `define()`. + + alias (str | None): + Override this attribute's parameter name in the generated + ``__init__`` method. If left None, default to ``name`` stripped + of leading underscores. See `private-attributes`. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionadded:: 22.2.0 *alias* + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for `attrs.make_class`. + Please note that type checkers ignore this metadata. + .. versionchanged:: 25.4.0 + *kw_only* can now be None, and its default is also changed from False to + None. + + .. seealso:: + + `attr.ib` + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + type=type, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + alias=alias, + ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) + + +def inspect(cls): + """ + Inspect the class and return its effective build parameters. + + Warning: + This feature is currently **experimental** and is not covered by our + strict backwards-compatibility guarantees. + + Args: + cls: The *attrs*-decorated class to inspect. + + Returns: + The effective build parameters of the class. + + Raises: + NotAnAttrsClassError: If the class is not an *attrs*-decorated class. + + .. versionadded:: 25.4.0 + """ + try: + return cls.__dict__["__attrs_props__"] + except KeyError: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) from None diff --git a/lib/python3.12/site-packages/attr/_typing_compat.pyi b/lib/python3.12/site-packages/attr/_typing_compat.pyi new file mode 100644 index 0000000000000000000000000000000000000000..ca7b71e906a28f88726bbd342fdfe636af0281e7 --- /dev/null +++ b/lib/python3.12/site-packages/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/lib/python3.12/site-packages/attr/_version_info.py b/lib/python3.12/site-packages/attr/_version_info.py new file mode 100644 index 0000000000000000000000000000000000000000..27f18884ad4d7a42f338e31a8ea19bc5d19999cd --- /dev/null +++ b/lib/python3.12/site-packages/attr/_version_info.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: MIT + + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo: + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them + + def __hash__(self): + return hash((self.year, self.minor, self.micro, self.releaselevel)) diff --git a/lib/python3.12/site-packages/attr/_version_info.pyi b/lib/python3.12/site-packages/attr/_version_info.pyi new file mode 100644 index 0000000000000000000000000000000000000000..45ced086337783c4b73b26cd17d2c1c260e24029 --- /dev/null +++ b/lib/python3.12/site-packages/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/lib/python3.12/site-packages/attr/converters.py b/lib/python3.12/site-packages/attr/converters.py new file mode 100644 index 0000000000000000000000000000000000000000..0a79deef04282fb33a42f6aca59563d49e70d4cb --- /dev/null +++ b/lib/python3.12/site-packages/attr/converters.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful converters. +""" + +import typing + +from ._compat import _AnnotationExtractor +from ._make import NOTHING, Converter, Factory, pipe + + +__all__ = [ + "default_if_none", + "optional", + "pipe", + "to_bool", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to `None`. + + Type annotations will be inferred from the wrapped converter's, if it has + any. + + Args: + converter (typing.Callable): + the converter that is used for non-`None` values. + + .. versionadded:: 17.1.0 + """ + + if isinstance(converter, Converter): + + def optional_converter(val, inst, field): + if val is None: + return None + return converter(val, inst, field) + + else: + + def optional_converter(val): + if val is None: + return None + return converter(val) + + xtr = _AnnotationExtractor(converter) + + t = xtr.get_first_param_type() + if t: + optional_converter.__annotations__["val"] = typing.Optional[t] + + rt = xtr.get_return_type() + if rt: + optional_converter.__annotations__["return"] = typing.Optional[rt] + + if isinstance(converter, Converter): + return Converter(optional_converter, takes_self=True, takes_field=True) + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace `None` values by *default* or the result + of *factory*. + + Args: + default: + Value to be used if `None` is passed. Passing an instance of + `attrs.Factory` is supported, however the ``takes_self`` option is + *not*. + + factory (typing.Callable): + A callable that takes no parameters whose result is used if `None` + is passed. + + Raises: + TypeError: If **neither** *default* or *factory* is passed. + + TypeError: If **both** *default* and *factory* are passed. + + ValueError: + If an instance of `attrs.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + msg = "Must pass either `default` or `factory`." + raise TypeError(msg) + + if default is not NOTHING and factory is not None: + msg = "Must pass either `default` or `factory` but not both." + raise TypeError(msg) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + msg = "`takes_self` is not supported by default_if_none." + raise ValueError(msg) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (for example, from environment variables) to real + booleans. + + Values mapping to `True`: + + - ``True`` + - ``"true"`` / ``"t"`` + - ``"yes"`` / ``"y"`` + - ``"on"`` + - ``"1"`` + - ``1`` + + Values mapping to `False`: + + - ``False`` + - ``"false"`` / ``"f"`` + - ``"no"`` / ``"n"`` + - ``"off"`` + - ``"0"`` + - ``0`` + + Raises: + ValueError: For any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + + if val in (True, "true", "t", "yes", "y", "on", "1", 1): + return True + if val in (False, "false", "f", "no", "n", "off", "0", 0): + return False + + msg = f"Cannot convert value to bool: {val!r}" + raise ValueError(msg) diff --git a/lib/python3.12/site-packages/attr/converters.pyi b/lib/python3.12/site-packages/attr/converters.pyi new file mode 100644 index 0000000000000000000000000000000000000000..12bd0c4f17bdc60fb8904598af0a3d56d5874a9e --- /dev/null +++ b/lib/python3.12/site-packages/attr/converters.pyi @@ -0,0 +1,19 @@ +from typing import Callable, Any, overload + +from attrs import _ConverterType, _CallableConverterType + +@overload +def pipe(*validators: _CallableConverterType) -> _CallableConverterType: ... +@overload +def pipe(*validators: _ConverterType) -> _ConverterType: ... +@overload +def optional(converter: _CallableConverterType) -> _CallableConverterType: ... +@overload +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: Any) -> _CallableConverterType: ... +@overload +def default_if_none( + *, factory: Callable[[], Any] +) -> _CallableConverterType: ... +def to_bool(val: str | int | bool) -> bool: ... diff --git a/lib/python3.12/site-packages/attr/exceptions.py b/lib/python3.12/site-packages/attr/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..3b7abb8154108aa1d0ae52fa9ee8e489f05b5563 --- /dev/null +++ b/lib/python3.12/site-packages/attr/exceptions.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import ClassVar + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + msg = "can't set attribute" + args: ClassVar[tuple[str]] = [msg] + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An *attrs* function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-*attrs* class has been passed into an *attrs* function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set when defining the field and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has a field without a type annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an *attrs* feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A field requiring a callable has been set with a value that is not + callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/lib/python3.12/site-packages/attr/exceptions.pyi b/lib/python3.12/site-packages/attr/exceptions.pyi new file mode 100644 index 0000000000000000000000000000000000000000..f2680118b404db8f5227d04d27e8439331341c4d --- /dev/null +++ b/lib/python3.12/site-packages/attr/exceptions.pyi @@ -0,0 +1,17 @@ +from typing import Any + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/lib/python3.12/site-packages/attr/filters.py b/lib/python3.12/site-packages/attr/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..689b1705a60ff110d6077bab996f8b4588e55b82 --- /dev/null +++ b/lib/python3.12/site-packages/attr/filters.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attrs.asdict` and `attrs.astuple`. +""" + +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Create a filter that only allows *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to include. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.1.0 Accept strings with field names. + """ + cls, names, attrs = _split_what(what) + + def include_(attribute, value): + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return include_ + + +def exclude(*what): + """ + Create a filter that does **not** allow *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to exclude. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.3.0 Accept field name string as input argument + """ + cls, names, attrs = _split_what(what) + + def exclude_(attribute, value): + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return exclude_ diff --git a/lib/python3.12/site-packages/attr/filters.pyi b/lib/python3.12/site-packages/attr/filters.pyi new file mode 100644 index 0000000000000000000000000000000000000000..974abdcdb51152393d9c9e460c21aa025c45880c --- /dev/null +++ b/lib/python3.12/site-packages/attr/filters.pyi @@ -0,0 +1,6 @@ +from typing import Any + +from . import Attribute, _FilterType + +def include(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... +def exclude(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... diff --git a/lib/python3.12/site-packages/attr/py.typed b/lib/python3.12/site-packages/attr/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/attr/setters.py b/lib/python3.12/site-packages/attr/setters.py new file mode 100644 index 0000000000000000000000000000000000000000..78b08398a6713fc5fa827c2dc853e0d05de743c4 --- /dev/null +++ b/lib/python3.12/site-packages/attr/setters.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly used hooks for on_setattr. +""" + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + # This can be removed once we drop 3.8 and use attrs.Converter instead. + from ._make import Converter + + if not isinstance(c, Converter): + return c(new_value) + + return c(new_value, instance, attrib) + + return new_value + + +# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. +# Sphinx's autodata stopped working, so the docstring is inlined in the API +# docs. +NO_OP = object() diff --git a/lib/python3.12/site-packages/attr/setters.pyi b/lib/python3.12/site-packages/attr/setters.pyi new file mode 100644 index 0000000000000000000000000000000000000000..73abf36e7d5b0f5f56e7fddeee716824c1c60d58 --- /dev/null +++ b/lib/python3.12/site-packages/attr/setters.pyi @@ -0,0 +1,20 @@ +from typing import Any, NewType, NoReturn, TypeVar + +from . import Attribute +from attrs import _OnSetAttrType + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/lib/python3.12/site-packages/attr/validators.py b/lib/python3.12/site-packages/attr/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..837e003b6c2e5ce485d0b1cd19bfa398f18a8d18 --- /dev/null +++ b/lib/python3.12/site-packages/attr/validators.py @@ -0,0 +1,748 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful validators. +""" + +import operator +import re + +from contextlib import contextmanager +from re import Pattern + +from ._config import get_run_validators, set_run_validators +from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none +from .exceptions import NotCallableError + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "disabled", + "ge", + "get_disabled", + "gt", + "in_", + "instance_of", + "is_callable", + "le", + "lt", + "matches_re", + "max_len", + "min_len", + "not_", + "optional", + "or_", + "set_disabled", +] + + +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + Args: + disabled (bool): If `True`, disable running all validators. + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + Returns: + bool:`True` if validators are currently disabled. + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(False) + try: + yield + finally: + set_run_validators(True) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InstanceOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type]): The type to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator: + pattern = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)" + raise ValueError( + msg, + attr, + self.pattern, + value, + ) + + def __repr__(self): + return f"" + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called with a + string that doesn't match *regex*. + + Args: + regex (str, re.Pattern): + A regex string or precompiled pattern to match against + + flags (int): + Flags that will be passed to the underlying re function (default 0) + + func (typing.Callable): + Which underlying `re` function to call. Valid options are + `re.fullmatch`, `re.search`, and `re.match`; the default `None` + means `re.fullmatch`. For performance reasons, the pattern is + always precompiled using `re.compile`. + + .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. + """ + valid_funcs = (re.fullmatch, None, re.search, re.match) + if func not in valid_funcs: + msg = "'func' must be one of {}.".format( + ", ".join( + sorted((e and e.__name__) or "None" for e in set(valid_funcs)) + ) + ) + raise ValueError(msg) + + if isinstance(regex, Pattern): + if flags: + msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" + raise TypeError(msg) + pattern = regex + else: + pattern = re.compile(regex, flags) + + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + else: + match_func = pattern.fullmatch + + return _MatchesReValidator(pattern, match_func) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OptionalValidator: + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return f"" + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to `None` in addition to satisfying the requirements of + the sub-validator. + + Args: + validator + (typing.Callable | tuple[typing.Callable] | list[typing.Callable]): + A validator (or validators) that is used for non-`None` values. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. + """ + if isinstance(validator, (list, tuple)): + return _OptionalValidator(_AndValidator(validator)) + + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InValidator: + options = attrib() + _original_options = attrib(hash=False) + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})" + raise ValueError( + msg, + attr, + self._original_options, + value, + ) + + def __repr__(self): + return f"" + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called with a + value that does not belong in the *options* provided. + + The check is performed using ``value in options``, so *options* has to + support that operation. + + To keep the validator hashable, dicts, lists, and sets are transparently + transformed into a `tuple`. + + Args: + options: Allowed options. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected options, and the value it got. + + .. versionadded:: 17.1.0 + .. versionchanged:: 22.1.0 + The ValueError was incomplete until now and only contained the human + readable error message. Now it contains all the information that has + been promised since 17.1.0. + .. versionchanged:: 24.1.0 + *options* that are a list, dict, or a set are now transformed into a + tuple to keep the validator hashable. + """ + repr_options = options + if isinstance(options, (list, dict, set)): + options = tuple(options) + + return _InValidator(options, repr_options) + + +@attrs(repr=False, slots=False, unsafe_hash=True) +class _IsCallableValidator: + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attrs.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute that is + not callable. + + .. versionadded:: 19.1.0 + + Raises: + attrs.exceptions.NotCallableError: + With a human readable error message containing the attribute + (`attrs.Attribute`) name, and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepIterable: + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else f" {self.iterable_validator!r}" + ) + return ( + f"" + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + Args: + member_validator: Validator(s) to apply to iterable members. + + iterable_validator: + Validator(s) to apply to iterable itself (optional). + + Raises + TypeError: if any sub-validators fail + + .. versionadded:: 19.1.0 + + .. versionchanged:: 25.4.0 + *member_validator* and *iterable_validator* can now be a list or tuple + of validators. + """ + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) + if isinstance(iterable_validator, (list, tuple)): + iterable_validator = and_(*iterable_validator) + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepMapping: + key_validator = attrib(validator=optional(is_callable())) + value_validator = attrib(validator=optional(is_callable())) + mapping_validator = attrib(validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + if self.key_validator is not None: + self.key_validator(inst, attr, key) + if self.value_validator is not None: + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return f"" + + +def deep_mapping( + key_validator=None, value_validator=None, mapping_validator=None +): + """ + A validator that performs deep validation of a dictionary. + + All validators are optional, but at least one of *key_validator* or + *value_validator* must be provided. + + Args: + key_validator: Validator(s) to apply to dictionary keys. + + value_validator: Validator(s) to apply to dictionary values. + + mapping_validator: + Validator(s) to apply to top-level mapping attribute. + + .. versionadded:: 19.1.0 + + .. versionchanged:: 25.4.0 + *key_validator* and *value_validator* are now optional, but at least one + of them must be provided. + + .. versionchanged:: 25.4.0 + *key_validator*, *value_validator*, and *mapping_validator* can now be a + list or tuple of validators. + + Raises: + TypeError: If any sub-validator fails on validation. + + ValueError: + If neither *key_validator* nor *value_validator* is provided on + instantiation. + """ + if key_validator is None and value_validator is None: + msg = ( + "At least one of key_validator or value_validator must be provided" + ) + raise ValueError(msg) + + if isinstance(key_validator, (list, tuple)): + key_validator = and_(*key_validator) + if isinstance(value_validator, (list, tuple)): + value_validator = and_(*value_validator) + if isinstance(mapping_validator, (list, tuple)): + mapping_validator = and_(*mapping_validator) + + return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator: + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number larger or equal to *val*. + + The validator uses `operator.lt` to compare the values. + + Args: + val: Exclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number greater than *val*. + + The validator uses `operator.le` to compare the values. + + Args: + val: Inclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller than *val*. + + The validator uses `operator.ge` to compare the values. + + Args: + val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller or equal to *val*. + + The validator uses `operator.gt` to compare the values. + + Args: + val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator: + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + Args: + length (int): Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) + + +@attrs(repr=False, frozen=True, slots=True) +class _MinLengthValidator: + min_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) < self.min_length: + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def min_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is shorter than *length*. + + Args: + length (int): Minimum length of the string or iterable + + .. versionadded:: 22.1.0 + """ + return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type, ...]): The type(s) to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return f"" + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + Args: + validator: A validator to be logically inverted. + + msg (str): + Message to raise if validator fails. Formatted with keys + ``exc_types`` and ``validator``. + + exc_types (tuple[type, ...]): + Exception type(s) to capture. Other types raised by child + validators will not be intercepted and pass through. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the validator that failed to raise an + exception, the value it got, and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OrValidator: + validators = attrib() + + def __call__(self, inst, attr, value): + for v in self.validators: + try: + v(inst, attr, value) + except Exception: # noqa: BLE001, PERF203, S112 + continue + else: + return + + msg = f"None of {self.validators!r} satisfied for value {value!r}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def or_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators until one of them is + satisfied. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + Raises: + ValueError: + If no validator is satisfied. Raised with a human-readable error + message listing all the wrapped validators and the value that + failed all of them. + + .. versionadded:: 24.1.0 + """ + vals = [] + for v in validators: + vals.extend(v.validators if isinstance(v, _OrValidator) else [v]) + + return _OrValidator(tuple(vals)) diff --git a/lib/python3.12/site-packages/attr/validators.pyi b/lib/python3.12/site-packages/attr/validators.pyi new file mode 100644 index 0000000000000000000000000000000000000000..36a7e800c237f46755ddfca306f8d867a7f493db --- /dev/null +++ b/lib/python3.12/site-packages/attr/validators.pyi @@ -0,0 +1,140 @@ +from types import UnionType +from typing import ( + Any, + AnyStr, + Callable, + Container, + ContextManager, + Iterable, + Mapping, + Match, + Pattern, + TypeVar, + overload, +) + +from attrs import _ValidatorType +from attrs import _ValidatorArgType + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_T4 = TypeVar("_T4") +_T5 = TypeVar("_T5") +_T6 = TypeVar("_T6") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: tuple[type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2]], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2], type[_T3]], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... +@overload +def instance_of(type: UnionType) -> _ValidatorType[Any]: ... +def optional( + validator: ( + _ValidatorType[_T] + | list[_ValidatorType[_T]] + | tuple[_ValidatorType[_T]] + ), +) -> _ValidatorType[_T | None]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: Pattern[AnyStr] | AnyStr, + flags: int = ..., + func: Callable[[AnyStr, AnyStr, int], Match[AnyStr] | None] | None = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorArgType[_T], + iterable_validator: _ValidatorArgType[_I] | None = ..., +) -> _ValidatorType[_I]: ... +@overload +def deep_mapping( + key_validator: _ValidatorArgType[_K], + value_validator: _ValidatorArgType[_V] | None = ..., + mapping_validator: _ValidatorArgType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +@overload +def deep_mapping( + key_validator: _ValidatorArgType[_K] | None = ..., + value_validator: _ValidatorArgType[_V] = ..., + mapping_validator: _ValidatorArgType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... +def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: str | None = None, + exc_types: type[Exception] | Iterable[type[Exception]] = ..., +) -> _ValidatorType[_T]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], + __v6: _ValidatorType[_T6], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ... +@overload +def or_( + __v1: _ValidatorType[Any], + __v2: _ValidatorType[Any], + __v3: _ValidatorType[Any], + __v4: _ValidatorType[Any], + __v5: _ValidatorType[Any], + __v6: _ValidatorType[Any], + *validators: _ValidatorType[Any], +) -> _ValidatorType[Any]: ... diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/INSTALLER b/lib/python3.12/site-packages/flask-2.3.3.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/flask-2.3.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/LICENSE.rst b/lib/python3.12/site-packages/flask-2.3.3.dist-info/LICENSE.rst new file mode 100644 index 0000000000000000000000000000000000000000..9d227a0cc43c3268d15722b763bd94ad298645a1 --- /dev/null +++ b/lib/python3.12/site-packages/flask-2.3.3.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/METADATA b/lib/python3.12/site-packages/flask-2.3.3.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..d7c314536239e2b54a4166f1edf315c2a31b7ea6 --- /dev/null +++ b/lib/python3.12/site-packages/flask-2.3.3.dist-info/METADATA @@ -0,0 +1,116 @@ +Metadata-Version: 2.1 +Name: Flask +Version: 2.3.3 +Summary: A simple framework for building complex web applications. +Maintainer-email: Pallets +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Dist: Werkzeug>=2.3.7 +Requires-Dist: Jinja2>=3.1.2 +Requires-Dist: itsdangerous>=2.1.2 +Requires-Dist: click>=8.1.3 +Requires-Dist: blinker>=1.6.2 +Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10' +Requires-Dist: asgiref>=3.2 ; extra == "async" +Requires-Dist: python-dotenv ; extra == "dotenv" +Project-URL: Changes, https://flask.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://flask.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Issue Tracker, https://github.com/pallets/flask/issues/ +Project-URL: Source Code, https://github.com/pallets/flask/ +Provides-Extra: async +Provides-Extra: dotenv + +Flask +===== + +Flask is a lightweight `WSGI`_ web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around `Werkzeug`_ +and `Jinja`_ and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +.. _WSGI: https://wsgi.readthedocs.io/ +.. _Werkzeug: https://werkzeug.palletsprojects.com/ +.. _Jinja: https://jinja.palletsprojects.com/ + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Flask + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +A Simple Example +---------------- + +.. code-block:: python + + # save this as app.py + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello, World!" + +.. code-block:: text + + $ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + + +Contributing +------------ + +For guidance on setting up a development environment and how to make a +contribution to Flask, see the `contributing guidelines`_. + +.. _contributing guidelines: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst + + +Donate +------ + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://flask.palletsprojects.com/ +- Changes: https://flask.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Flask/ +- Source Code: https://github.com/pallets/flask/ +- Issue Tracker: https://github.com/pallets/flask/issues/ +- Chat: https://discord.gg/pallets + diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/RECORD b/lib/python3.12/site-packages/flask-2.3.3.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..6071e67c47c2f46e8fbe5c69acfcac4c6f31d737 --- /dev/null +++ b/lib/python3.12/site-packages/flask-2.3.3.dist-info/RECORD @@ -0,0 +1,53 @@ +../../../bin/flask,sha256=u2KgtjDhP6gWVLV93L8roQIkRwH7epasXVW7zvXR5Xo,244 +flask-2.3.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask-2.3.3.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +flask-2.3.3.dist-info/METADATA,sha256=-BtXVsnPe7lNA3mcFZHJfsVIiVin1A8LUstChm8qiHo,3588 +flask-2.3.3.dist-info/RECORD,, +flask-2.3.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask-2.3.3.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +flask-2.3.3.dist-info/entry_points.txt,sha256=bBP7hTOS5fz9zLtC7sPofBZAlMkEvBxu7KqS6l5lvc4,40 +flask/__init__.py,sha256=xq09XNKP-Y-fdv6BeGH7RlFaY006tUA3o_llGcl-dno,3731 +flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30 +flask/__pycache__/__init__.cpython-312.pyc,, +flask/__pycache__/__main__.cpython-312.pyc,, +flask/__pycache__/app.cpython-312.pyc,, +flask/__pycache__/blueprints.cpython-312.pyc,, +flask/__pycache__/cli.cpython-312.pyc,, +flask/__pycache__/config.cpython-312.pyc,, +flask/__pycache__/ctx.cpython-312.pyc,, +flask/__pycache__/debughelpers.cpython-312.pyc,, +flask/__pycache__/globals.cpython-312.pyc,, +flask/__pycache__/helpers.cpython-312.pyc,, +flask/__pycache__/logging.cpython-312.pyc,, +flask/__pycache__/scaffold.cpython-312.pyc,, +flask/__pycache__/sessions.cpython-312.pyc,, +flask/__pycache__/signals.cpython-312.pyc,, +flask/__pycache__/templating.cpython-312.pyc,, +flask/__pycache__/testing.cpython-312.pyc,, +flask/__pycache__/typing.cpython-312.pyc,, +flask/__pycache__/views.cpython-312.pyc,, +flask/__pycache__/wrappers.cpython-312.pyc,, +flask/app.py,sha256=ht3Qx9U9z0I1qUfLoS7bYhJcubdpk-i54eHq37LDlN8,87620 +flask/blueprints.py,sha256=ZpVrwa8UY-YnVDsX_1K10XQjDwCUp7Qn2hmKln5icEQ,24332 +flask/cli.py,sha256=PDwZCfPagi5GUzb-D6dEN7y20gWiVAg3ejRnxBKNHPA,33821 +flask/config.py,sha256=YZSZ-xpFj1iW1B1Kj1iDhpc5s7pHncloiRLqXhsU7Hs,12856 +flask/ctx.py,sha256=x2kGzUXtPzVyi2YSKrU_PV1AvtxTmh2iRdriJRTSPGM,14841 +flask/debughelpers.py,sha256=BR0xkd-sAyFuFW07D6NfrqNwSZxk1IrkG5n8zem-3sw,5547 +flask/globals.py,sha256=KUzVvSPh8v28kUasVDi_aQKB9hI2jZSYQHqaDU2P414,2945 +flask/helpers.py,sha256=uVhMwhhfwgjBt8b--zIZTjkfBRK28yPpmNhgVzhP444,25106 +flask/json/__init__.py,sha256=pdtpoK2b0b1u7Sxbx3feM7VWhsI20l1yGAvbYWxaxvc,5572 +flask/json/__pycache__/__init__.cpython-312.pyc,, +flask/json/__pycache__/provider.cpython-312.pyc,, +flask/json/__pycache__/tag.cpython-312.pyc,, +flask/json/provider.py,sha256=Os0frb8oGfyWKL-TDxb0Uy-MY6gDhPdJkRaUl5xAOXI,7637 +flask/json/tag.py,sha256=ihb7QWrNEr0YC3KD4TolZbftgSPCuLk7FAvK49huYC0,8871 +flask/logging.py,sha256=lArx2Bq9oTtUJ-DnZL9t88xU2zytzp4UWSM9Bd72NDQ,2327 +flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask/scaffold.py,sha256=ALGHLcy2qSbJ7ENd1H8dOnq5VDgH5XSFsOkDelcOKV8,33217 +flask/sessions.py,sha256=rFH2QKXG24dEazkKGxAHqUpAUh_30hDHrddhVYgAcY0,14169 +flask/signals.py,sha256=s1H4yKjf3c5dgVr41V6sJpE9dLJvmTJMYuK0rkqx3sw,1146 +flask/templating.py,sha256=XdP2hMFnZ5FCZOG7HUaLjC2VC-b4uHSWlDjwv_1p3qc,7503 +flask/testing.py,sha256=h7AinggrMgGzKlDN66VfB0JjWW4Z1U_OD6FyjqBNiYM,10017 +flask/typing.py,sha256=4Lj-YTxUoYvPYofC9GKu-1o0Ht8lyjp9z3I336J13_o,3005 +flask/views.py,sha256=V5hOGZLx0Bn99QGcM6mh5x_uM-MypVT0-RysEFU84jc,6789 +flask/wrappers.py,sha256=PhMp3teK3SnEmIdog59cO_DHiZ9Btn0qI1EifrTdwP8,5709 diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/REQUESTED b/lib/python3.12/site-packages/flask-2.3.3.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/WHEEL b/lib/python3.12/site-packages/flask-2.3.3.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..3b5e64b5e6c4a210201d1676a891fd57b15cda99 --- /dev/null +++ b/lib/python3.12/site-packages/flask-2.3.3.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.12/site-packages/flask-2.3.3.dist-info/entry_points.txt b/lib/python3.12/site-packages/flask-2.3.3.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..eec6733e577feb9487435b9722713a820bd4ccc1 --- /dev/null +++ b/lib/python3.12/site-packages/flask-2.3.3.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +flask=flask.cli:main + diff --git a/lib/python3.12/site-packages/flask/__init__.py b/lib/python3.12/site-packages/flask/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bdca1b0149179a48a8e417b0c0ae912ebb2a0b57 --- /dev/null +++ b/lib/python3.12/site-packages/flask/__init__.py @@ -0,0 +1,102 @@ +from . import json as json +from .app import Flask as Flask +from .app import Request as Request +from .app import Response as Response +from .blueprints import Blueprint as Blueprint +from .config import Config as Config +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .helpers import abort as abort +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_response as make_response +from .helpers import redirect as redirect +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for +from .json import jsonify as jsonify +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import template_rendered as template_rendered +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string +from .templating import stream_template as stream_template +from .templating import stream_template_string as stream_template_string + +__version__ = "2.3.3" + + +def __getattr__(name): + if name == "_app_ctx_stack": + import warnings + from .globals import __app_ctx_stack + + warnings.warn( + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + from .globals import __request_ctx_stack + + warnings.warn( + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + return __request_ctx_stack + + if name == "escape": + import warnings + from markupsafe import escape + + warnings.warn( + "'flask.escape' is deprecated and will be removed in Flask 2.4. Import" + " 'markupsafe.escape' instead.", + DeprecationWarning, + stacklevel=2, + ) + return escape + + if name == "Markup": + import warnings + from markupsafe import Markup + + warnings.warn( + "'flask.Markup' is deprecated and will be removed in Flask 2.4. Import" + " 'markupsafe.Markup' instead.", + DeprecationWarning, + stacklevel=2, + ) + return Markup + + if name == "signals_available": + import warnings + + warnings.warn( + "'signals_available' is deprecated and will be removed in Flask 2.4." + " Signals are always available", + DeprecationWarning, + stacklevel=2, + ) + return True + + raise AttributeError(name) diff --git a/lib/python3.12/site-packages/flask/__main__.py b/lib/python3.12/site-packages/flask/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..4e28416e104515e90fca4b69cc60d0c61fd15d61 --- /dev/null +++ b/lib/python3.12/site-packages/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/lib/python3.12/site-packages/flask/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9c7345073c949e641bac12448df9e190ff87076 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/__main__.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f30380477e3bd6f66cb16d7b4eab1fa2c910ae6 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/__main__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/app.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c2556565ab3d39436b20f412f5b8cc06f002c26 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/app.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/blueprints.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/blueprints.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79348fd13c9b78e60ad6464157d74df910660c59 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/blueprints.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/cli.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d897274e7ded2e85e66dd73b7799ffc57c82f8d Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/cli.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b6162ea036d4e8f233dff793856737644f40cc5 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/ctx.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/ctx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7279004ea661b0c38360776dd60d1e6b642173b Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/ctx.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/debughelpers.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/debughelpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2ab3c9f63e8f08b03467dd721571d0817007a30 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/debughelpers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/globals.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/globals.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..038aaf967cbe5ae94e61b657f7888d5a0cfc78e5 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/globals.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/helpers.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75ef6628412954bf200cfeb847ad2078c1426558 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/helpers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/logging.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/logging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c256e203fa8d6d879909f68ce4124261b7d256c0 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/logging.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/scaffold.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/scaffold.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7c70ba91a0f5bd4c3a06fa6fc1e6259542e86ce Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/scaffold.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/sessions.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/sessions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87875ab9204a89a63553660db2e7df3b25617781 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/sessions.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/signals.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/signals.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5876546dd1784d8d74ae1deb1e1c25835b69c9b8 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/signals.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/templating.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/templating.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ab9c2979fdbd34ea09144f96ec12cbc8f92b829 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/templating.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/testing.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/testing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a85cc4d95f2708de45f48329819f445e6e3947e6 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/testing.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/typing.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/typing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..173b556570a8c178fbd7c098c7fe31e222f30878 Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/typing.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/views.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82c8ab2924728c66a5d6c194a1c330849fd57d0d Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/views.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/__pycache__/wrappers.cpython-312.pyc b/lib/python3.12/site-packages/flask/__pycache__/wrappers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b1651074500d25e54e81f63bff23472c88346fd Binary files /dev/null and b/lib/python3.12/site-packages/flask/__pycache__/wrappers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/app.py b/lib/python3.12/site-packages/flask/app.py new file mode 100644 index 0000000000000000000000000000000000000000..3b6b38d8ad7f32af70edfead740af1aa313b76f3 --- /dev/null +++ b/lib/python3.12/site-packages/flask/app.py @@ -0,0 +1,2213 @@ +from __future__ import annotations + +import logging +import os +import sys +import typing as t +import weakref +from collections.abc import Iterator as _abc_Iterator +from datetime import timedelta +from inspect import iscoroutinefunction +from itertools import chain +from types import TracebackType +from urllib.parse import quote as _url_quote + +import click +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import Aborter +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.routing import BuildError +from werkzeug.routing import Map +from werkzeug.routing import MapAdapter +from werkzeug.routing import RequestRedirect +from werkzeug.routing import RoutingException +from werkzeug.routing import Rule +from werkzeug.serving import is_running_from_reloader +from werkzeug.utils import cached_property +from werkzeug.utils import redirect as _wz_redirect +from werkzeug.wrappers import Response as BaseResponse + +from . import cli +from . import typing as ft +from .config import Config +from .config import ConfigAttribute +from .ctx import _AppCtxGlobals +from .ctx import AppContext +from .ctx import RequestContext +from .globals import _cv_app +from .globals import _cv_request +from .globals import g +from .globals import request +from .globals import request_ctx +from .globals import session +from .helpers import _split_blueprint_path +from .helpers import get_debug_flag +from .helpers import get_flashed_messages +from .helpers import get_load_dotenv +from .json.provider import DefaultJSONProvider +from .json.provider import JSONProvider +from .logging import create_logger +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import find_package +from .scaffold import Scaffold +from .scaffold import setupmethod +from .sessions import SecureCookieSessionInterface +from .sessions import SessionInterface +from .signals import appcontext_tearing_down +from .signals import got_request_exception +from .signals import request_finished +from .signals import request_started +from .signals import request_tearing_down +from .templating import DispatchingJinjaLoader +from .templating import Environment +from .wrappers import Request +from .wrappers import Response + +if t.TYPE_CHECKING: # pragma: no cover + from .blueprints import Blueprint + from .testing import FlaskClient + from .testing import FlaskCliRunner + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class Flask(Scaffold): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response + + #: The class of the object assigned to :attr:`aborter`, created by + #: :meth:`create_aborter`. That object is called by + #: :func:`flask.abort` to raise HTTP errors, and can be + #: called directly as well. + #: + #: Defaults to :class:`werkzeug.exceptions.Aborter`. + #: + #: .. versionadded:: 2.2 + aborter_class = Aborter + + #: The class that is used for the Jinja environment. + #: + #: .. versionadded:: 0.11 + jinja_environment = Environment + + #: The class that is used for the :data:`~flask.g` instance. + #: + #: Example use cases for a custom class: + #: + #: 1. Store arbitrary attributes on flask.g. + #: 2. Add a property for lazy per-request database connectors. + #: 3. Return None instead of AttributeError on unexpected attributes. + #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. + #: + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is now application context scoped. + #: + #: .. versionadded:: 0.10 + app_ctx_globals_class = _AppCtxGlobals + + #: The class that is used for the ``config`` attribute of this app. + #: Defaults to :class:`~flask.Config`. + #: + #: Example use cases for a custom class: + #: + #: 1. Default values for certain config options. + #: 2. Access to config values through attributes in addition to keys. + #: + #: .. versionadded:: 0.11 + config_class = Config + + #: The testing flag. Set this to ``True`` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate test helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: ``TESTING`` configuration key. Defaults to ``False``. + testing = ConfigAttribute("TESTING") + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. + secret_key = ConfigAttribute("SECRET_KEY") + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute( + "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta + ) + + json_provider_class: type[JSONProvider] = DefaultJSONProvider + """A subclass of :class:`~flask.json.provider.JSONProvider`. An + instance is created and assigned to :attr:`app.json` when creating + the app. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses + Python's built-in :mod:`json` library. A different provider can use + a different JSON library. + + .. versionadded:: 2.2 + """ + + #: Options that are passed to the Jinja environment in + #: :meth:`create_jinja_environment`. Changing these options after + #: the environment is created (accessing :attr:`jinja_env`) will + #: have no effect. + #: + #: .. versionchanged:: 1.1.0 + #: This is a ``dict`` instead of an ``ImmutableDict`` to allow + #: easier configuration. + #: + jinja_options: dict = {} + + #: Default configuration parameters. + default_config = ImmutableDict( + { + "DEBUG": None, + "TESTING": False, + "PROPAGATE_EXCEPTIONS": None, + "SECRET_KEY": None, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "SERVER_NAME": None, + "APPLICATION_ROOT": "/", + "SESSION_COOKIE_NAME": "session", + "SESSION_COOKIE_DOMAIN": None, + "SESSION_COOKIE_PATH": None, + "SESSION_COOKIE_HTTPONLY": True, + "SESSION_COOKIE_SECURE": False, + "SESSION_COOKIE_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "SEND_FILE_MAX_AGE_DEFAULT": None, + "TRAP_BAD_REQUEST_ERRORS": None, + "TRAP_HTTP_EXCEPTIONS": False, + "EXPLAIN_TEMPLATE_LOADING": False, + "PREFERRED_URL_SCHEME": "http", + "TEMPLATES_AUTO_RELOAD": None, + "MAX_COOKIE_SIZE": 4093, + } + ) + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: The map object to use for storing the URL rules and routing + #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. + #: + #: .. versionadded:: 1.1.0 + url_map_class = Map + + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. + #: + #: .. versionadded:: 0.7 + test_client_class: type[FlaskClient] | None = None + + #: The :class:`~click.testing.CliRunner` subclass, by default + #: :class:`~flask.testing.FlaskCliRunner` that is used by + #: :meth:`test_cli_runner`. Its ``__init__`` method should take a + #: Flask app object as the first argument. + #: + #: .. versionadded:: 1.0 + test_cli_runner_class: type[FlaskCliRunner] | None = None + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface: SessionInterface = SecureCookieSessionInterface() + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError( + "If an instance path is provided it must be absolute." + " A relative path was given instead." + ) + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + #: An instance of :attr:`aborter_class` created by + #: :meth:`make_aborter`. This is called by :func:`flask.abort` + #: to raise HTTP errors, and can be called directly as well. + #: + #: .. versionadded:: 2.2 + #: Moved from ``flask.abort``, which calls this object. + self.aborter = self.make_aborter() + + self.json: JSONProvider = self.json_provider_class(self) + """Provides access to JSON methods. Functions in ``flask.json`` + will call methods on this provider when the application context + is active. Used for handling JSON requests and responses. + + An instance of :attr:`json_provider_class`. Can be customized by + changing that attribute on a subclass, or by assigning to this + attribute afterwards. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, + uses Python's built-in :mod:`json` library. A different provider + can use a different JSON library. + + .. versionadded:: 2.2 + """ + + #: A list of functions that are called by + #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function is called + #: with ``error``, ``endpoint`` and ``values``. If a function + #: returns ``None`` or raises a ``BuildError``, it is skipped. + #: Otherwise, its return value is returned by ``url_for``. + #: + #: .. versionadded:: 0.9 + self.url_build_error_handlers: list[ + t.Callable[[Exception, str, dict[str, t.Any]], str] + ] = [] + + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] + + #: A list of shell context processor functions that should be run + #: when a shell context is created. + #: + #: .. versionadded:: 0.11 + self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] + + #: Maps registered blueprint names to blueprint objects. The + #: dict retains the order the blueprints were registered in. + #: Blueprints can be registered multiple times, this dict does + #: not track how often they were attached. + #: + #: .. versionadded:: 0.7 + self.blueprints: dict[str, Blueprint] = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. + #: + #: The key must match the name of the extension module. For example in + #: case of a "Flask-Foo" extension in `flask_foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions: dict = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(super(ListConverter, self).to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = self.url_map_class() + + self.url_map.host_matching = host_matching + self.subdomain_matching = subdomain_matching + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + + # Add a static route using the provided static_url_path, static_host, + # and static_folder if there is a configured static_folder. + # Note we do this without checking if static_folder exists. + # For one, it might be created while the server is running (e.g. during + # development). Also, Google App Engine stores static files somewhere + if self.has_static_folder: + assert ( + bool(static_host) == host_matching + ), "Invalid static_host/host_matching combination" + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + host=static_host, + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ) + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_first_request: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called" + " on the application. It has already handled its first" + " request, any changes will not be applied" + " consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the application are done before" + " running it." + ) + + @cached_property + def name(self) -> str: # type: ignore + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overridden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == "__main__": + fn = getattr(sys.modules["__main__"], "__file__", None) + if fn is None: + return "__main__" + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @cached_property + def logger(self) -> logging.Logger: + """A standard Python :class:`~logging.Logger` for the app, with + the same name as :attr:`name`. + + In debug mode, the logger's :attr:`~logging.Logger.level` will + be set to :data:`~logging.DEBUG`. + + If there are no handlers configured, a default handler will be + added. See :doc:`/logging` for more information. + + .. versionchanged:: 1.1.0 + The logger takes the same name as :attr:`name` rather than + hard-coding ``"flask.app"``. + + .. versionchanged:: 1.0.0 + Behavior was simplified. The logger is always named + ``"flask.app"``. The level is only set during configuration, + it doesn't check ``app.debug`` each time. Only one format is + used, not different ones depending on ``app.debug``. No + handlers are removed, and a handler is only added if no + handlers are already configured. + + .. versionadded:: 0.3 + """ + return create_logger(self) + + @cached_property + def jinja_env(self) -> Environment: + """The Jinja environment used to load templates. + + The environment is created the first time this property is + accessed. Changing :attr:`jinja_options` after that will have no + effect. + """ + return self.create_jinja_environment() + + @property + def got_first_request(self) -> bool: + """This attribute is set to ``True`` if the application started + handling the first request. + + .. deprecated:: 2.3 + Will be removed in Flask 2.4. + + .. versionadded:: 0.8 + """ + import warnings + + warnings.warn( + "'got_first_request' is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + return self._got_first_request + + def make_config(self, instance_relative: bool = False) -> Config: + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + defaults = dict(self.default_config) + defaults["DEBUG"] = get_debug_flag() + return self.config_class(root_path, defaults) + + def make_aborter(self) -> Aborter: + """Create the object to assign to :attr:`aborter`. That object + is called by :func:`flask.abort` to raise HTTP errors, and can + be called directly as well. + + By default, this creates an instance of :attr:`aborter_class`, + which defaults to :class:`werkzeug.exceptions.Aborter`. + + .. versionadded:: 2.2 + """ + return self.aborter_class() + + def auto_find_instance_path(self) -> str: + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, "instance") + return os.path.join(prefix, "var", f"{self.name}-instance") + + def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + :param mode: resource file opening mode, default is 'rb'. + """ + return open(os.path.join(self.instance_path, resource), mode) + + def create_jinja_environment(self) -> Environment: + """Create the Jinja environment based on :attr:`jinja_options` + and the various Jinja-related methods of the app. Changing + :attr:`jinja_options` after this will have no effect. Also adds + Flask-related globals and filters to the environment. + + .. versionchanged:: 0.11 + ``Environment.auto_reload`` set in accordance with + ``TEMPLATES_AUTO_RELOAD`` configuration option. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + + if "autoescape" not in options: + options["autoescape"] = self.select_jinja_autoescape + + if "auto_reload" not in options: + auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] + + if auto_reload is None: + auto_reload = self.debug + + options["auto_reload"] = auto_reload + + rv = self.jinja_environment(self, **options) + rv.globals.update( + url_for=self.url_for, + get_flashed_messages=get_flashed_messages, + config=self.config, + # request, session and g are normally added with the + # context processor for efficiency reasons but for imported + # templates we also want the proxies in there. + request=request, + session=session, + g=g, + ) + rv.policies["json.dumps_function"] = self.json.dumps + return rv + + def create_global_jinja_loader(self) -> DispatchingJinjaLoader: + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def select_jinja_autoescape(self, filename: str) -> bool: + """Returns ``True`` if autoescaping should be active for the given + template name. If no template name is given, returns `True`. + + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + + .. versionadded:: 0.5 + """ + if filename is None: + return True + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) + + def update_template_context(self, context: dict) -> None: + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overridden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + names: t.Iterable[str | None] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. + orig_ctx = context.copy() + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(func()) + + context.update(orig_ctx) + + def make_shell_context(self) -> dict: + """Returns the shell context for an interactive shell for this + application. This runs all the registered shell context + processors. + + .. versionadded:: 0.11 + """ + rv = {"app": self, "g": g} + for processor in self.shell_context_processors: + rv.update(processor()) + return rv + + @property + def debug(self) -> bool: + """Whether debug mode is enabled. When using ``flask run`` to start the + development server, an interactive debugger will be shown for unhandled + exceptions, and the server will be reloaded when code changes. This maps to the + :data:`DEBUG` config key. It may not behave as expected if set late. + + **Do not enable debug mode when deploying in production.** + + Default: ``False`` + """ + return self.config["DEBUG"] + + @debug.setter + def debug(self, value: bool) -> None: + self.config["DEBUG"] = value + + if self.config["TEMPLATES_AUTO_RELOAD"] is None: + self.jinja_env.auto_reload = value + + def run( + self, + host: str | None = None, + port: int | None = None, + debug: bool | None = None, + load_dotenv: bool = True, + **options: t.Any, + ) -> None: + """Runs the application on a local development server. + + Do not use ``run()`` in a production setting. It is not intended to + meet security and performance requirements for a production server. + Instead, see :doc:`/deploying/index` for WSGI server recommendations. + + If the :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + It is not recommended to use this function for development with + automatic reloading as this is badly supported. Instead you should + be using the :command:`flask` command line script's ``run`` support. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to ``True`` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to + have the server available externally as well. Defaults to + ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable + if present. + :param port: the port of the webserver. Defaults to ``5000`` or the + port defined in the ``SERVER_NAME`` config variable if present. + :param debug: if given, enable or disable debug mode. See + :attr:`debug`. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param options: the options to be forwarded to the underlying Werkzeug + server. See :func:`werkzeug.serving.run_simple` for more + information. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment + variables from :file:`.env` and :file:`.flaskenv` files. + + The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. + + Threaded mode is enabled by default. + + .. versionchanged:: 0.10 + The default port is now picked from the ``SERVER_NAME`` + variable. + """ + # Ignore this call so that it doesn't start another server if + # the 'flask run' command is used. + if os.environ.get("FLASK_RUN_FROM_CLI") == "true": + if not is_running_from_reloader(): + click.secho( + " * Ignoring a call to 'app.run()' that would block" + " the current 'flask' CLI command.\n" + " Only call 'app.run()' in an 'if __name__ ==" + ' "__main__"\' guard.', + fg="red", + ) + + return + + if get_load_dotenv(load_dotenv): + cli.load_dotenv() + + # if set, env var overrides existing value + if "FLASK_DEBUG" in os.environ: + self.debug = get_debug_flag() + + # debug passed to method overrides all other sources + if debug is not None: + self.debug = bool(debug) + + server_name = self.config.get("SERVER_NAME") + sn_host = sn_port = None + + if server_name: + sn_host, _, sn_port = server_name.partition(":") + + if not host: + if sn_host: + host = sn_host + else: + host = "127.0.0.1" + + if port or port == 0: + port = int(port) + elif sn_port: + port = int(sn_port) + else: + port = 5000 + + options.setdefault("use_reloader", self.debug) + options.setdefault("use_debugger", self.debug) + options.setdefault("threaded", True) + + cli.show_server_banner(self.debug, self.name) + + from werkzeug.serving import run_simple + + try: + run_simple(t.cast(str, host), port, self, **options) + finally: + # reset the first request information if the development server + # reset normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: + """Creates a test client for this application. For information + about unit testing head over to :doc:`/testing`. + + Note that if you are testing for assertions or exceptions in your + application code, you must set ``app.testing = True`` in order for the + exceptions to propagate to the test client. Otherwise, the exception + will be handled by the application (not visible to the test client) and + the only indication of an AssertionError or other exception will be a + 500 status code response to the test client. See the :attr:`testing` + attribute. For example:: + + app.testing = True + client = app.test_client() + + The test client can be used in a ``with`` block to defer the closing down + of the context until the end of the ``with`` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + Additionally, you may pass optional keyword arguments that will then + be passed to the application's :attr:`test_client_class` constructor. + For example:: + + from flask.testing import FlaskClient + + class CustomClient(FlaskClient): + def __init__(self, *args, **kwargs): + self._authentication = kwargs.pop("authentication") + super(CustomClient,self).__init__( *args, **kwargs) + + app.test_client_class = CustomClient + client = app.test_client(authentication='Basic ....') + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for ``with`` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + + .. versionchanged:: 0.11 + Added `**kwargs` to support passing additional keyword arguments to + the constructor of :attr:`test_client_class`. + """ + cls = self.test_client_class + if cls is None: + from .testing import FlaskClient as cls + return cls( # type: ignore + self, self.response_class, use_cookies=use_cookies, **kwargs + ) + + def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: + """Create a CLI runner for testing CLI commands. + See :ref:`testing-cli`. + + Returns an instance of :attr:`test_cli_runner_class`, by default + :class:`~flask.testing.FlaskCliRunner`. The Flask app object is + passed as the first argument. + + .. versionadded:: 1.0 + """ + cls = self.test_cli_runner_class + + if cls is None: + from .testing import FlaskCliRunner as cls + + return cls(self, **kwargs) # type: ignore + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on the application. Keyword + arguments passed to this method will override the defaults set on the + blueprint. + + Calls the blueprint's :meth:`~flask.Blueprint.register` method after + recording the blueprint in the application's :attr:`blueprints`. + + :param blueprint: The blueprint to register. + :param url_prefix: Blueprint routes will be prefixed with this. + :param subdomain: Blueprint routes will match on this subdomain. + :param url_defaults: Blueprint routes will use these default values for + view arguments. + :param options: Additional keyword arguments are passed to + :class:`~flask.blueprints.BlueprintSetupState`. They can be + accessed in :meth:`~flask.Blueprint.record` callbacks. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 0.7 + """ + blueprint.register(self, options) + + def iter_blueprints(self) -> t.ValuesView[Blueprint]: + """Iterates over all blueprints by the order they were registered. + + .. versionadded:: 0.11 + """ + return self.blueprints.values() + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + options["endpoint"] = endpoint + methods = options.pop("methods", None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only ``GET`` as default. + if methods is None: + methods = getattr(view_func, "methods", None) or ("GET",) + if isinstance(methods, str): + raise TypeError( + "Allowed methods must be a list of strings, for" + ' example: @app.route(..., methods=["POST"])' + ) + methods = {item.upper() for item in methods} + + # Methods that should always be added + required_methods = set(getattr(view_func, "required_methods", ())) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + if provide_automatic_options is None: + provide_automatic_options = getattr( + view_func, "provide_automatic_options", None + ) + + if provide_automatic_options is None: + if "OPTIONS" not in methods: + provide_automatic_options = True + required_methods.add("OPTIONS") + else: + provide_automatic_options = False + + # Add the required methods now. + methods |= required_methods + + rule = self.url_rule_class(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options # type: ignore + + self.url_map.add(rule) + if view_func is not None: + old_func = self.view_functions.get(endpoint) + if old_func is not None and old_func != view_func: + raise AssertionError( + "View function mapping is overwriting an existing" + f" endpoint function: {endpoint}" + ) + self.view_functions[endpoint] = view_func + + @setupmethod + def template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + + @setupmethod + def template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod + def template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + self.jinja_env.globals[name or f.__name__] = f + + @setupmethod + def teardown_appcontext(self, f: T_teardown) -> T_teardown: + """Registers a function to be called when the application + context is popped. The application context is typically popped + after the request context for each request, at the end of CLI + commands, or after a manually pushed context ends. + + .. code-block:: python + + with app.app_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the app context is + made inactive. Since a request context typically also manages an + application context it would also be called when you pop a + request context. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + + @setupmethod + def shell_context_processor( + self, f: T_shell_context_processor + ) -> T_shell_context_processor: + """Registers a shell context processor function. + + .. versionadded:: 0.11 + """ + self.shell_context_processors.append(f) + return f + + def _find_error_handler(self, e: Exception) -> ft.ErrorHandlerCallable | None: + """Return a registered error handler for an exception in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint handler for an exception class, app handler for an exception + class, or ``None`` if a suitable handler is not found. + """ + exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*request.blueprints, None) + + for c in (code, None) if code is not None else (None,): + for name in names: + handler_map = self.error_handler_spec[name][c] + + if not handler_map: + continue + + for cls in exc_class.__mro__: + handler = handler_map.get(cls) + + if handler is not None: + return handler + return None + + def handle_http_exception( + self, e: HTTPException + ) -> HTTPException | ft.ResponseReturnValue: + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionchanged:: 1.0.3 + ``RoutingException``, used internally for actions such as + slash redirects during routing, is not passed to error + handlers. + + .. versionchanged:: 1.0 + Exceptions are looked up by code *and* by MRO, so + ``HTTPException`` subclasses can be handled with a catch-all + handler for the base ``HTTPException``. + + .. versionadded:: 0.3 + """ + # Proxy exceptions don't have error codes. We want to always return + # those unchanged as errors + if e.code is None: + return e + + # RoutingExceptions are used internally to trigger routing + # actions, such as slash redirects raising RequestRedirect. They + # are not raised or handled in user code. + if isinstance(e, RoutingException): + return e + + handler = self._find_error_handler(e) + if handler is None: + return e + return self.ensure_sync(handler)(e) + + def trap_http_exception(self, e: Exception) -> bool: + """Checks if an HTTP exception should be trapped or not. By default + this will return ``False`` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It + also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. + + This is called for all HTTP exceptions raised by a view function. + If it returns ``True`` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionchanged:: 1.0 + Bad request errors are not trapped by default in debug mode. + + .. versionadded:: 0.8 + """ + if self.config["TRAP_HTTP_EXCEPTIONS"]: + return True + + trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] + + # if unset, trap key errors in debug mode + if ( + trap_bad_request is None + and self.debug + and isinstance(e, BadRequestKeyError) + ): + return True + + if trap_bad_request: + return isinstance(e, BadRequest) + + return False + + def handle_user_exception( + self, e: Exception + ) -> HTTPException | ft.ResponseReturnValue: + """This method is called whenever an exception occurs that + should be handled. A special case is :class:`~werkzeug + .exceptions.HTTPException` which is forwarded to the + :meth:`handle_http_exception` method. This function will either + return a response value or reraise the exception with the same + traceback. + + .. versionchanged:: 1.0 + Key errors raised from request data like ``form`` show the + bad key in debug mode rather than a generic bad request + message. + + .. versionadded:: 0.7 + """ + if isinstance(e, BadRequestKeyError) and ( + self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] + ): + e.show_exception = True + + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + handler = self._find_error_handler(e) + + if handler is None: + raise + + return self.ensure_sync(handler)(e) + + def handle_exception(self, e: Exception) -> Response: + """Handle an exception that did not have an error handler + associated with it, or that was raised from an error handler. + This always causes a 500 ``InternalServerError``. + + Always sends the :data:`got_request_exception` signal. + + If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug + mode, the error will be re-raised so that the debugger can + display it. Otherwise, the original exception is logged, and + an :exc:`~werkzeug.exceptions.InternalServerError` is returned. + + If an error handler is registered for ``InternalServerError`` or + ``500``, it will be used. For consistency, the handler will + always receive the ``InternalServerError``. The original + unhandled exception is available as ``e.original_exception``. + + .. versionchanged:: 1.1.0 + Always passes the ``InternalServerError`` instance to the + handler, setting ``original_exception`` to the unhandled + error. + + .. versionchanged:: 1.1.0 + ``after_request`` functions and other finalization is done + even for the default 500 response when there is no handler. + + .. versionadded:: 0.3 + """ + exc_info = sys.exc_info() + got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) + propagate = self.config["PROPAGATE_EXCEPTIONS"] + + if propagate is None: + propagate = self.testing or self.debug + + if propagate: + # Re-raise if called with an active exception, otherwise + # raise the passed in exception. + if exc_info[1] is e: + raise + + raise e + + self.log_exception(exc_info) + server_error: InternalServerError | ft.ResponseReturnValue + server_error = InternalServerError(original_exception=e) + handler = self._find_error_handler(server_error) + + if handler is not None: + server_error = self.ensure_sync(handler)(server_error) + + return self.finalize_request(server_error, from_error_handler=True) + + def log_exception( + self, + exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), + ) -> None: + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error( + f"Exception on {request.path} [{request.method}]", exc_info=exc_info + ) + + def raise_routing_exception(self, request: Request) -> t.NoReturn: + """Intercept routing exceptions and possibly do something else. + + In debug mode, intercept a routing redirect and replace it with + an error if the body will be discarded. + + With modern Werkzeug this shouldn't occur, since it now uses a + 308 status which tells the browser to resend the method and + body. + + .. versionchanged:: 2.1 + Don't intercept 307 and 308 redirects. + + :meta private: + :internal: + """ + if ( + not self.debug + or not isinstance(request.routing_exception, RequestRedirect) + or request.routing_exception.code in {307, 308} + or request.method in {"GET", "HEAD", "OPTIONS"} + ): + raise request.routing_exception # type: ignore + + from .debughelpers import FormDataRoutingRedirect + + raise FormDataRoutingRedirect(request) + + def dispatch_request(self) -> ft.ResponseReturnValue: + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = request_ctx.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule: Rule = req.url_rule # type: ignore[assignment] + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if ( + getattr(rule, "provide_automatic_options", False) + and req.method == "OPTIONS" + ): + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + + def full_dispatch_request(self) -> Response: + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self._got_first_request = True + + try: + request_started.send(self, _async_wrapper=self.ensure_sync) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception as e: + rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request( + self, + rv: ft.ResponseReturnValue | HTTPException, + from_error_handler: bool = False, + ) -> Response: + """Given the return value from a view function this finalizes + the request by converting it into a response and invoking the + postprocessing functions. This is invoked for both normal + request dispatching as well as error handlers. + + Because this means that it might be called as a result of a + failure a special safe mode is available which can be enabled + with the `from_error_handler` flag. If enabled, failures in + response processing will be logged and otherwise ignored. + + :internal: + """ + response = self.make_response(rv) + try: + response = self.process_response(response) + request_finished.send( + self, _async_wrapper=self.ensure_sync, response=response + ) + except Exception: + if not from_error_handler: + raise + self.logger.exception( + "Request finalizing failed with an error while handling an error" + ) + return response + + def make_default_options_response(self) -> Response: + """This method is called to create the default ``OPTIONS`` response. + This can be changed through subclassing to change the default + behavior of ``OPTIONS`` responses. + + .. versionadded:: 0.7 + """ + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] + rv = self.response_class() + rv.allow.update(methods) + return rv + + def should_ignore_error(self, error: BaseException | None) -> bool: + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns ``True`` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + + def ensure_sync(self, func: t.Callable) -> t.Callable: + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. + + .. versionadded:: 2.0 + """ + if iscoroutinefunction(func): + return self.async_to_sync(func) + + return func + + def async_to_sync( + self, func: t.Callable[..., t.Coroutine] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) from None + + return asgiref_async_to_sync(func) + + def url_for( + self, + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, + ) -> str: + """Generate a URL to the given endpoint with the given values. + + This is called by :func:`flask.url_for`, and can be called + directly as well. + + An *endpoint* is the name of a URL rule, usually added with + :meth:`@app.route() `, and usually the same name as the + view function. A route defined in a :class:`~flask.Blueprint` + will prepend the blueprint's name separated by a ``.`` to the + endpoint. + + In some cases, such as email messages, you want URLs to include + the scheme and domain, like ``https://example.com/hello``. When + not in an active request, URLs will be external by default, but + this requires setting :data:`SERVER_NAME` so Flask knows what + domain to use. :data:`APPLICATION_ROOT` and + :data:`PREFERRED_URL_SCHEME` should also be configured as + needed. This config is only used when not in an active request. + + Functions can be decorated with :meth:`url_defaults` to modify + keyword arguments before the URL is built. + + If building fails for some reason, such as an unknown endpoint + or incorrect values, the app's :meth:`handle_url_build_error` + method is called. If that returns a string, that is returned, + otherwise a :exc:`~werkzeug.routing.BuildError` is raised. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it + is external. + :param _external: If given, prefer the URL to be internal + (False) or require it to be external (True). External URLs + include the scheme and domain. When not in an active + request, URLs are external by default. + :param values: Values to use for the variable parts of the URL + rule. Unknown keys are appended as query string arguments, + like ``?a=b&c=d``. + + .. versionadded:: 2.2 + Moved from ``flask.url_for``, which calls this method. + """ + req_ctx = _cv_request.get(None) + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_ctx.request.blueprint + + # If the endpoint starts with "." and the request matches a + # blueprint, the endpoint is relative to the blueprint. + if endpoint[:1] == ".": + if blueprint_name is not None: + endpoint = f"{blueprint_name}{endpoint}" + else: + endpoint = endpoint[1:] + + # When in a request, generate a URL without scheme and + # domain by default, unless a scheme is given. + if _external is None: + _external = _scheme is not None + else: + app_ctx = _cv_app.get(None) + + # If called by helpers.url_for, an app context is active, + # use its url_adapter. Otherwise, app.url_for was called + # directly, build an adapter. + if app_ctx is not None: + url_adapter = app_ctx.url_adapter + else: + url_adapter = self.create_url_adapter(None) + + if url_adapter is None: + raise RuntimeError( + "Unable to build URLs outside an active request" + " without 'SERVER_NAME' configured. Also configure" + " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" + " needed." + ) + + # When outside a request, generate a URL with scheme and + # domain by default. + if _external is None: + _external = True + + # It is an error to set _scheme when _external=False, in order + # to avoid accidental insecure URLs. + if _scheme is not None and not _external: + raise ValueError("When specifying '_scheme', '_external' must be True.") + + self.inject_url_defaults(endpoint, values) + + try: + rv = url_adapter.build( # type: ignore[union-attr] + endpoint, + values, + method=_method, + url_scheme=_scheme, + force_external=_external, + ) + except BuildError as error: + values.update( + _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external + ) + return self.handle_url_build_error(error, endpoint, values) + + if _anchor is not None: + _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") + rv = f"{rv}#{_anchor}" + + return rv + + def redirect(self, location: str, code: int = 302) -> BaseResponse: + """Create a redirect response object. + + This is called by :func:`flask.redirect`, and can be called + directly as well. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + + .. versionadded:: 2.2 + Moved from ``flask.redirect``, which calls this method. + """ + return _wz_redirect(location, code=code, Response=self.response_class) + + def make_response(self, rv: ft.ResponseReturnValue) -> Response: + """Convert the return value from a view function to an instance of + :attr:`response_class`. + + :param rv: the return value from the view function. The view function + must return a response. Returning ``None``, or the view ending + without returning, is not allowed. The following types are allowed + for ``view_rv``: + + ``str`` + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` + A response object is created with the bytes as the body. + + ``dict`` + A dictionary that will be jsonify'd before being returned. + + ``list`` + A list that will be jsonify'd before being returned. + + ``generator`` or ``iterator`` + A generator that returns ``str`` or ``bytes`` to be + streamed as the response. + + ``tuple`` + Either ``(body, status, headers)``, ``(body, status)``, or + ``(body, headers)``, where ``body`` is any of the other types + allowed here, ``status`` is a string or an integer, and + ``headers`` is a dictionary or a list of ``(key, value)`` + tuples. If ``body`` is a :attr:`response_class` instance, + ``status`` overwrites the exiting value and ``headers`` are + extended. + + :attr:`response_class` + The object is returned unchanged. + + other :class:`~werkzeug.wrappers.Response` class + The object is coerced to :attr:`response_class`. + + :func:`callable` + The function is called as a WSGI application. The result is + used to create a response object. + + .. versionchanged:: 2.2 + A generator will be converted to a streaming response. + A list will be converted to a JSON response. + + .. versionchanged:: 1.1 + A dict will be converted to a JSON response. + + .. versionchanged:: 0.9 + Previously a tuple was interpreted as the arguments for the + response object. + """ + + status = headers = None + + # unpack tuple returns + if isinstance(rv, tuple): + len_rv = len(rv) + + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv # type: ignore[misc] + # decide if a 2-tuple has status or headers + elif len_rv == 2: + if isinstance(rv[1], (Headers, dict, tuple, list)): + rv, headers = rv + else: + rv, status = rv # type: ignore[assignment,misc] + # other sized tuples are not allowed + else: + raise TypeError( + "The view function did not return a valid response tuple." + " The tuple must have the form (body, status, headers)," + " (body, status), or (body, headers)." + ) + + # the body must not be None + if rv is None: + raise TypeError( + f"The view function for {request.endpoint!r} did not" + " return a valid response. The function either returned" + " None or ended without a return statement." + ) + + # make sure the body is an instance of the response class + if not isinstance(rv, self.response_class): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, _abc_Iterator): + # let the response class set the status and headers instead of + # waiting to do it manually, so that the class can handle any + # special logic + rv = self.response_class( + rv, + status=status, + headers=headers, # type: ignore[arg-type] + ) + status = headers = None + elif isinstance(rv, (dict, list)): + rv = self.json.response(rv) + elif isinstance(rv, BaseResponse) or callable(rv): + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type( + rv, request.environ # type: ignore[arg-type] + ) + except TypeError as e: + raise TypeError( + f"{e}\nThe view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it" + f" was a {type(rv).__name__}." + ).with_traceback(sys.exc_info()[2]) from None + else: + raise TypeError( + "The view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it was a" + f" {type(rv).__name__}." + ) + + rv = t.cast(Response, rv) + # prefer the status if it was provided + if status is not None: + if isinstance(status, (str, bytes, bytearray)): + rv.status = status + else: + rv.status_code = status + + # extend existing headers with provided headers + if headers: + rv.headers.update(headers) # type: ignore[arg-type] + + return rv + + def create_url_adapter(self, request: Request | None) -> MapAdapter | None: + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set + up so the request is passed explicitly. + + .. versionadded:: 0.6 + + .. versionchanged:: 0.9 + This can now also be called without a request object when the + URL adapter is created for the application context. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + """ + if request is not None: + # If subdomain matching is disabled (the default), use the + # default subdomain in all cases. This should be the default + # in Werkzeug but it currently does not have that feature. + if not self.subdomain_matching: + subdomain = self.url_map.default_subdomain or None + else: + subdomain = None + + return self.url_map.bind_to_environ( + request.environ, + server_name=self.config["SERVER_NAME"], + subdomain=subdomain, + ) + # We need at the very least the server name to be set for this + # to work. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + def inject_url_defaults(self, endpoint: str, values: dict) -> None: + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + names: t.Iterable[str | None] = (None,) + + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. + if "." in endpoint: + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) + + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) + + def handle_url_build_error( + self, error: BuildError, endpoint: str, values: dict[str, t.Any] + ) -> str: + """Called by :meth:`.url_for` if a + :exc:`~werkzeug.routing.BuildError` was raised. If this returns + a value, it will be returned by ``url_for``, otherwise the error + will be re-raised. + + Each function in :attr:`url_build_error_handlers` is called with + ``error``, ``endpoint`` and ``values``. If a function returns + ``None`` or raises a ``BuildError``, it is skipped. Otherwise, + its return value is returned by ``url_for``. + + :param error: The active ``BuildError`` being handled. + :param endpoint: The endpoint being built. + :param values: The keyword arguments passed to ``url_for``. + """ + for handler in self.url_build_error_handlers: + try: + rv = handler(error, endpoint, values) + except BuildError as e: + # make error available outside except block + error = e + else: + if rv is not None: + return rv + + # Re-raise if called with an active exception, otherwise raise + # the passed in exception. + if error is sys.exc_info()[1]: + raise + + raise error + + def preprocess_request(self) -> ft.ResponseReturnValue | None: + """Called before the request is dispatched. Calls + :attr:`url_value_preprocessors` registered with the app and the + current blueprint (if any). Then calls :attr:`before_request_funcs` + registered with the app and the blueprint. + + If any :meth:`before_request` handler returns a non-None value, the + value is handled as if it was the return value from the view, and + further request handling is stopped. + """ + names = (None, *reversed(request.blueprints)) + + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) + + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv + + return None + + def process_response(self, response: Response) -> Response: + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + + if not self.session_interface.is_null_session(ctx.session): + self.session_interface.save_session(self, ctx.session, response) + + return response + + def do_teardown_request( + self, exc: BaseException | None = _sentinel # type: ignore + ) -> None: + """Called after the request is dispatched and the response is + returned, right before the request context is popped. + + This calls all functions decorated with + :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` + if a blueprint handled the request. Finally, the + :data:`request_tearing_down` signal is sent. + + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. + + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. + + .. versionchanged:: 0.9 + Added the ``exc`` argument. + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def do_teardown_appcontext( + self, exc: BaseException | None = _sentinel # type: ignore + ) -> None: + """Called right before the application context is popped. + + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. + + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. + + This is called by + :meth:`AppContext.pop() `. + + .. versionadded:: 0.9 + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for func in reversed(self.teardown_appcontext_funcs): + self.ensure_sync(func)(exc) + + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def app_context(self) -> AppContext: + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. + + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. + + :: + + with app.app_context(): + init_db() + + See :doc:`/appcontext`. + + .. versionadded:: 0.9 + """ + return AppContext(self) + + def request_context(self, environ: dict) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` representing a + WSGI environment. Use a ``with`` block to push the context, + which will make :data:`request` point at this request. + + See :doc:`/reqcontext`. + + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. + + See :doc:`/reqcontext`. + + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: + + with app.test_request_context(...): + generate_report() + + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() + + Takes the same arguments as Werkzeug's + :class:`~werkzeug.test.EnvironBuilder`, with some defaults from + the application. See the linked Werkzeug docs for most of the + available arguments. Flask-specific behavior is listed here. + + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to + :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param data: The request body, either as a string or a dict of + form keys and values. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + from .testing import EnvironBuilder + + builder = EnvironBuilder(self, *args, **kwargs) + + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: + """The actual WSGI application. This is not implemented in + :meth:`__call__` so that middlewares can be applied without + losing a reference to the app object. Instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + Teardown events for the request and app contexts are called + even if an unhandled error occurs. Other events may not be + called depending on when an error occurs during dispatch. + See :ref:`callbacks-and-errors`. + + :param environ: A WSGI environment. + :param start_response: A callable accepting a status code, + a list of headers, and an optional exception context to + start the response. + """ + ctx = self.request_context(environ) + error: BaseException | None = None + try: + try: + ctx.push() + response = self.full_dispatch_request() + except Exception as e: + error = e + response = self.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + return response(environ, start_response) + finally: + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) + + if error is not None and self.should_ignore_error(error): + error = None + + ctx.pop(error) + + def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: + """The WSGI server calls the Flask application object as the + WSGI application. This calls :meth:`wsgi_app`, which can be + wrapped to apply middleware. + """ + return self.wsgi_app(environ, start_response) diff --git a/lib/python3.12/site-packages/flask/blueprints.py b/lib/python3.12/site-packages/flask/blueprints.py new file mode 100644 index 0000000000000000000000000000000000000000..0407f86fefdca40ce4487ad006a4e65bee0d1ee6 --- /dev/null +++ b/lib/python3.12/site-packages/flask/blueprints.py @@ -0,0 +1,626 @@ +from __future__ import annotations + +import os +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from . import typing as ft +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + +DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) + + +class BlueprintSetupState: + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__( + self, + blueprint: Blueprint, + app: Flask, + options: t.Any, + first_registration: bool, + ) -> None: + #: a reference to the current application + self.app = app + + #: a reference to the blueprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get("subdomain") + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, ``None`` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get("url_prefix") + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + self.name = self.options.get("name", blueprint.name) + self.name_prefix = self.options.get("name_prefix", "") + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get("url_defaults", ())) + + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: t.Callable | None = None, + **options: t.Any, + ) -> None: + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix is not None: + if rule: + rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) + else: + rule = self.url_prefix + options.setdefault("subdomain", self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + defaults = self.url_defaults + if "defaults" in options: + defaults = dict(defaults, **options.pop("defaults")) + + self.app.add_url_rule( + rule, + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), + view_func, + defaults=defaults, + **options, + ) + + +class Blueprint(Scaffold): + """Represents a blueprint, a collection of routes and other + app-related functions that can be registered on a real application + later. + + A blueprint is an object that allows defining application functions + without requiring an application object ahead of time. It uses the + same decorators as :class:`~flask.Flask`, but defers the need for an + application by recording them for later registration. + + Decorating a function with a blueprint creates a deferred function + that is called with :class:`~flask.blueprints.BlueprintSetupState` + when the blueprint is registered on an application. + + See :doc:`/blueprints` for more information. + + :param name: The name of the blueprint. Will be prepended to each + endpoint name. + :param import_name: The name of the blueprint package, usually + ``__name__``. This helps locate the ``root_path`` for the + blueprint. + :param static_folder: A folder with static files that should be + served by the blueprint's static route. The path is relative to + the blueprint's root path. Blueprint static files are disabled + by default. + :param static_url_path: The url to serve static files from. + Defaults to ``static_folder``. If the blueprint does not have + a ``url_prefix``, the app's static route will take precedence, + and the blueprint's static files won't be accessible. + :param template_folder: A folder with templates that should be added + to the app's template search path. The path is relative to the + blueprint's root path. Blueprint templates are disabled by + default. Blueprint templates have a lower precedence than those + in the app's templates folder. + :param url_prefix: A path to prepend to all of the blueprint's URLs, + to make them distinct from the rest of the app's routes. + :param subdomain: A subdomain that blueprint routes will match on by + default. + :param url_defaults: A dict of default values that blueprint routes + will receive by default. + :param root_path: By default, the blueprint will automatically set + this based on ``import_name``. In certain situations this + automatic detection can fail, so the path can be specified + manually instead. + + .. versionchanged:: 1.1.0 + Blueprints have a ``cli`` group to register nested CLI commands. + The ``cli_group`` parameter controls the name of the group under + the ``flask`` command. + + .. versionadded:: 0.7 + """ + + _got_registered_once = False + + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if not name: + raise ValueError("'name' may not be empty.") + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.deferred_functions: list[DeferredSetupFunction] = [] + + if url_defaults is None: + url_defaults = {} + + self.url_values_defaults = url_defaults + self.cli_group = cli_group + self._blueprints: list[tuple[Blueprint, dict]] = [] + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called on the blueprint" + f" '{self.name}'. It has already been registered at least once, any" + " changes will not be applied consistently.\n" + "Make sure all imports, decorators, functions, etc. needed to set up" + " the blueprint are done before registering it." + ) + + @setupmethod + def record(self, func: t.Callable) -> None: + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + self.deferred_functions.append(func) + + @setupmethod + def record_once(self, func: t.Callable) -> None: + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + + def wrapper(state: BlueprintSetupState) -> None: + if state.first_registration: + func(state) + + self.record(update_wrapper(wrapper, func)) + + def make_setup_state( + self, app: Flask, options: dict, first_registration: bool = False + ) -> BlueprintSetupState: + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on this blueprint. Keyword + arguments passed to this method will override the defaults set + on the blueprint. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 2.0 + """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") + self._blueprints.append((blueprint, options)) + + def register(self, app: Flask, options: dict) -> None: + """Called by :meth:`Flask.register_blueprint` to register all + views and callbacks registered on the blueprint with the + application. Creates a :class:`.BlueprintSetupState` and calls + each :meth:`record` callback with it. + + :param app: The application this blueprint is being registered + with. + :param options: Keyword arguments forwarded from + :meth:`~Flask.register_blueprint`. + + .. versionchanged:: 2.3 + Nested blueprints now correctly apply subdomains. + + .. versionchanged:: 2.1 + Registering the same blueprint with the same name multiple + times is an error. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + """ + name_prefix = options.get("name_prefix", "") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") + + if name in app.blueprints: + bp_desc = "this" if app.blueprints[name] is self else "a different" + existing_at = f" '{name}'" if self_name != name else "" + + raise ValueError( + f"The name '{self_name}' is already registered for" + f" {bp_desc} blueprint{existing_at}. Use 'name=' to" + f" provide a unique name." + ) + + first_bp_registration = not any(bp is self for bp in app.blueprints.values()) + first_name_registration = name not in app.blueprints + + app.blueprints[name] = self + self._got_registered_once = True + state = self.make_setup_state(app, options, first_bp_registration) + + if self.has_static_folder: + state.add_url_rule( + f"{self.static_url_path}/", + view_func=self.send_static_file, + endpoint="static", + ) + + # Merge blueprint data into parent. + if first_bp_registration or first_name_registration: + + def extend(bp_dict, parent_dict): + for key, values in bp_dict.items(): + key = name if key is None else f"{name}.{key}" + parent_dict[key].extend(values) + + for key, value in self.error_handler_spec.items(): + key = name if key is None else f"{name}.{key}" + value = defaultdict( + dict, + { + code: { + exc_class: func for exc_class, func in code_values.items() + } + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value + + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = func + + extend(self.before_request_funcs, app.before_request_funcs) + extend(self.after_request_funcs, app.after_request_funcs) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ) + extend(self.url_default_functions, app.url_default_functions) + extend(self.url_value_preprocessors, app.url_value_preprocessors) + extend(self.template_context_processors, app.template_context_processors) + + for deferred in self.deferred_functions: + deferred(state) + + cli_resolved_group = options.get("cli_group", self.cli_group) + + if self.cli.commands: + if cli_resolved_group is None: + app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: + self.cli.name = name + app.cli.add_command(self.cli) + else: + self.cli.name = cli_resolved_group + app.cli.add_command(self.cli) + + for blueprint, bp_options in self._blueprints: + bp_options = bp_options.copy() + bp_url_prefix = bp_options.get("url_prefix") + bp_subdomain = bp_options.get("subdomain") + + if bp_subdomain is None: + bp_subdomain = blueprint.subdomain + + if state.subdomain is not None and bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + "." + state.subdomain + elif bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + elif state.subdomain is not None: + bp_options["subdomain"] = state.subdomain + + if bp_url_prefix is None: + bp_url_prefix = blueprint.url_prefix + + if state.url_prefix is not None and bp_url_prefix is not None: + bp_options["url_prefix"] = ( + state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") + ) + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: + bp_options["url_prefix"] = state.url_prefix + + bp_options["name_prefix"] = name + blueprint.register(app, bp_options) + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for + full documentation. + + The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, + used with :func:`url_for`, is prefixed with the blueprint's name. + """ + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") + + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") + + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) + + @setupmethod + def app_template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """Register a template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_app_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.filters[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_app_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.tests[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_app_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.globals[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def before_app_request(self, f: T_before_request) -> T_before_request: + """Like :meth:`before_request`, but before every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.before_request`. + """ + self.record_once( + lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def after_app_request(self, f: T_after_request) -> T_after_request: + """Like :meth:`after_request`, but after every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.after_request`. + """ + self.record_once( + lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def teardown_app_request(self, f: T_teardown) -> T_teardown: + """Like :meth:`teardown_request`, but after every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. + """ + self.record_once( + lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_context_processor( + self, f: T_template_context_processor + ) -> T_template_context_processor: + """Like :meth:`context_processor`, but for templates rendered by every view, not + only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. + """ + self.record_once( + lambda s: s.app.template_context_processors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_errorhandler( + self, code: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Like :meth:`errorhandler`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.errorhandler`. + """ + + def decorator(f: T_error_handler) -> T_error_handler: + self.record_once(lambda s: s.app.errorhandler(code)(f)) + return f + + return decorator + + @setupmethod + def app_url_value_preprocessor( + self, f: T_url_value_preprocessor + ) -> T_url_value_preprocessor: + """Like :meth:`url_value_preprocessor`, but for every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. + """ + self.record_once( + lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Like :meth:`url_defaults`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.url_defaults`. + """ + self.record_once( + lambda s: s.app.url_default_functions.setdefault(None, []).append(f) + ) + return f diff --git a/lib/python3.12/site-packages/flask/cli.py b/lib/python3.12/site-packages/flask/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..dda266b30ca810c1db50e429e9357845b9a9962d --- /dev/null +++ b/lib/python3.12/site-packages/flask/cli.py @@ -0,0 +1,1068 @@ +from __future__ import annotations + +import ast +import importlib.metadata +import inspect +import os +import platform +import re +import sys +import traceback +import typing as t +from functools import update_wrapper +from operator import itemgetter + +import click +from click.core import ParameterSource +from werkzeug import run_simple +from werkzeug.serving import is_running_from_reloader +from werkzeug.utils import import_string + +from .globals import current_app +from .helpers import get_debug_flag +from .helpers import get_load_dotenv + +if t.TYPE_CHECKING: + from .app import Flask + + +class NoAppException(click.UsageError): + """Raised if an application cannot be found or loaded.""" + + +def find_best_app(module): + """Given a module instance this tries to find the best possible + application in the module or raises an exception. + """ + from . import Flask + + # Search for the most common names first. + for attr_name in ("app", "application"): + app = getattr(module, attr_name, None) + + if isinstance(app, Flask): + return app + + # Otherwise find the only object that is a Flask instance. + matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise NoAppException( + "Detected multiple Flask applications in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify the correct one." + ) + + # Search for app factory functions. + for attr_name in ("create_app", "make_app"): + app_factory = getattr(module, attr_name, None) + + if inspect.isfunction(app_factory): + try: + app = app_factory() + + if isinstance(app, Flask): + return app + except TypeError as e: + if not _called_with_wrong_args(app_factory): + raise + + raise NoAppException( + f"Detected factory '{attr_name}' in module '{module.__name__}'," + " but could not call it without arguments. Use" + f" '{module.__name__}:{attr_name}(args)'" + " to specify arguments." + ) from e + + raise NoAppException( + "Failed to find Flask application or factory in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify one." + ) + + +def _called_with_wrong_args(f): + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the factory raised the + error. + + :param f: The function that was called. + :return: ``True`` if the call failed. + """ + tb = sys.exc_info()[2] + + try: + while tb is not None: + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. + return False + + tb = tb.tb_next + + # Didn't reach the function. + return True + finally: + # Delete tb to break a circular reference. + # https://docs.python.org/2/library/sys.html#sys.exc_info + del tb + + +def find_app_by_string(module, app_name): + """Check if the given string is a variable name or a function. Call + a function to get the app instance, or return the variable directly. + """ + from . import Flask + + # Parse app_name as a single expression to determine if it's a valid + # attribute name or function call. + try: + expr = ast.parse(app_name.strip(), mode="eval").body + except SyntaxError: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) from None + + if isinstance(expr, ast.Name): + name = expr.id + args = [] + kwargs = {} + elif isinstance(expr, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expr.func, ast.Name): + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) + + name = expr.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expr.args] + kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise NoAppException( + f"Failed to parse arguments as literal values: {app_name!r}." + ) from None + else: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) + + try: + attr = getattr(module, name) + except AttributeError as e: + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e + + # If the attribute is a function, call it with any args and kwargs + # to get the real application. + if inspect.isfunction(attr): + try: + app = attr(*args, **kwargs) + except TypeError as e: + if not _called_with_wrong_args(attr): + raise + + raise NoAppException( + f"The factory {app_name!r} in module" + f" {module.__name__!r} could not be called with the" + " specified arguments." + ) from e + else: + app = attr + + if isinstance(app, Flask): + return app + + raise NoAppException( + "A valid Flask application was not obtained from" + f" '{module.__name__}:{app_name}'." + ) + + +def prepare_import(path): + """Given a filename this will try to calculate the python path, add it + to the search path and return the actual module name that is expected. + """ + path = os.path.realpath(path) + + fname, ext = os.path.splitext(path) + if ext == ".py": + path = fname + + if os.path.basename(path) == "__init__": + path = os.path.dirname(path) + + module_name = [] + + # move up until outside package structure (no __init__.py) + while True: + path, name = os.path.split(path) + module_name.append(name) + + if not os.path.exists(os.path.join(path, "__init__.py")): + break + + if sys.path[0] != path: + sys.path.insert(0, path) + + return ".".join(module_name[::-1]) + + +def locate_app(module_name, app_name, raise_if_not_found=True): + try: + __import__(module_name) + except ImportError: + # Reraise the ImportError if it occurred within the imported module. + # Determine this by checking whether the trace has a depth > 1. + if sys.exc_info()[2].tb_next: + raise NoAppException( + f"While importing {module_name!r}, an ImportError was" + f" raised:\n\n{traceback.format_exc()}" + ) from None + elif raise_if_not_found: + raise NoAppException(f"Could not import {module_name!r}.") from None + else: + return + + module = sys.modules[module_name] + + if app_name is None: + return find_best_app(module) + else: + return find_app_by_string(module, app_name) + + +def get_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + + flask_version = importlib.metadata.version("flask") + werkzeug_version = importlib.metadata.version("werkzeug") + + click.echo( + f"Python {platform.python_version()}\n" + f"Flask {flask_version}\n" + f"Werkzeug {werkzeug_version}", + color=ctx.color, + ) + ctx.exit() + + +version_option = click.Option( + ["--version"], + help="Show the Flask version.", + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True, +) + + +class ScriptInfo: + """Helper object to deal with Flask applications. This is usually not + necessary to interface with as it's used internally in the dispatching + to click. In future versions of Flask this object will most likely play + a bigger role. Typically it's created automatically by the + :class:`FlaskGroup` but you can also manually create it and pass it + onwards as click object. + """ + + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: bool = True, + ) -> None: + #: Optionally the import path for the Flask application. + self.app_import_path = app_import_path + #: Optionally a function that is passed the script info to create + #: the instance of the application. + self.create_app = create_app + #: A dictionary with arbitrary data that can be associated with + #: this script info. + self.data: dict[t.Any, t.Any] = {} + self.set_debug_flag = set_debug_flag + self._loaded_app: Flask | None = None + + def load_app(self) -> Flask: + """Loads the Flask app (if not yet loaded) and returns it. Calling + this multiple times will just result in the already loaded app to + be returned. + """ + if self._loaded_app is not None: + return self._loaded_app + + if self.create_app is not None: + app = self.create_app() + else: + if self.app_import_path: + path, name = ( + re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] + )[:2] + import_name = prepare_import(path) + app = locate_app(import_name, name) + else: + for path in ("wsgi.py", "app.py"): + import_name = prepare_import(path) + app = locate_app(import_name, None, raise_if_not_found=False) + + if app: + break + + if not app: + raise NoAppException( + "Could not locate a Flask application. Use the" + " 'flask --app' option, 'FLASK_APP' environment" + " variable, or a 'wsgi.py' or 'app.py' file in the" + " current directory." + ) + + if self.set_debug_flag: + # Update the app's debug flag through the descriptor so that + # other values repopulate as well. + app.debug = get_debug_flag() + + self._loaded_app = app + return app + + +pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) + + +def with_appcontext(f): + """Wraps a callback so that it's guaranteed to be executed with the + script's application context. + + Custom commands (and their options) registered under ``app.cli`` or + ``blueprint.cli`` will always have an app context available, this + decorator is not required in that case. + + .. versionchanged:: 2.2 + The app context is active for subcommands as well as the + decorated callback. The app context is always available to + ``app.cli`` command and parameter callbacks. + """ + + @click.pass_context + def decorator(__ctx, *args, **kwargs): + if not current_app: + app = __ctx.ensure_object(ScriptInfo).load_app() + __ctx.with_resource(app.app_context()) + + return __ctx.invoke(f, *args, **kwargs) + + return update_wrapper(decorator, f) + + +class AppGroup(click.Group): + """This works similar to a regular click :class:`~click.Group` but it + changes the behavior of the :meth:`command` decorator so that it + automatically wraps the functions in :func:`with_appcontext`. + + Not to be confused with :class:`FlaskGroup`. + """ + + def command(self, *args, **kwargs): + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` + unless it's disabled by passing ``with_appcontext=False``. + """ + wrap_for_ctx = kwargs.pop("with_appcontext", True) + + def decorator(f): + if wrap_for_ctx: + f = with_appcontext(f) + return click.Group.command(self, *args, **kwargs)(f) + + return decorator + + def group(self, *args, **kwargs): + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it defaults the group class to + :class:`AppGroup`. + """ + kwargs.setdefault("cls", AppGroup) + return click.Group.group(self, *args, **kwargs) + + +def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: + if value is None: + return None + + info = ctx.ensure_object(ScriptInfo) + info.app_import_path = value + return value + + +# This option is eager so the app will be available if --help is given. +# --help is also eager, so --app must be before it in the param list. +# no_args_is_help bypasses eager processing, so this option must be +# processed manually in that case to ensure FLASK_APP gets picked up. +_app_option = click.Option( + ["-A", "--app"], + metavar="IMPORT", + help=( + "The Flask application or factory function to load, in the form 'module:name'." + " Module can be a dotted import or file path. Name is not required if it is" + " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" + " pass arguments." + ), + is_eager=True, + expose_value=False, + callback=_set_app, +) + + +def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: + # If the flag isn't provided, it will default to False. Don't use + # that, let debug be set by env in that case. + source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] + + if source is not None and source in ( + ParameterSource.DEFAULT, + ParameterSource.DEFAULT_MAP, + ): + return None + + # Set with env var instead of ScriptInfo.load so that it can be + # accessed early during a factory function. + os.environ["FLASK_DEBUG"] = "1" if value else "0" + return value + + +_debug_option = click.Option( + ["--debug/--no-debug"], + help="Set debug mode.", + expose_value=False, + callback=_set_debug, +) + + +def _env_file_callback( + ctx: click.Context, param: click.Option, value: str | None +) -> str | None: + if value is None: + return None + + import importlib + + try: + importlib.import_module("dotenv") + except ImportError: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Don't check FLASK_SKIP_DOTENV, that only disables automatically + # loading .env and .flaskenv files. + load_dotenv(value) + return value + + +# This option is eager so env vars are loaded as early as possible to be +# used by other options. +_env_file_option = click.Option( + ["-e", "--env-file"], + type=click.Path(exists=True, dir_okay=False), + help="Load environment variables from this file. python-dotenv must be installed.", + is_eager=True, + expose_value=False, + callback=_env_file_callback, +) + + +class FlaskGroup(AppGroup): + """Special subclass of the :class:`AppGroup` group that supports + loading more commands from the configured Flask app. Normally a + developer does not have to interface with this class but there are + some very advanced use cases for which it makes sense to create an + instance of this. see :ref:`custom-scripts`. + + :param add_default_commands: if this is True then the default run and + shell commands will be added. + :param add_version_option: adds the ``--version`` option. + :param create_app: an optional callback that is passed the script info and + returns the loaded app. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param set_debug_flag: Set the app's debug flag. + + .. versionchanged:: 2.2 + Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. + + .. versionchanged:: 2.2 + An app context is pushed when running ``app.cli`` commands, so + ``@with_appcontext`` is no longer required for those commands. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment variables + from :file:`.env` and :file:`.flaskenv` files. + """ + + def __init__( + self, + add_default_commands: bool = True, + create_app: t.Callable[..., Flask] | None = None, + add_version_option: bool = True, + load_dotenv: bool = True, + set_debug_flag: bool = True, + **extra: t.Any, + ) -> None: + params = list(extra.pop("params", None) or ()) + # Processing is done with option callbacks instead of a group + # callback. This allows users to make a custom group callback + # without losing the behavior. --env-file must come first so + # that it is eagerly evaluated before --app. + params.extend((_env_file_option, _app_option, _debug_option)) + + if add_version_option: + params.append(version_option) + + if "context_settings" not in extra: + extra["context_settings"] = {} + + extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") + + super().__init__(params=params, **extra) + + self.create_app = create_app + self.load_dotenv = load_dotenv + self.set_debug_flag = set_debug_flag + + if add_default_commands: + self.add_command(run_command) + self.add_command(shell_command) + self.add_command(routes_command) + + self._loaded_plugin_commands = False + + def _load_plugin_commands(self): + if self._loaded_plugin_commands: + return + + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata + + for ep in metadata.entry_points(group="flask.commands"): + self.add_command(ep.load(), ep.name) + + self._loaded_plugin_commands = True + + def get_command(self, ctx, name): + self._load_plugin_commands() + # Look up built-in and plugin commands, which should be + # available even if the app fails to load. + rv = super().get_command(ctx, name) + + if rv is not None: + return rv + + info = ctx.ensure_object(ScriptInfo) + + # Look up commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + app = info.load_app() + except NoAppException as e: + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + return None + + # Push an app context for the loaded app unless it is already + # active somehow. This makes the context available to parameter + # and command callbacks without needing @with_appcontext. + if not current_app or current_app._get_current_object() is not app: + ctx.with_resource(app.app_context()) + + return app.cli.get_command(ctx, name) + + def list_commands(self, ctx): + self._load_plugin_commands() + # Start with the built-in and plugin commands. + rv = set(super().list_commands(ctx)) + info = ctx.ensure_object(ScriptInfo) + + # Add commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException as e: + # When an app couldn't be loaded, show the error message + # without the traceback. + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + except Exception: + # When any other errors occurred during loading, show the + # full traceback. + click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") + + return sorted(rv) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: click.Context | None = None, + **extra: t.Any, + ) -> click.Context: + # Set a flag to tell app.run to become a no-op. If app.run was + # not in a __name__ == __main__ guard, it would start the server + # when importing, blocking whatever command is being called. + os.environ["FLASK_RUN_FROM_CLI"] = "true" + + # Attempt to load .env and .flask env files. The --env-file + # option can cause another file to be loaded. + if get_load_dotenv(self.load_dotenv): + load_dotenv() + + if "obj" not in extra and "obj" not in self.context_settings: + extra["obj"] = ScriptInfo( + create_app=self.create_app, set_debug_flag=self.set_debug_flag + ) + + return super().make_context(info_name, args, parent=parent, **extra) + + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help: + # Attempt to load --env-file and --app early in case they + # were given as env vars. Otherwise no_args_is_help will not + # see commands from app.cli. + _env_file_option.handle_parse_result(ctx, {}, []) + _app_option.handle_parse_result(ctx, {}, []) + + return super().parse_args(ctx, args) + + +def _path_is_ancestor(path, other): + """Take ``other`` and remove the length of ``path`` from it. Then join it + to ``path``. If it is the original value, ``path`` is an ancestor of + ``other``.""" + return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other + + +def load_dotenv(path: str | os.PathLike | None = None) -> bool: + """Load "dotenv" files in order of precedence to set environment variables. + + If an env var is already set it is not overwritten, so earlier files in the + list are preferred over later files. + + This is a no-op if `python-dotenv`_ is not installed. + + .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme + + :param path: Load the file at this location instead of searching. + :return: ``True`` if a file was loaded. + + .. versionchanged:: 2.0 + The current directory is not changed to the location of the + loaded file. + + .. versionchanged:: 2.0 + When loading the env files, set the default encoding to UTF-8. + + .. versionchanged:: 1.1.0 + Returns ``False`` when python-dotenv is not installed, or when + the given path isn't a file. + + .. versionadded:: 1.0 + """ + try: + import dotenv + except ImportError: + if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): + click.secho( + " * Tip: There are .env or .flaskenv files present." + ' Do "pip install python-dotenv" to use them.', + fg="yellow", + err=True, + ) + + return False + + # Always return after attempting to load a given path, don't load + # the default files. + if path is not None: + if os.path.isfile(path): + return dotenv.load_dotenv(path, encoding="utf-8") + + return False + + loaded = False + + for name in (".env", ".flaskenv"): + path = dotenv.find_dotenv(name, usecwd=True) + + if not path: + continue + + dotenv.load_dotenv(path, encoding="utf-8") + loaded = True + + return loaded # True if at least one file was located and loaded. + + +def show_server_banner(debug, app_import_path): + """Show extra startup messages the first time the server is run, + ignoring the reloader. + """ + if is_running_from_reloader(): + return + + if app_import_path is not None: + click.echo(f" * Serving Flask app '{app_import_path}'") + + if debug is not None: + click.echo(f" * Debug mode: {'on' if debug else 'off'}") + + +class CertParamType(click.ParamType): + """Click option type for the ``--cert`` option. Allows either an + existing file, the string ``'adhoc'``, or an import for a + :class:`~ssl.SSLContext` object. + """ + + name = "path" + + def __init__(self): + self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) + + def convert(self, value, param, ctx): + try: + import ssl + except ImportError: + raise click.BadParameter( + 'Using "--cert" requires Python to be compiled with SSL support.', + ctx, + param, + ) from None + + try: + return self.path_type(value, param, ctx) + except click.BadParameter: + value = click.STRING(value, param, ctx).lower() + + if value == "adhoc": + try: + import cryptography # noqa: F401 + except ImportError: + raise click.BadParameter( + "Using ad-hoc certificates requires the cryptography library.", + ctx, + param, + ) from None + + return value + + obj = import_string(value, silent=True) + + if isinstance(obj, ssl.SSLContext): + return obj + + raise + + +def _validate_key(ctx, param, value): + """The ``--key`` option must be specified when ``--cert`` is a file. + Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. + """ + cert = ctx.params.get("cert") + is_adhoc = cert == "adhoc" + + try: + import ssl + except ImportError: + is_context = False + else: + is_context = isinstance(cert, ssl.SSLContext) + + if value is not None: + if is_adhoc: + raise click.BadParameter( + 'When "--cert" is "adhoc", "--key" is not used.', ctx, param + ) + + if is_context: + raise click.BadParameter( + 'When "--cert" is an SSLContext object, "--key is not used.', ctx, param + ) + + if not cert: + raise click.BadParameter('"--cert" must also be specified.', ctx, param) + + ctx.params["cert"] = cert, value + + else: + if cert and not (is_adhoc or is_context): + raise click.BadParameter('Required when using "--cert".', ctx, param) + + return value + + +class SeparatedPathType(click.Path): + """Click option type that accepts a list of values separated by the + OS's path separator (``:``, ``;`` on Windows). Each value is + validated as a :class:`click.Path` type. + """ + + def convert(self, value, param, ctx): + items = self.split_envvar_value(value) + super_convert = super().convert + return [super_convert(item, param, ctx) for item in items] + + +@click.command("run", short_help="Run a development server.") +@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") +@click.option("--port", "-p", default=5000, help="The port to bind to.") +@click.option( + "--cert", + type=CertParamType(), + help="Specify a certificate file to use HTTPS.", + is_eager=True, +) +@click.option( + "--key", + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + callback=_validate_key, + expose_value=False, + help="The key file to use when specifying a certificate.", +) +@click.option( + "--reload/--no-reload", + default=None, + help="Enable or disable the reloader. By default the reloader " + "is active if debug is enabled.", +) +@click.option( + "--debugger/--no-debugger", + default=None, + help="Enable or disable the debugger. By default the debugger " + "is active if debug is enabled.", +) +@click.option( + "--with-threads/--without-threads", + default=True, + help="Enable or disable multithreading.", +) +@click.option( + "--extra-files", + default=None, + type=SeparatedPathType(), + help=( + "Extra files that trigger a reload on change. Multiple paths" + f" are separated by {os.path.pathsep!r}." + ), +) +@click.option( + "--exclude-patterns", + default=None, + type=SeparatedPathType(), + help=( + "Files matching these fnmatch patterns will not trigger a reload" + " on change. Multiple patterns are separated by" + f" {os.path.pathsep!r}." + ), +) +@pass_script_info +def run_command( + info, + host, + port, + reload, + debugger, + with_threads, + cert, + extra_files, + exclude_patterns, +): + """Run a local development server. + + This server is for development purposes only. It does not provide + the stability, security, or performance of production WSGI servers. + + The reloader and debugger are enabled by default with the '--debug' + option. + """ + try: + app = info.load_app() + except Exception as e: + if is_running_from_reloader(): + # When reloading, print out the error immediately, but raise + # it later so the debugger or server can handle it. + traceback.print_exc() + err = e + + def app(environ, start_response): + raise err from None + + else: + # When not reloading, raise the error immediately so the + # command fails. + raise e from None + + debug = get_debug_flag() + + if reload is None: + reload = debug + + if debugger is None: + debugger = debug + + show_server_banner(debug, info.app_import_path) + + run_simple( + host, + port, + app, + use_reloader=reload, + use_debugger=debugger, + threaded=with_threads, + ssl_context=cert, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + ) + + +run_command.params.insert(0, _debug_option) + + +@click.command("shell", short_help="Run a shell in the app context.") +@with_appcontext +def shell_command() -> None: + """Run an interactive Python shell in the context of a given + Flask application. The application will populate the default + namespace of this shell according to its configuration. + + This is useful for executing small snippets of management code + without having to manually configure the application. + """ + import code + + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f"App: {current_app.import_name}\n" + f"Instance: {current_app.instance_path}" + ) + ctx: dict = {} + + # Support the regular Python interpreter startup script if someone + # is using it. + startup = os.environ.get("PYTHONSTARTUP") + if startup and os.path.isfile(startup): + with open(startup) as f: + eval(compile(f.read(), startup, "exec"), ctx) + + ctx.update(current_app.make_shell_context()) + + # Site, customize, or startup script can set a hook to call when + # entering interactive mode. The default one sets up readline with + # tab and history completion. + interactive_hook = getattr(sys, "__interactivehook__", None) + + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + # rlcompleter uses __main__.__dict__ by default, which is + # flask.__main__. Use the shell context instead. + readline.set_completer(Completer(ctx).complete) + + interactive_hook() + + code.interact(banner=banner, local=ctx) + + +@click.command("routes", short_help="Show the routes for the app.") +@click.option( + "--sort", + "-s", + type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), + default="endpoint", + help=( + "Method to sort routes by. 'match' is the order that Flask will match routes" + " when dispatching a request." + ), +) +@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") +@with_appcontext +def routes_command(sort: str, all_methods: bool) -> None: + """Show all registered routes with endpoints and methods.""" + rules = list(current_app.url_map.iter_rules()) + + if not rules: + click.echo("No routes were registered.") + return + + ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} + host_matching = current_app.url_map.host_matching + has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) + rows = [] + + for rule in rules: + row = [ + rule.endpoint, + ", ".join(sorted((rule.methods or set()) - ignored_methods)), + ] + + if has_domain: + row.append((rule.host if host_matching else rule.subdomain) or "") + + row.append(rule.rule) + rows.append(row) + + headers = ["Endpoint", "Methods"] + sorts = ["endpoint", "methods"] + + if has_domain: + headers.append("Host" if host_matching else "Subdomain") + sorts.append("domain") + + headers.append("Rule") + sorts.append("rule") + + try: + rows.sort(key=itemgetter(sorts.index(sort))) + except ValueError: + pass + + rows.insert(0, headers) + widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] + rows.insert(1, ["-" * w for w in widths]) + template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) + + for row in rows: + click.echo(template.format(*row)) + + +cli = FlaskGroup( + name="flask", + help="""\ +A general utility script for Flask applications. + +An application to load must be given with the '--app' option, +'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file +in the current directory. +""", +) + + +def main() -> None: + cli.main() + + +if __name__ == "__main__": + main() diff --git a/lib/python3.12/site-packages/flask/config.py b/lib/python3.12/site-packages/flask/config.py new file mode 100644 index 0000000000000000000000000000000000000000..5f921b4dff57067cef833689a835c21e3454f845 --- /dev/null +++ b/lib/python3.12/site-packages/flask/config.py @@ -0,0 +1,347 @@ +from __future__ import annotations + +import errno +import json +import os +import types +import typing as t + +from werkzeug.utils import import_string + + +class ConfigAttribute: + """Makes an attribute forward to the config""" + + def __init__(self, name: str, get_converter: t.Callable | None = None) -> None: + self.__name__ = name + self.get_converter = get_converter + + def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any: + if obj is None: + return self + rv = obj.config[self.__name__] + if self.get_converter is not None: + rv = self.get_converter(rv) + return rv + + def __set__(self, obj: t.Any, value: t.Any) -> None: + obj.config[self.__name__] = value + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__( + self, root_path: str | os.PathLike, defaults: dict | None = None + ) -> None: + super().__init__(defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name: str, silent: bool = False) -> bool: + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError( + f"The environment variable {variable_name!r} is not set" + " and as such configuration could not be loaded. Set" + " this variable and make it point to a configuration" + " file" + ) + return self.from_pyfile(rv, silent=silent) + + def from_prefixed_env( + self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads + ) -> bool: + """Load any environment variables that start with ``FLASK_``, + dropping the prefix from the env key for the config key. Values + are passed through a loading function to attempt to convert them + to more specific types than strings. + + Keys are loaded in :func:`sorted` order. + + The default loading function attempts to parse values as any + valid JSON type, including dicts and lists. + + Specific items in nested dicts can be set by separating the + keys with double underscores (``__``). If an intermediate key + doesn't exist, it will be initialized to an empty dict. + + :param prefix: Load env vars that start with this prefix, + separated with an underscore (``_``). + :param loads: Pass each string value to this function and use + the returned value as the config value. If any error is + raised it is ignored and the value remains a string. The + default is :func:`json.loads`. + + .. versionadded:: 2.1 + """ + prefix = f"{prefix}_" + len_prefix = len(prefix) + + for key in sorted(os.environ): + if not key.startswith(prefix): + continue + + value = os.environ[key] + + try: + value = loads(value) + except Exception: + # Keep the value as a string if loading failed. + pass + + # Change to key.removeprefix(prefix) on Python >= 3.9. + key = key[len_prefix:] + + if "__" not in key: + # A non-nested key, set directly. + self[key] = value + continue + + # Traverse nested dictionaries with keys separated by "__". + current = self + *parts, tail = key.split("__") + + for part in parts: + # If an intermediate dict does not exist, create it. + if part not in current: + current[part] = {} + + current = current[part] + + current[tail] = value + + return True + + def from_pyfile(self, filename: str | os.PathLike, silent: bool = False) -> bool: + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = types.ModuleType("config") + d.__file__ = filename + try: + with open(filename, mode="rb") as config_file: + exec(compile(config_file.read(), filename, "exec"), d.__dict__) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): + return False + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + self.from_object(d) + return True + + def from_object(self, obj: object | str) -> None: + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. + + Example of module-based configuration:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + Nothing is done to the object before loading. If the object is a + class and has ``@property`` attributes, it needs to be + instantiated before being passed to this method. + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + + :param obj: an import name or object + """ + if isinstance(obj, str): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def from_file( + self, + filename: str | os.PathLike, + load: t.Callable[[t.IO[t.Any]], t.Mapping], + silent: bool = False, + text: bool = True, + ) -> bool: + """Update the values in the config from a file that is loaded + using the ``load`` parameter. The loaded data is passed to the + :meth:`from_mapping` method. + + .. code-block:: python + + import json + app.config.from_file("config.json", load=json.load) + + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) + + :param filename: The path to the data file. This can be an + absolute path or relative to the config root path. + :param load: A callable that takes a file handle and returns a + mapping of loaded data from the file. + :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` + implements a ``read`` method. + :param silent: Ignore the file if it doesn't exist. + :param text: Open the file in text or binary mode. + :return: ``True`` if the file was loaded successfully. + + .. versionchanged:: 2.3 + The ``text`` parameter was added. + + .. versionadded:: 2.0 + """ + filename = os.path.join(self.root_path, filename) + + try: + with open(filename, "r" if text else "rb") as f: + obj = load(f) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + + return self.from_mapping(obj) + + def from_mapping( + self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any + ) -> bool: + """Updates the config like :meth:`update` ignoring items with + non-upper keys. + + :return: Always returns ``True``. + + .. versionadded:: 0.11 + """ + mappings: dict[str, t.Any] = {} + if mapping is not None: + mappings.update(mapping) + mappings.update(kwargs) + for key, value in mappings.items(): + if key.isupper(): + self[key] = value + return True + + def get_namespace( + self, namespace: str, lowercase: bool = True, trim_namespace: bool = True + ) -> dict[str, t.Any]: + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store_config` would look like:: + + { + 'type': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + :param trim_namespace: a flag indicating if the keys of the resulting + dictionary should not include the namespace + + .. versionadded:: 0.11 + """ + rv = {} + for k, v in self.items(): + if not k.startswith(namespace): + continue + if trim_namespace: + key = k[len(namespace) :] + else: + key = k + if lowercase: + key = key.lower() + rv[key] = v + return rv + + def __repr__(self) -> str: + return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/lib/python3.12/site-packages/flask/ctx.py b/lib/python3.12/site-packages/flask/ctx.py new file mode 100644 index 0000000000000000000000000000000000000000..b37e4e04a6c69254ba0f755813fccbeab9715ba4 --- /dev/null +++ b/lib/python3.12/site-packages/flask/ctx.py @@ -0,0 +1,440 @@ +from __future__ import annotations + +import contextvars +import sys +import typing as t +from functools import update_wrapper +from types import TracebackType + +from werkzeug.exceptions import HTTPException + +from . import typing as ft +from .globals import _cv_app +from .globals import _cv_request +from .signals import appcontext_popped +from .signals import appcontext_pushed + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .sessions import SessionMixin + from .wrappers import Request + + +# a singleton sentinel value for parameter defaults +_sentinel = object() + + +class _AppCtxGlobals: + """A plain object. Used as a namespace for storing data during an + application context. + + Creating an app context automatically creates this object, which is + made available as the :data:`g` proxy. + + .. describe:: 'key' in g + + Check whether an attribute is present. + + .. versionadded:: 0.10 + + .. describe:: iter(g) + + Return an iterator over the attribute names. + + .. versionadded:: 0.10 + """ + + # Define attr methods to let mypy know this is a namespace object + # that has arbitrary attributes. + + def __getattr__(self, name: str) -> t.Any: + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + self.__dict__[name] = value + + def __delattr__(self, name: str) -> None: + try: + del self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def get(self, name: str, default: t.Any | None = None) -> t.Any: + """Get an attribute by name, or a default value. Like + :meth:`dict.get`. + + :param name: Name of attribute to get. + :param default: Value to return if the attribute is not present. + + .. versionadded:: 0.10 + """ + return self.__dict__.get(name, default) + + def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: + """Get and remove an attribute by name. Like :meth:`dict.pop`. + + :param name: Name of attribute to pop. + :param default: Value to return if the attribute is not present, + instead of raising a ``KeyError``. + + .. versionadded:: 0.11 + """ + if default is _sentinel: + return self.__dict__.pop(name) + else: + return self.__dict__.pop(name, default) + + def setdefault(self, name: str, default: t.Any = None) -> t.Any: + """Get the value of an attribute if it is present, otherwise + set and return a default value. Like :meth:`dict.setdefault`. + + :param name: Name of attribute to get. + :param default: Value to set and return if the attribute is not + present. + + .. versionadded:: 0.11 + """ + return self.__dict__.setdefault(name, default) + + def __contains__(self, item: str) -> bool: + return item in self.__dict__ + + def __iter__(self) -> t.Iterator[str]: + return iter(self.__dict__) + + def __repr__(self) -> str: + ctx = _cv_app.get(None) + if ctx is not None: + return f"" + return object.__repr__(self) + + +def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(response): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'after_this_request' can only be used when a request" + " context is active, such as in a view function." + ) + + ctx._after_request_functions.append(f) + return f + + +def copy_current_request_context(f: t.Callable) -> t.Callable: + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. The current session is also + included in the copied request context. + + Example:: + + import gevent + from flask import copy_current_request_context + + @app.route('/') + def index(): + @copy_current_request_context + def do_some_work(): + # do some work here, it can access flask.request or + # flask.session like you would otherwise in the view function. + ... + gevent.spawn(do_some_work) + return 'Regular response' + + .. versionadded:: 0.10 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." + ) + + ctx = ctx.copy() + + def wrapper(*args, **kwargs): + with ctx: + return ctx.app.ensure_sync(f)(*args, **kwargs) + + return update_wrapper(wrapper, f) + + +def has_request_context() -> bool: + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g`) for truthness:: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _cv_request.get(None) is not None + + +def has_app_context() -> bool: + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _cv_app.get(None) is not None + + +class AppContext: + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. + """ + + def __init__(self, app: Flask) -> None: + self.app = app + self.url_adapter = app.create_url_adapter(None) + self.g: _AppCtxGlobals = app.app_ctx_globals_class() + self._cv_tokens: list[contextvars.Token] = [] + + def push(self) -> None: + """Binds the app context to the current context.""" + self._cv_tokens.append(_cv_app.set(self)) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the app context.""" + try: + if len(self._cv_tokens) == 1: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) + + def __enter__(self) -> AppContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + +class RequestContext: + """The request context contains per-request information. The Flask + app creates and pushes it at the beginning of the request, then pops + it at the end of the request. It will create the URL adapter and + request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the + request. When using the interactive debugger, the context will be + restored so ``request`` is still accessible. Similarly, the test + client can preserve the context after the request ends. However, + teardown functions may already have closed some resources such as + database connections. + """ + + def __init__( + self, + app: Flask, + environ: dict, + request: Request | None = None, + session: SessionMixin | None = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + request.json_module = app.json + self.request: Request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes: list[tuple[str, str]] | None = None + self.session: SessionMixin | None = session + # Functions that should be executed after the request on the response + # object. These will be called before the regular "after_request" + # functions. + self._after_request_functions: list[ft.AfterRequestCallable] = [] + + self._cv_tokens: list[tuple[contextvars.Token, AppContext | None]] = [] + + def copy(self) -> RequestContext: + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: + app_ctx = self.app.app_context() + app_ctx.push() + else: + app_ctx = None + + self._cv_tokens.append((_cv_request.set(self), app_ctx)) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. + if self.url_adapter is not None: + self.match_request() + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + clear_request = len(self._cv_tokens) == 1 + + try: + if clear_request: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + finally: + ctx = _cv_request.get() + token, app_ctx = self._cv_tokens.pop() + _cv_request.reset(token) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + ctx.request.environ["werkzeug.request"] = None + + if app_ctx is not None: + app_ctx.pop(exc) + + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) + + def __enter__(self) -> RequestContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" + ) diff --git a/lib/python3.12/site-packages/flask/debughelpers.py b/lib/python3.12/site-packages/flask/debughelpers.py new file mode 100644 index 0000000000000000000000000000000000000000..6061441a89fbf984ff6414b0b597192e7bca5bbc --- /dev/null +++ b/lib/python3.12/site-packages/flask/debughelpers.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import typing as t + +from .app import Flask +from .blueprints import Blueprint +from .globals import request_ctx + + +class UnexpectedUnicodeError(AssertionError, UnicodeError): + """Raised in places where we want some better error reporting for + unexpected unicode or binary data. + """ + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request, key): + form_matches = request.form.getlist(key) + buf = [ + f"You tried to access the file {key!r} in the request.files" + " dictionary but it does not exist. The mimetype for the" + f" request is {request.mimetype!r} instead of" + " 'multipart/form-data' which means that no file contents" + " were transmitted. To fix this error you should provide" + ' enctype="multipart/form-data" in your form.' + ] + if form_matches: + names = ", ".join(repr(x) for x in form_matches) + buf.append( + "\n\nThe browser instead transmitted some file names. " + f"This was submitted: {names}" + ) + self.msg = "".join(buf) + + def __str__(self): + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised in debug mode if a routing redirect + would cause the browser to drop the method or body. This happens + when method is not GET, HEAD or OPTIONS and the status code is not + 307 or 308. + """ + + def __init__(self, request): + exc = request.routing_exception + buf = [ + f"A request was sent to '{request.url}', but routing issued" + f" a redirect to the canonical URL '{exc.new_url}'." + ] + + if f"{request.base_url}/" == exc.new_url.partition("?")[0]: + buf.append( + " The URL was defined with a trailing slash. Flask" + " will redirect to the URL with a trailing slash if it" + " was accessed without one." + ) + + buf.append( + " Send requests to the canonical URL, or use 307 or 308 for" + " routing redirects. Otherwise, browsers will drop form" + " data.\n\n" + "This exception is only raised in debug mode." + ) + super().__init__("".join(buf)) + + +def attach_enctype_error_multidict(request): + """Patch ``request.files.__getitem__`` to raise a descriptive error + about ``enctype=multipart/form-data``. + + :param request: The request to patch. + :meta private: + """ + oldcls = request.files.__class__ + + class newcls(oldcls): + def __getitem__(self, key): + try: + return super().__getitem__(key) + except KeyError as e: + if key not in request.form: + raise + + raise DebugFilesKeyError(request, key).with_traceback( + e.__traceback__ + ) from None + + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls + + +def _dump_loader_info(loader) -> t.Generator: + yield f"class: {type(loader).__module__}.{type(loader).__name__}" + for key, value in sorted(loader.__dict__.items()): + if key.startswith("_"): + continue + if isinstance(value, (tuple, list)): + if not all(isinstance(x, str) for x in value): + continue + yield f"{key}:" + for item in value: + yield f" - {item}" + continue + elif not isinstance(value, (str, int, float, bool)): + continue + yield f"{key}: {value!r}" + + +def explain_template_loading_attempts(app: Flask, template, attempts) -> None: + """This should help developers understand what failed""" + info = [f"Locating template {template!r}:"] + total_found = 0 + blueprint = None + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint + + for idx, (loader, srcobj, triple) in enumerate(attempts): + if isinstance(srcobj, Flask): + src_info = f"application {srcobj.import_name!r}" + elif isinstance(srcobj, Blueprint): + src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" + else: + src_info = repr(srcobj) + + info.append(f"{idx + 1:5}: trying loader of {src_info}") + + for line in _dump_loader_info(loader): + info.append(f" {line}") + + if triple is None: + detail = "no match" + else: + detail = f"found ({triple[1] or ''!r})" + total_found += 1 + info.append(f" -> {detail}") + + seems_fishy = False + if total_found == 0: + info.append("Error: the template could not be found.") + seems_fishy = True + elif total_found > 1: + info.append("Warning: multiple loaders returned a match for the template.") + seems_fishy = True + + if blueprint is not None and seems_fishy: + info.append( + " The template was looked up from an endpoint that belongs" + f" to the blueprint {blueprint!r}." + ) + info.append(" Maybe you did not place a template in the right folder?") + info.append(" See https://flask.palletsprojects.com/blueprints/#templates") + + app.logger.info("\n".join(info)) diff --git a/lib/python3.12/site-packages/flask/globals.py b/lib/python3.12/site-packages/flask/globals.py new file mode 100644 index 0000000000000000000000000000000000000000..e9cd4acfcda8a51dda20c604678fd706709070b2 --- /dev/null +++ b/lib/python3.12/site-packages/flask/globals.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import typing as t +from contextvars import ContextVar + +from werkzeug.local import LocalProxy + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .ctx import _AppCtxGlobals + from .ctx import AppContext + from .ctx import RequestContext + from .sessions import SessionMixin + from .wrappers import Request + + +class _FakeStack: + def __init__(self, name: str, cv: ContextVar[t.Any]) -> None: + self.name = name + self.cv = cv + + @property + def top(self) -> t.Any | None: + import warnings + + warnings.warn( + f"'_{self.name}_ctx_stack' is deprecated and will be removed in Flask 2.4." + f" Use 'g' to store data, or '{self.name}_ctx' to access the current" + " context.", + DeprecationWarning, + stacklevel=2, + ) + return self.cv.get(None) + + +_no_app_msg = """\ +Working outside of application context. + +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ +""" +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") +__app_ctx_stack = _FakeStack("app", _cv_app) +app_ctx: AppContext = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg +) +current_app: Flask = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg +) +g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg +) + +_no_req_msg = """\ +Working outside of request context. + +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") +__request_ctx_stack = _FakeStack("request", _cv_request) +request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] + _cv_request, unbound_message=_no_req_msg +) +request: Request = LocalProxy( # type: ignore[assignment] + _cv_request, "request", unbound_message=_no_req_msg +) +session: SessionMixin = LocalProxy( # type: ignore[assignment] + _cv_request, "session", unbound_message=_no_req_msg +) + + +def __getattr__(name: str) -> t.Any: + if name == "_app_ctx_stack": + import warnings + + warnings.warn( + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + + warnings.warn( + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + return __request_ctx_stack + + raise AttributeError(name) diff --git a/lib/python3.12/site-packages/flask/helpers.py b/lib/python3.12/site-packages/flask/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..284c36965b527553d55dcf058c51110483061134 --- /dev/null +++ b/lib/python3.12/site-packages/flask/helpers.py @@ -0,0 +1,701 @@ +from __future__ import annotations + +import importlib.util +import os +import socket +import sys +import typing as t +import warnings +from datetime import datetime +from functools import lru_cache +from functools import update_wrapper +from threading import RLock + +import werkzeug.utils +from werkzeug.exceptions import abort as _wz_abort +from werkzeug.utils import redirect as _wz_redirect + +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .globals import request_ctx +from .globals import session +from .signals import message_flashed + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.wrappers import Response as BaseResponse + from .wrappers import Response + + +def get_debug_flag() -> bool: + """Get whether debug mode should be enabled for the app, indicated by the + :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. + """ + val = os.environ.get("FLASK_DEBUG") + return bool(val and val.lower() not in {"0", "false", "no"}) + + +def get_load_dotenv(default: bool = True) -> bool: + """Get whether the user has disabled loading default dotenv files by + setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load + the files. + + :param default: What to return if the env var isn't set. + """ + val = os.environ.get("FLASK_SKIP_DOTENV") + + if not val: + return default + + return val.lower() in ("0", "false", "no") + + +def stream_with_context( + generator_or_function: ( + t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]] + ) +) -> t.Iterator[t.AnyStr]: + """Request contexts disappear when the response is started on the server. + This is done for efficiency reasons and to make it less likely to encounter + memory leaks with badly written WSGI middlewares. The downside is that if + you are using streamed responses, the generator cannot access request bound + information any more. + + This function however can help you keep the context around for longer:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + @stream_with_context + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(generate()) + + Alternatively it can also be used around a specific generator:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(stream_with_context(generate())) + + .. versionadded:: 0.9 + """ + try: + gen = iter(generator_or_function) # type: ignore + except TypeError: + + def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: + gen = generator_or_function(*args, **kwargs) # type: ignore + return stream_with_context(gen) + + return update_wrapper(decorator, generator_or_function) # type: ignore + + def generator() -> t.Generator: + ctx = _cv_request.get(None) + if ctx is None: + raise RuntimeError( + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." + ) + with ctx: + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. + try: + yield from gen + finally: + if hasattr(gen, "close"): + gen.close() + + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g + + +def make_response(*args: t.Any) -> Response: + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) # type: ignore + + +def url_for( + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, +) -> str: + """Generate a URL to the given endpoint with the given values. + + This requires an active request or application context, and calls + :meth:`current_app.url_for() `. See that method + for full documentation. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it is + external. + :param _external: If given, prefer the URL to be internal (False) or + require it to be external (True). External URLs include the + scheme and domain. When not in an active request, URLs are + external by default. + :param values: Values to use for the variable parts of the URL rule. + Unknown keys are appended as query string arguments, like + ``?a=b&c=d``. + + .. versionchanged:: 2.2 + Calls ``current_app.url_for``, allowing an app to override the + behavior. + + .. versionchanged:: 0.10 + The ``_scheme`` parameter was added. + + .. versionchanged:: 0.9 + The ``_anchor`` and ``_method`` parameters were added. + + .. versionchanged:: 0.9 + Calls ``app.handle_url_build_error`` on build errors. + """ + return current_app.url_for( + endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + ) + + +def redirect( + location: str, code: int = 302, Response: type[BaseResponse] | None = None +) -> BaseResponse: + """Create a redirect response object. + + If :data:`~flask.current_app` is available, it will use its + :meth:`~flask.Flask.redirect` method, otherwise it will use + :func:`werkzeug.utils.redirect`. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + :param Response: The response class to use. Not used when + ``current_app`` is active, which uses ``app.response_class``. + + .. versionadded:: 2.2 + Calls ``current_app.redirect`` if available instead of always + using Werkzeug's default ``redirect``. + """ + if current_app: + return current_app.redirect(location, code=code) + + return _wz_redirect(location, code=code, Response=Response) + + +def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. + + If :data:`~flask.current_app` is available, it will call its + :attr:`~flask.Flask.aborter` object, otherwise it will use + :func:`werkzeug.exceptions.abort`. + + :param code: The status code for the exception, which must be + registered in ``app.aborter``. + :param args: Passed to the exception. + :param kwargs: Passed to the exception. + + .. versionadded:: 2.2 + Calls ``current_app.aborter`` if available instead of always + using Werkzeug's default ``abort``. + """ + if current_app: + current_app.aborter(code, *args, **kwargs) + + _wz_abort(code, *args, **kwargs) + + +def get_template_attribute(template_name: str, attribute: str) -> t.Any: + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named :file:`_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to access + """ + return getattr(current_app.jinja_env.get_template(template_name).module, attribute) + + +def flash(message: str, category: str = "message") -> None: + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged:: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # always in sync with the session object, which is not true for session + # implementations that use external storage for keeping their keys/values. + flashes = session.get("_flashes", []) + flashes.append((category, message)) + session["_flashes"] = flashes + app = current_app._get_current_object() # type: ignore + message_flashed.send( + app, + _async_wrapper=app.ensure_sync, + message=message, + category=category, + ) + + +def get_flashed_messages( + with_categories: bool = False, category_filter: t.Iterable[str] = () +) -> list[str] | list[tuple[str, str]]: + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to ``True``, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Filter the flashed messages to one or more categories by providing those + categories in `category_filter`. This allows rendering categories in + separate html blocks. The `with_categories` and `category_filter` + arguments are distinct: + + * `with_categories` controls whether categories are returned with message + text (``True`` gives a tuple, where ``False`` gives just the message text). + * `category_filter` filters the messages down to only those matching the + provided categories. + + See :doc:`/patterns/flashing` for examples. + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + .. versionchanged:: 0.9 + `category_filter` parameter added. + + :param with_categories: set to ``True`` to also receive categories. + :param category_filter: filter of categories to limit return values. Only + categories in the list will be returned. + """ + flashes = request_ctx.flashes + if flashes is None: + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_ctx.flashes = flashes + if category_filter: + flashes = list(filter(lambda f: f[0] in category_filter, flashes)) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: + if kwargs.get("max_age") is None: + kwargs["max_age"] = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + use_x_sendfile=current_app.config["USE_X_SENDFILE"], + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore + ) + return kwargs + + +def send_file( + path_or_file: os.PathLike | str | t.BinaryIO, + mimetype: str | None = None, + as_attachment: bool = False, + download_name: str | None = None, + conditional: bool = True, + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, +) -> Response: + """Send the contents of a file to the client. + + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. + + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. Use :func:`send_from_directory` to safely serve + user-requested paths from within a directory. + + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, configuring Flask with + ``USE_X_SENDFILE = True`` will tell the server to send the given + path, which is much more efficient than reading it in Python. + + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. + + .. versionchanged:: 2.0 + ``download_name`` replaces the ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. + + .. versionchanged:: 2.0 + ``max_age`` replaces the ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. + + .. versionchanged:: 2.0 + ``etag`` replaces the ``add_etags`` parameter. It can be a + string to use instead of generating one. + + .. versionchanged:: 2.0 + Passing a file-like object that inherits from + :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather + than sending an empty file. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionchanged:: 1.1 + ``filename`` may be a :class:`~os.PathLike` object. + + .. versionchanged:: 1.1 + Passing a :class:`~io.BytesIO` object supports range requests. + + .. versionchanged:: 1.0.3 + Filenames are encoded with ASCII instead of Latin-1 for broader + compatibility with WSGI servers. + + .. versionchanged:: 1.0 + UTF-8 filenames as specified in :rfc:`2231` are supported. + + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file + objects. If you want to use automatic MIME and etag support, + pass a filename via ``filename_or_fp`` or + ``attachment_filename``. + + .. versionchanged:: 0.12 + ``attachment_filename`` is preferred over ``filename`` for MIME + detection. + + .. versionchanged:: 0.9 + ``cache_timeout`` defaults to + :meth:`Flask.get_send_file_max_age`. + + .. versionchanged:: 0.7 + MIME guessing and etag support for file-like objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. + + .. versionchanged:: 0.5 + The ``add_etags``, ``cache_timeout`` and ``conditional`` + parameters were added. The default behavior is to add etags. + + .. versionadded:: 0.2 + """ + return werkzeug.utils.send_file( # type: ignore[return-value] + **_prepare_send_file_kwargs( + path_or_file=path_or_file, + environ=request.environ, + mimetype=mimetype, + as_attachment=as_attachment, + download_name=download_name, + conditional=conditional, + etag=etag, + last_modified=last_modified, + max_age=max_age, + ) + ) + + +def send_from_directory( + directory: os.PathLike | str, + path: os.PathLike | str, + **kwargs: t.Any, +) -> Response: + """Send a file from within a directory using :func:`send_file`. + + .. code-block:: python + + @app.route("/uploads/") + def download_file(name): + return send_from_directory( + app.config['UPLOAD_FOLDER'], name, as_attachment=True + ) + + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. + + If the final path does not point to an existing regular file, + raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under, + relative to the current application's root path. + :param path: The path to the file to send, relative to + ``directory``. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionchanged:: 2.0 + ``path`` replaces the ``filename`` parameter. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionadded:: 0.5 + """ + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + directory, path, **_prepare_send_file_kwargs(**kwargs) + ) + + +def get_root_path(import_name: str) -> str: + """Find the root path of a package, or the path that contains a + module. If it cannot be found, returns the current working + directory. + + Not to be confused with the value returned by :func:`find_package`. + + :meta private: + """ + # Module already imported and has a file attribute. Use that first. + mod = sys.modules.get(import_name) + + if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: + return os.path.dirname(os.path.abspath(mod.__file__)) + + # Next attempt: check the loader. + try: + spec = importlib.util.find_spec(import_name) + + if spec is None: + raise ValueError + except (ImportError, ValueError): + loader = None + else: + loader = spec.loader + + # Loader does not exist or we're referring to an unloaded main + # module or a main module without path (interactive sessions), go + # with the current working directory. + if loader is None: + return os.getcwd() + + if hasattr(loader, "get_filename"): + filepath = loader.get_filename(import_name) + else: + # Fall back to imports. + __import__(import_name) + mod = sys.modules[import_name] + filepath = getattr(mod, "__file__", None) + + # If we don't have a file path it might be because it is a + # namespace package. In this case pick the root path from the + # first module that is contained in the package. + if filepath is None: + raise RuntimeError( + "No root path can be found for the provided module" + f" {import_name!r}. This can happen because the module" + " came from an import hook that does not provide file" + " name information or because it's a namespace package." + " In this case the root path needs to be explicitly" + " provided." + ) + + # filepath is import_name.py for a module, or __init__.py for a package. + return os.path.dirname(os.path.abspath(filepath)) + + +class locked_cached_property(werkzeug.utils.cached_property): + """A :func:`property` that is only evaluated once. Like + :class:`werkzeug.utils.cached_property` except access uses a lock + for thread safety. + + .. deprecated:: 2.3 + Will be removed in Flask 2.4. Use a lock inside the decorated function if + locking is needed. + + .. versionchanged:: 2.0 + Inherits from Werkzeug's ``cached_property`` (and ``property``). + """ + + def __init__( + self, + fget: t.Callable[[t.Any], t.Any], + name: str | None = None, + doc: str | None = None, + ) -> None: + import warnings + + warnings.warn( + "'locked_cached_property' is deprecated and will be removed in Flask 2.4." + " Use a lock inside the decorated function if locking is needed.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(fget, name=name, doc=doc) + self.lock = RLock() + + def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore + if obj is None: + return self + + with self.lock: + return super().__get__(obj, type=type) + + def __set__(self, obj: object, value: t.Any) -> None: + with self.lock: + super().__set__(obj, value) + + def __delete__(self, obj: object) -> None: + with self.lock: + super().__delete__(obj) + + +def is_ip(value: str) -> bool: + """Determine if the given string is an IP address. + + :param value: value to check + :type value: str + + :return: True if string is an IP address + :rtype: bool + + .. deprecated:: 2.3 + Will be removed in Flask 2.4. + """ + warnings.warn( + "The 'is_ip' function is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + + for family in (socket.AF_INET, socket.AF_INET6): + try: + socket.inet_pton(family, value) + except OSError: + pass + else: + return True + + return False + + +@lru_cache(maxsize=None) +def _split_blueprint_path(name: str) -> list[str]: + out: list[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/lib/python3.12/site-packages/flask/json/__init__.py b/lib/python3.12/site-packages/flask/json/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f15296fed0fbf3aa4157429aed9266b803370e3d --- /dev/null +++ b/lib/python3.12/site-packages/flask/json/__init__.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json as _json +import typing as t + +from ..globals import current_app +from .provider import _default + +if t.TYPE_CHECKING: # pragma: no cover + from ..wrappers import Response + + +def dumps(obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dumps() ` + method, otherwise it will use :func:`json.dumps`. + + :param obj: The data to serialize. + :param kwargs: Arguments passed to the ``dumps`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dumps``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.dumps(obj, **kwargs) + + kwargs.setdefault("default", _default) + return _json.dumps(obj, **kwargs) + + +def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dump() ` + method, otherwise it will use :func:`json.dump`. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: Arguments passed to the ``dump`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dump``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + Writing to a binary file, and the ``encoding`` argument, will be + removed in Flask 2.1. + """ + if current_app: + current_app.json.dump(obj, fp, **kwargs) + else: + kwargs.setdefault("default", _default) + _json.dump(obj, fp, **kwargs) + + +def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.loads() ` + method, otherwise it will use :func:`json.loads`. + + :param s: Text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``loads`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.loads``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The data must be a + string or UTF-8 bytes. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.loads(s, **kwargs) + + return _json.loads(s, **kwargs) + + +def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.load() ` + method, otherwise it will use :func:`json.load`. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``load`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.load``, allowing an app to override + the behavior. + + .. versionchanged:: 2.2 + The ``app`` parameter will be removed in Flask 2.3. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The file must be text + mode, or binary mode with UTF-8 bytes. + """ + if current_app: + return current_app.json.load(fp, **kwargs) + + return _json.load(fp, **kwargs) + + +def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. A dict or list returned from a view will be converted to a + JSON response automatically without needing to call this. + + This requires an active request or application context, and calls + :meth:`app.json.response() `. + + In debug mode, the output is formatted with indentation to make it + easier to read. This may also be controlled by the provider. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + + .. versionchanged:: 2.2 + Calls ``current_app.json.response``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 0.11 + Added support for serializing top-level arrays. This was a + security risk in ancient browsers. See :ref:`security-json`. + + .. versionadded:: 0.2 + """ + return current_app.json.response(*args, **kwargs) diff --git a/lib/python3.12/site-packages/flask/json/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/flask/json/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..674da725ed7faa33d17d649349050f71ee6f4556 Binary files /dev/null and b/lib/python3.12/site-packages/flask/json/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/json/__pycache__/provider.cpython-312.pyc b/lib/python3.12/site-packages/flask/json/__pycache__/provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5863420195b1178d4983fc27287cc4b44947330 Binary files /dev/null and b/lib/python3.12/site-packages/flask/json/__pycache__/provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/json/__pycache__/tag.cpython-312.pyc b/lib/python3.12/site-packages/flask/json/__pycache__/tag.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91ccf04a9c039dc32833491bd2731d4a69fad55f Binary files /dev/null and b/lib/python3.12/site-packages/flask/json/__pycache__/tag.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/flask/json/provider.py b/lib/python3.12/site-packages/flask/json/provider.py new file mode 100644 index 0000000000000000000000000000000000000000..0edd3d58d7d9ca85e5fc216b24da0eacc16d47ec --- /dev/null +++ b/lib/python3.12/site-packages/flask/json/provider.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import dataclasses +import decimal +import json +import typing as t +import uuid +import weakref +from datetime import date + +from werkzeug.http import http_date + +if t.TYPE_CHECKING: # pragma: no cover + from ..app import Flask + from ..wrappers import Response + + +class JSONProvider: + """A standard set of JSON operations for an application. Subclasses + of this can be used to customize JSON behavior or use different + JSON libraries. + + To implement a provider for a specific library, subclass this base + class and implement at least :meth:`dumps` and :meth:`loads`. All + other methods have default implementations. + + To use a different provider, either subclass ``Flask`` and set + :attr:`~flask.Flask.json_provider_class` to a provider class, or set + :attr:`app.json ` to an instance of the class. + + :param app: An application instance. This will be stored as a + :class:`weakref.proxy` on the :attr:`_app` attribute. + + .. versionadded:: 2.2 + """ + + def __init__(self, app: Flask) -> None: + self._app = weakref.proxy(app) + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + :param obj: The data to serialize. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: May be passed to the underlying JSON library. + """ + fp.write(self.dumps(obj, **kwargs)) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + :param s: Text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + return self.loads(fp.read(), **kwargs) + + def _prepare_response_obj( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> t.Any: + if args and kwargs: + raise TypeError("app.json.response() takes either args or kwargs, not both") + + if not args and not kwargs: + return None + + if len(args) == 1: + return args[0] + + return args or kwargs + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. + + The :func:`~flask.json.jsonify` function calls this method for + the current application. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + return self._app.response_class(self.dumps(obj), mimetype="application/json") + + +def _default(o: t.Any) -> t.Any: + if isinstance(o, date): + return http_date(o) + + if isinstance(o, (decimal.Decimal, uuid.UUID)): + return str(o) + + if dataclasses and dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + + if hasattr(o, "__html__"): + return str(o.__html__()) + + raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") + + +class DefaultJSONProvider(JSONProvider): + """Provide JSON operations using Python's built-in :mod:`json` + library. Serializes the following additional data types: + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + """ + + default: t.Callable[[t.Any], t.Any] = staticmethod( + _default + ) # type: ignore[assignment] + """Apply this function to any object that :meth:`json.dumps` does + not know how to serialize. It should return a valid JSON type or + raise a ``TypeError``. + """ + + ensure_ascii = True + """Replace non-ASCII characters with escape sequences. This may be + more compatible with some clients, but can be disabled for better + performance and size. + """ + + sort_keys = True + """Sort the keys in any serialized dicts. This may be useful for + some caching situations, but can be disabled for better performance. + When enabled, keys must all be strings, they are not converted + before sorting. + """ + + compact: bool | None = None + """If ``True``, or ``None`` out of debug mode, the :meth:`response` + output will not add indentation, newlines, or spaces. If ``False``, + or ``None`` in debug mode, it will use a non-compact representation. + """ + + mimetype = "application/json" + """The mimetype set in :meth:`response`.""" + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON to a string. + + Keyword arguments are passed to :func:`json.dumps`. Sets some + parameter defaults from the :attr:`default`, + :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. + + :param obj: The data to serialize. + :param kwargs: Passed to :func:`json.dumps`. + """ + kwargs.setdefault("default", self.default) + kwargs.setdefault("ensure_ascii", self.ensure_ascii) + kwargs.setdefault("sort_keys", self.sort_keys) + return json.dumps(obj, **kwargs) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON from a string or bytes. + + :param s: Text or UTF-8 bytes. + :param kwargs: Passed to :func:`json.loads`. + """ + return json.loads(s, **kwargs) + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with it. The response mimetype + will be "application/json" and can be changed with + :attr:`mimetype`. + + If :attr:`compact` is ``False`` or debug mode is enabled, the + output will be formatted to be easier to read. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + dump_args: dict[str, t.Any] = {} + + if (self.compact is None and self._app.debug) or self.compact is False: + dump_args.setdefault("indent", 2) + else: + dump_args.setdefault("separators", (",", ":")) + + return self._app.response_class( + f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype + ) diff --git a/lib/python3.12/site-packages/flask/json/tag.py b/lib/python3.12/site-packages/flask/json/tag.py new file mode 100644 index 0000000000000000000000000000000000000000..91cc4412d69b22bedc260d3cf598d135d706ed8f --- /dev/null +++ b/lib/python3.12/site-packages/flask/json/tag.py @@ -0,0 +1,314 @@ +""" +Tagged JSON +~~~~~~~~~~~ + +A compact representation for lossless serialization of non-standard JSON +types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this +to serialize the session data, but it may be useful in other places. It +can be extended to support other types. + +.. autoclass:: TaggedJSONSerializer + :members: + +.. autoclass:: JSONTag + :members: + +Let's see an example that adds support for +:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so +to handle this we will dump the items as a list of ``[key, value]`` +pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to +identify the type. The session serializer processes dicts first, so +insert the new tag at the front of the order since ``OrderedDict`` must +be processed before ``dict``. + +.. code-block:: python + + from flask.json.tag import JSONTag + + class TagOrderedDict(JSONTag): + __slots__ = ('serializer',) + key = ' od' + + def check(self, value): + return isinstance(value, OrderedDict) + + def to_json(self, value): + return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] + + def to_python(self, value): + return OrderedDict(value) + + app.session_interface.serializer.register(TagOrderedDict, index=0) +""" +from __future__ import annotations + +import typing as t +from base64 import b64decode +from base64 import b64encode +from datetime import datetime +from uuid import UUID + +from markupsafe import Markup +from werkzeug.http import http_date +from werkzeug.http import parse_date + +from ..json import dumps +from ..json import loads + + +class JSONTag: + """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" + + __slots__ = ("serializer",) + + #: The tag to mark the serialized object with. If ``None``, this tag is + #: only used as an intermediate step during tagging. + key: str | None = None + + def __init__(self, serializer: TaggedJSONSerializer) -> None: + """Create a tagger for the given serializer.""" + self.serializer = serializer + + def check(self, value: t.Any) -> bool: + """Check if the given value should be tagged by this tag.""" + raise NotImplementedError + + def to_json(self, value: t.Any) -> t.Any: + """Convert the Python object to an object that is a valid JSON type. + The tag will be added later.""" + raise NotImplementedError + + def to_python(self, value: t.Any) -> t.Any: + """Convert the JSON representation back to the correct type. The tag + will already be removed.""" + raise NotImplementedError + + def tag(self, value: t.Any) -> t.Any: + """Convert the value to a valid JSON type and add the tag structure + around it.""" + return {self.key: self.to_json(value)} + + +class TagDict(JSONTag): + """Tag for 1-item dicts whose only key matches a registered tag. + + Internally, the dict key is suffixed with `__`, and the suffix is removed + when deserializing. + """ + + __slots__ = () + key = " di" + + def check(self, value: t.Any) -> bool: + return ( + isinstance(value, dict) + and len(value) == 1 + and next(iter(value)) in self.serializer.tags + ) + + def to_json(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {f"{key}__": self.serializer.tag(value[key])} + + def to_python(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {key[:-2]: value[key]} + + +class PassDict(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, dict) + + def to_json(self, value: t.Any) -> t.Any: + # JSON objects may only have string keys, so don't bother tagging the + # key here. + return {k: self.serializer.tag(v) for k, v in value.items()} + + tag = to_json + + +class TagTuple(JSONTag): + __slots__ = () + key = " t" + + def check(self, value: t.Any) -> bool: + return isinstance(value, tuple) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + def to_python(self, value: t.Any) -> t.Any: + return tuple(value) + + +class PassList(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, list) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + tag = to_json + + +class TagBytes(JSONTag): + __slots__ = () + key = " b" + + def check(self, value: t.Any) -> bool: + return isinstance(value, bytes) + + def to_json(self, value: t.Any) -> t.Any: + return b64encode(value).decode("ascii") + + def to_python(self, value: t.Any) -> t.Any: + return b64decode(value) + + +class TagMarkup(JSONTag): + """Serialize anything matching the :class:`~markupsafe.Markup` API by + having a ``__html__`` method to the result of that method. Always + deserializes to an instance of :class:`~markupsafe.Markup`.""" + + __slots__ = () + key = " m" + + def check(self, value: t.Any) -> bool: + return callable(getattr(value, "__html__", None)) + + def to_json(self, value: t.Any) -> t.Any: + return str(value.__html__()) + + def to_python(self, value: t.Any) -> t.Any: + return Markup(value) + + +class TagUUID(JSONTag): + __slots__ = () + key = " u" + + def check(self, value: t.Any) -> bool: + return isinstance(value, UUID) + + def to_json(self, value: t.Any) -> t.Any: + return value.hex + + def to_python(self, value: t.Any) -> t.Any: + return UUID(value) + + +class TagDateTime(JSONTag): + __slots__ = () + key = " d" + + def check(self, value: t.Any) -> bool: + return isinstance(value, datetime) + + def to_json(self, value: t.Any) -> t.Any: + return http_date(value) + + def to_python(self, value: t.Any) -> t.Any: + return parse_date(value) + + +class TaggedJSONSerializer: + """Serializer that uses a tag system to compactly represent objects that + are not JSON types. Passed as the intermediate serializer to + :class:`itsdangerous.Serializer`. + + The following extra types are supported: + + * :class:`dict` + * :class:`tuple` + * :class:`bytes` + * :class:`~markupsafe.Markup` + * :class:`~uuid.UUID` + * :class:`~datetime.datetime` + """ + + __slots__ = ("tags", "order") + + #: Tag classes to bind when creating the serializer. Other tags can be + #: added later using :meth:`~register`. + default_tags = [ + TagDict, + PassDict, + TagTuple, + PassList, + TagBytes, + TagMarkup, + TagUUID, + TagDateTime, + ] + + def __init__(self) -> None: + self.tags: dict[str, JSONTag] = {} + self.order: list[JSONTag] = [] + + for cls in self.default_tags: + self.register(cls) + + def register( + self, + tag_class: type[JSONTag], + force: bool = False, + index: int | None = None, + ) -> None: + """Register a new tag with this serializer. + + :param tag_class: tag class to register. Will be instantiated with this + serializer instance. + :param force: overwrite an existing tag. If false (default), a + :exc:`KeyError` is raised. + :param index: index to insert the new tag in the tag order. Useful when + the new tag is a special case of an existing tag. If ``None`` + (default), the tag is appended to the end of the order. + + :raise KeyError: if the tag key is already registered and ``force`` is + not true. + """ + tag = tag_class(self) + key = tag.key + + if key is not None: + if not force and key in self.tags: + raise KeyError(f"Tag '{key}' is already registered.") + + self.tags[key] = tag + + if index is None: + self.order.append(tag) + else: + self.order.insert(index, tag) + + def tag(self, value: t.Any) -> dict[str, t.Any]: + """Convert a value to a tagged representation if necessary.""" + for tag in self.order: + if tag.check(value): + return tag.tag(value) + + return value + + def untag(self, value: dict[str, t.Any]) -> t.Any: + """Convert a tagged representation back to the original type.""" + if len(value) != 1: + return value + + key = next(iter(value)) + + if key not in self.tags: + return value + + return self.tags[key].to_python(value[key]) + + def dumps(self, value: t.Any) -> str: + """Tag the value and dump it to a compact JSON string.""" + return dumps(self.tag(value), separators=(",", ":")) + + def loads(self, value: str) -> t.Any: + """Load data from a JSON string and deserialized any tagged objects.""" + return loads(value, object_hook=self.untag) diff --git a/lib/python3.12/site-packages/flask/logging.py b/lib/python3.12/site-packages/flask/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..99f6be85be8cf576729086d47e79d44aec53d6f5 --- /dev/null +++ b/lib/python3.12/site-packages/flask/logging.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import logging +import sys +import typing as t + +from werkzeug.local import LocalProxy + +from .globals import request + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + + +@LocalProxy +def wsgi_errors_stream() -> t.TextIO: + """Find the most appropriate error stream for the application. If a request + is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. + + If you configure your own :class:`logging.StreamHandler`, you may want to + use this for the stream. If you are using file or dict configuration and + can't import this directly, you can refer to it as + ``ext://flask.logging.wsgi_errors_stream``. + """ + return request.environ["wsgi.errors"] if request else sys.stderr + + +def has_level_handler(logger: logging.Logger) -> bool: + """Check if there is a handler in the logging chain that will handle the + given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. + """ + level = logger.getEffectiveLevel() + current = logger + + while current: + if any(handler.level <= level for handler in current.handlers): + return True + + if not current.propagate: + break + + current = current.parent # type: ignore + + return False + + +#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format +#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. +default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore +default_handler.setFormatter( + logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") +) + + +def create_logger(app: Flask) -> logging.Logger: + """Get the Flask app's logger and configure it if needed. + + The logger name will be the same as + :attr:`app.import_name `. + + When :attr:`~flask.Flask.debug` is enabled, set the logger level to + :data:`logging.DEBUG` if it is not set. + + If there is no handler for the logger's effective level, add a + :class:`~logging.StreamHandler` for + :func:`~flask.logging.wsgi_errors_stream` with a basic format. + """ + logger = logging.getLogger(app.name) + + if app.debug and not logger.level: + logger.setLevel(logging.DEBUG) + + if not has_level_handler(logger): + logger.addHandler(default_handler) + + return logger diff --git a/lib/python3.12/site-packages/flask/py.typed b/lib/python3.12/site-packages/flask/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/flask/scaffold.py b/lib/python3.12/site-packages/flask/scaffold.py new file mode 100644 index 0000000000000000000000000000000000000000..d15b87338d7da90229dc6b9bdc5b684d71b5fb1b --- /dev/null +++ b/lib/python3.12/site-packages/flask/scaffold.py @@ -0,0 +1,873 @@ +from __future__ import annotations + +import importlib.util +import os +import pathlib +import sys +import typing as t +from collections import defaultdict +from datetime import timedelta +from functools import update_wrapper + +from jinja2 import FileSystemLoader +from werkzeug.exceptions import default_exceptions +from werkzeug.exceptions import HTTPException +from werkzeug.utils import cached_property + +from . import typing as ft +from .cli import AppGroup +from .globals import current_app +from .helpers import get_root_path +from .helpers import send_from_directory +from .templating import _default_template_ctx_processor + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + +# a singleton sentinel value for parameter defaults +_sentinel = object() + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) +T_route = t.TypeVar("T_route", bound=ft.RouteCallable) + + +def setupmethod(f: F) -> F: + f_name = f.__name__ + + def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + self._check_setup_finished(f_name) + return f(self, *args, **kwargs) + + return t.cast(F, update_wrapper(wrapper_func, f)) + + +class Scaffold: + """Common behavior shared between :class:`~flask.Flask` and + :class:`~flask.blueprints.Blueprint`. + + :param import_name: The import name of the module where this object + is defined. Usually :attr:`__name__` should be used. + :param static_folder: Path to a folder of static files to serve. + If this is set, a static route will be added. + :param static_url_path: URL prefix for the static route. + :param template_folder: Path to a folder containing template files. + for rendering. If this is set, a Jinja loader will be added. + :param root_path: The path that static, template, and resource files + are relative to. Typically not set, it is discovered based on + the ``import_name``. + + .. versionadded:: 2.0 + """ + + name: str + _static_folder: str | None = None + _static_url_path: str | None = None + + def __init__( + self, + import_name: str, + static_folder: str | os.PathLike | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike | None = None, + root_path: str | None = None, + ): + #: The name of the package or module that this object belongs + #: to. Do not change this once it is set by the constructor. + self.import_name = import_name + + self.static_folder = static_folder # type: ignore + self.static_url_path = static_url_path + + #: The path to the templates folder, relative to + #: :attr:`root_path`, to add to the template loader. ``None`` if + #: templates should not be added. + self.template_folder = template_folder + + if root_path is None: + root_path = get_root_path(self.import_name) + + #: Absolute path to the package on the filesystem. Used to look + #: up resources contained in the package. + self.root_path = root_path + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = AppGroup() + + #: A dictionary mapping endpoint names to view functions. + #: + #: To register a view function, use the :meth:`route` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.view_functions: dict[str, t.Callable] = {} + + #: A data structure of registered error handlers, in the format + #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is + #: the name of a blueprint the handlers are active for, or + #: ``None`` for all requests. The ``code`` key is the HTTP + #: status code for ``HTTPException``, or ``None`` for + #: other exceptions. The innermost dictionary maps exception + #: classes to handler functions. + #: + #: To register an error handler, use the :meth:`errorhandler` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.error_handler_spec: dict[ + ft.AppOrBlueprintKey, + dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], + ] = defaultdict(lambda: defaultdict(dict)) + + #: A data structure of functions to call at the beginning of + #: each request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`before_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.before_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`after_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.after_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.AfterRequestCallable] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request even if an exception is raised, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`teardown_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.teardown_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.TeardownCallable] + ] = defaultdict(list) + + #: A data structure of functions to call to pass extra context + #: values when rendering templates, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`context_processor` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.template_context_processors: dict[ + ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] + ] = defaultdict(list, {None: [_default_template_ctx_processor]}) + + #: A data structure of functions to call to modify the keyword + #: arguments passed to the view function, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the + #: :meth:`url_value_preprocessor` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_value_preprocessors: dict[ + ft.AppOrBlueprintKey, + list[ft.URLValuePreprocessorCallable], + ] = defaultdict(list) + + #: A data structure of functions to call to modify the keyword + #: arguments when generating URLs, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`url_defaults` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_default_functions: dict[ + ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] + ] = defaultdict(list) + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.name!r}>" + + def _check_setup_finished(self, f_name: str) -> None: + raise NotImplementedError + + @property + def static_folder(self) -> str | None: + """The absolute path to the configured static folder. ``None`` + if no static folder is set. + """ + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + else: + return None + + @static_folder.setter + def static_folder(self, value: str | os.PathLike | None) -> None: + if value is not None: + value = os.fspath(value).rstrip(r"\/") + + self._static_folder = value + + @property + def has_static_folder(self) -> bool: + """``True`` if :attr:`static_folder` is set. + + .. versionadded:: 0.5 + """ + return self.static_folder is not None + + @property + def static_url_path(self) -> str | None: + """The URL prefix that the static route will be accessible from. + + If it was not configured during init, it is derived from + :attr:`static_folder`. + """ + if self._static_url_path is not None: + return self._static_url_path + + if self.static_folder is not None: + basename = os.path.basename(self.static_folder) + return f"/{basename}".rstrip("/") + + return None + + @static_url_path.setter + def static_url_path(self, value: str | None) -> None: + if value is not None: + value = value.rstrip("/") + + self._static_url_path = value + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + .. versionadded:: 0.5 + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + @cached_property + def jinja_loader(self) -> FileSystemLoader | None: + """The Jinja loader for this object's templates. By default this + is a class :class:`jinja2.loaders.FileSystemLoader` to + :attr:`template_folder` if it is set. + + .. versionadded:: 0.5 + """ + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) + else: + return None + + def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for + reading. + + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: + + .. code-block:: python + + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) + + :param resource: Path to the resource relative to + :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is + supported, valid values are "r" (or "rt") and "rb". + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) + + def _method_route( + self, + method: str, + rule: str, + options: dict, + ) -> t.Callable[[T_route], T_route]: + if "methods" in options: + raise TypeError("Use the 'route' decorator to use the 'methods' argument.") + + return self.route(rule, methods=[method], **options) + + @setupmethod + def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["GET"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("GET", rule, options) + + @setupmethod + def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["POST"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("POST", rule, options) + + @setupmethod + def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PUT"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PUT", rule, options) + + @setupmethod + def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["DELETE"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("DELETE", rule, options) + + @setupmethod + def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PATCH"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PATCH", rule, options) + + @setupmethod + def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Decorate a view function to register it with the given URL + rule and options. Calls :meth:`add_url_rule`, which has more + details about the implementation. + + .. code-block:: python + + @app.route("/") + def index(): + return "Hello, World!" + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and + ``OPTIONS`` are added automatically. + + :param rule: The URL rule string. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + + def decorator(f: T_route) -> T_route: + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) + return f + + return decorator + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a rule for routing incoming requests and building + URLs. The :meth:`route` decorator is a shortcut to call this + with the ``view_func`` argument. These are equivalent: + + .. code-block:: python + + @app.route("/") + def index(): + ... + + .. code-block:: python + + def index(): + ... + + app.add_url_rule("/", view_func=index) + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. An error + will be raised if a function has already been registered for the + endpoint. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is + always added automatically, and ``OPTIONS`` is added + automatically by default. + + ``view_func`` does not necessarily need to be passed, but if the + rule should participate in routing an endpoint name must be + associated with a view function at some point with the + :meth:`endpoint` decorator. + + .. code-block:: python + + app.add_url_rule("/", endpoint="index") + + @app.endpoint("index") + def index(): + ... + + If ``view_func`` has a ``required_methods`` attribute, those + methods are added to the passed and automatic methods. If it + has a ``provide_automatic_methods`` attribute, it is used as the + default if the parameter is not passed. + + :param rule: The URL rule string. + :param endpoint: The endpoint name to associate with the rule + and view function. Used when routing and building URLs. + Defaults to ``view_func.__name__``. + :param view_func: The view function to associate with the + endpoint name. + :param provide_automatic_options: Add the ``OPTIONS`` method and + respond to ``OPTIONS`` requests automatically. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + raise NotImplementedError + + @setupmethod + def endpoint(self, endpoint: str) -> t.Callable[[F], F]: + """Decorate a view function to register it for the given + endpoint. Used if a rule is added without a ``view_func`` with + :meth:`add_url_rule`. + + .. code-block:: python + + app.add_url_rule("/ex", endpoint="example") + + @app.endpoint("example") + def example(): + ... + + :param endpoint: The endpoint name to associate with the view + function. + """ + + def decorator(f: F) -> F: + self.view_functions[endpoint] = f + return f + + return decorator + + @setupmethod + def before_request(self, f: T_before_request) -> T_before_request: + """Register a function to run before each request. + + For example, this can be used to open a database connection, or + to load the logged in user from the session. + + .. code-block:: python + + @app.before_request + def load_user(): + if "user_id" in session: + g.user = db.session.get(session["user_id"]) + + The function will be called without any arguments. If it returns + a non-``None`` value, the value is handled as if it was the + return value from the view, and further request handling is + stopped. + + This is available on both app and blueprint objects. When used on an app, this + executes before every request. When used on a blueprint, this executes before + every request that the blueprint handles. To register with a blueprint and + execute before every request, use :meth:`.Blueprint.before_app_request`. + """ + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def after_request(self, f: T_after_request) -> T_after_request: + """Register a function to run after each request to this object. + + The function is called with the response object, and must return + a response object. This allows the functions to modify or + replace the response before it is sent. + + If a function raises an exception, any remaining + ``after_request`` functions will not be called. Therefore, this + should not be used for actions that must execute, such as to + close resources. Use :meth:`teardown_request` for that. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.after_app_request`. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f: T_teardown) -> T_teardown: + """Register a function to be called when the request context is + popped. Typically this happens at the end of each request, but + contexts may be pushed manually as well during testing. + + .. code-block:: python + + with app.test_request_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the request context is + made inactive. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.teardown_app_request`. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor( + self, + f: T_template_context_processor, + ) -> T_template_context_processor: + """Registers a template context processor function. These functions run before + rendering a template. The keys of the returned dict are added as variables + available in the template. + + This is available on both app and blueprint objects. When used on an app, this + is called for every rendered template. When used on a blueprint, this is called + for templates rendered from the blueprint's views. To register with a blueprint + and affect every template, use :meth:`.Blueprint.app_context_processor`. + """ + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor( + self, + f: T_url_value_preprocessor, + ) -> T_url_value_preprocessor: + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_value_preprocessor`. + """ + self.url_value_preprocessors[None].append(f) + return f + + @setupmethod + def url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_defaults`. + """ + self.url_default_functions[None].append(f) + return f + + @setupmethod + def errorhandler( + self, code_or_exception: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + This is available on both app and blueprint objects. When used on an app, this + can handle errors from every request. When used on a blueprint, this can handle + errors from requests that the blueprint handles. To register with a blueprint + and affect every request, use :meth:`.Blueprint.app_errorhandler`. + + .. versionadded:: 0.7 + Use :meth:`register_error_handler` instead of modifying + :attr:`error_handler_spec` directly, for application wide error + handlers. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception + """ + + def decorator(f: T_error_handler) -> T_error_handler: + self.register_error_handler(code_or_exception, f) + return f + + return decorator + + @setupmethod + def register_error_handler( + self, + code_or_exception: type[Exception] | int, + f: ft.ErrorHandlerCallable, + ) -> None: + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + exc_class, code = self._get_exc_class_and_code(code_or_exception) + self.error_handler_spec[None][code][exc_class] = f + + @staticmethod + def _get_exc_class_and_code( + exc_class_or_code: type[Exception] | int, + ) -> tuple[type[Exception], int | None]: + """Get the exception class being handled. For HTTP status codes + or ``HTTPException`` subclasses, return both the exception and + status code. + + :param exc_class_or_code: Any exception class, or an HTTP status + code as an integer. + """ + exc_class: type[Exception] + + if isinstance(exc_class_or_code, int): + try: + exc_class = default_exceptions[exc_class_or_code] + except KeyError: + raise ValueError( + f"'{exc_class_or_code}' is not a recognized HTTP" + " error code. Use a subclass of HTTPException with" + " that code instead." + ) from None + else: + exc_class = exc_class_or_code + + if isinstance(exc_class, Exception): + raise TypeError( + f"{exc_class!r} is an instance, not a class. Handlers" + " can only be registered for Exception classes or HTTP" + " error codes." + ) + + if not issubclass(exc_class, Exception): + raise ValueError( + f"'{exc_class.__name__}' is not a subclass of Exception." + " Handlers can only be registered for Exception classes" + " or HTTP error codes." + ) + + if issubclass(exc_class, HTTPException): + return exc_class, exc_class.code + else: + return exc_class, None + + +def _endpoint_from_view_func(view_func: t.Callable) -> str: + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__ + + +def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool: + # Path.is_relative_to doesn't exist until Python 3.9 + try: + path.relative_to(base) + return True + except ValueError: + return False + + +def _find_package_path(import_name): + """Find the path that contains the package or module.""" + root_mod_name, _, _ = import_name.partition(".") + + try: + root_spec = importlib.util.find_spec(root_mod_name) + + if root_spec is None: + raise ValueError("not found") + except (ImportError, ValueError): + # ImportError: the machinery told us it does not exist + # ValueError: + # - the module name was invalid + # - the module name is __main__ + # - we raised `ValueError` due to `root_spec` being `None` + return os.getcwd() + + if root_spec.origin in {"namespace", None}: + # namespace package + package_spec = importlib.util.find_spec(import_name) + + if package_spec is not None and package_spec.submodule_search_locations: + # Pick the path in the namespace that contains the submodule. + package_path = pathlib.Path( + os.path.commonpath(package_spec.submodule_search_locations) + ) + search_location = next( + location + for location in root_spec.submodule_search_locations + if _path_is_relative_to(package_path, location) + ) + else: + # Pick the first path. + search_location = root_spec.submodule_search_locations[0] + + return os.path.dirname(search_location) + elif root_spec.submodule_search_locations: + # package with __init__.py + return os.path.dirname(os.path.dirname(root_spec.origin)) + else: + # module + return os.path.dirname(root_spec.origin) + + +def find_package(import_name: str): + """Find the prefix that a package is installed under, and the path + that it would be imported from. + + The prefix is the directory containing the standard directory + hierarchy (lib, bin, etc.). If the package is not installed to the + system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), + ``None`` is returned. + + The path is the entry in :attr:`sys.path` that contains the package + for import. If the package is not installed, it's assumed that the + package was imported from the current working directory. + """ + package_path = _find_package_path(import_name) + py_prefix = os.path.abspath(sys.prefix) + + # installed to the system + if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix): + return py_prefix, package_path + + site_parent, site_folder = os.path.split(package_path) + + # installed to a virtualenv + if site_folder.lower() == "site-packages": + parent, folder = os.path.split(site_parent) + + # Windows (prefix/lib/site-packages) + if folder.lower() == "lib": + return parent, package_path + + # Unix (prefix/lib/pythonX.Y/site-packages) + if os.path.basename(parent).lower() == "lib": + return os.path.dirname(parent), package_path + + # something else (prefix/site-packages) + return site_parent, package_path + + # not installed + return None, package_path diff --git a/lib/python3.12/site-packages/flask/sessions.py b/lib/python3.12/site-packages/flask/sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..e5650d6862ba235cda3f935e21679cc9d866958c --- /dev/null +++ b/lib/python3.12/site-packages/flask/sessions.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import hashlib +import typing as t +from collections.abc import MutableMapping +from datetime import datetime +from datetime import timezone + +from itsdangerous import BadSignature +from itsdangerous import URLSafeTimedSerializer +from werkzeug.datastructures import CallbackDict + +from .json.tag import TaggedJSONSerializer + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .wrappers import Request, Response + + +class SessionMixin(MutableMapping): + """Expands a basic dictionary with session attributes.""" + + @property + def permanent(self) -> bool: + """This reflects the ``'_permanent'`` key in the dict.""" + return self.get("_permanent", False) + + @permanent.setter + def permanent(self, value: bool) -> None: + self["_permanent"] = bool(value) + + #: Some implementations can detect whether a session is newly + #: created, but that is not guaranteed. Use with caution. The mixin + # default is hard-coded ``False``. + new = False + + #: Some implementations can detect changes to the session and set + #: this when that happens. The mixin default is hard coded to + #: ``True``. + modified = True + + #: Some implementations can detect when session data is read or + #: written and set this when that happens. The mixin default is hard + #: coded to ``True``. + accessed = True + + +class SecureCookieSession(CallbackDict, SessionMixin): + """Base class for sessions based on signed cookies. + + This session backend will set the :attr:`modified` and + :attr:`accessed` attributes. It cannot reliably track whether a + session is new (vs. empty), so :attr:`new` remains hard coded to + ``False``. + """ + + #: When data is changed, this is set to ``True``. Only the session + #: dictionary itself is tracked; if the session contains mutable + #: data (for example a nested dict) then this must be set to + #: ``True`` manually when modifying that data. The session cookie + #: will only be written to the response if this is ``True``. + modified = False + + #: When data is read or written, this is set to ``True``. Used by + # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` + #: header, which allows caching proxies to cache different pages for + #: different users. + accessed = False + + def __init__(self, initial: t.Any = None) -> None: + def on_update(self) -> None: + self.modified = True + self.accessed = True + + super().__init__(initial, on_update) + + def __getitem__(self, key: str) -> t.Any: + self.accessed = True + return super().__getitem__(key) + + def get(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().get(key, default) + + def setdefault(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().setdefault(key, default) + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + raise RuntimeError( + "The session is unavailable because no secret " + "key was set. Set the secret_key on the " + "application to something unique and secret." + ) + + __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 + del _fail + + +class SessionInterface: + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns ``None`` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + Multiple requests with the same session may be sent and handled + concurrently. When implementing a new session interface, consider + whether reads or writes to the backing store must be synchronized. + There is no guarantee on the order in which the session for each + request is opened or saved, it will occur in the order that requests + begin and end processing. + + .. versionadded:: 0.8 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + #: A flag that indicates if the session interface is pickle based. + #: This can be used by Flask extensions to make a decision in regards + #: to how to deal with the session object. + #: + #: .. versionadded:: 0.10 + pickle_based = False + + def make_null_session(self, app: Flask) -> NullSession: + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj: object) -> bool: + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_name(self, app: Flask) -> str: + """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" + return app.config["SESSION_COOKIE_NAME"] + + def get_cookie_domain(self, app: Flask) -> str | None: + """The value of the ``Domain`` parameter on the session cookie. If not set, + browsers will only send the cookie to the exact domain it was set from. + Otherwise, they will send it to any subdomain of the given value as well. + + Uses the :data:`SESSION_COOKIE_DOMAIN` config. + + .. versionchanged:: 2.3 + Not set by default, does not fall back to ``SERVER_NAME``. + """ + rv = app.config["SESSION_COOKIE_DOMAIN"] + return rv if rv else None + + def get_cookie_path(self, app: Flask) -> str: + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the ``SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's ``None``. + """ + return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] + + def get_cookie_httponly(self, app: Flask) -> bool: + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config["SESSION_COOKIE_HTTPONLY"] + + def get_cookie_secure(self, app: Flask) -> bool: + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config["SESSION_COOKIE_SECURE"] + + def get_cookie_samesite(self, app: Flask) -> str: + """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the + ``SameSite`` attribute. This currently just returns the value of + the :data:`SESSION_COOKIE_SAMESITE` setting. + """ + return app.config["SESSION_COOKIE_SAMESITE"] + + def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: + """A helper method that returns an expiration date for the session + or ``None`` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.now(timezone.utc) + app.permanent_session_lifetime + return None + + def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: + """Used by session backends to determine if a ``Set-Cookie`` header + should be set for this session cookie for this response. If the session + has been modified, the cookie is set. If the session is permanent and + the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is + always set. + + This check is usually skipped if the session was deleted. + + .. versionadded:: 0.11 + """ + + return session.modified or ( + session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] + ) + + def open_session(self, app: Flask, request: Request) -> SessionMixin | None: + """This is called at the beginning of each request, after + pushing the request context, before matching the URL. + + This must return an object which implements a dictionary-like + interface as well as the :class:`SessionMixin` interface. + + This will return ``None`` to indicate that loading failed in + some way that is not immediately an error. The request + context will fall back to using :meth:`make_null_session` + in this case. + """ + raise NotImplementedError() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + """This is called at the end of each request, after generating + a response, before removing the request context. It is skipped + if :meth:`is_null_session` returns ``True``. + """ + raise NotImplementedError() + + +session_json_serializer = TaggedJSONSerializer() + + +class SecureCookieSessionInterface(SessionInterface): + """The default session interface that stores sessions in signed cookies + through the :mod:`itsdangerous` module. + """ + + #: the salt that should be applied on top of the secret key for the + #: signing of cookie based sessions. + salt = "cookie-session" + #: the hash function to use for the signature. The default is sha1 + digest_method = staticmethod(hashlib.sha1) + #: the name of the itsdangerous supported key derivation. The default + #: is hmac. + key_derivation = "hmac" + #: A python serializer for the payload. The default is a compact + #: JSON derived serializer with support for some extra Python types + #: such as datetime objects or tuples. + serializer = session_json_serializer + session_class = SecureCookieSession + + def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: + if not app.secret_key: + return None + signer_kwargs = dict( + key_derivation=self.key_derivation, digest_method=self.digest_method + ) + return URLSafeTimedSerializer( + app.secret_key, + salt=self.salt, + serializer=self.serializer, + signer_kwargs=signer_kwargs, + ) + + def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: + s = self.get_signing_serializer(app) + if s is None: + return None + val = request.cookies.get(self.get_cookie_name(app)) + if not val: + return self.session_class() + max_age = int(app.permanent_session_lifetime.total_seconds()) + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + name = self.get_cookie_name(app) + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + secure = self.get_cookie_secure(app) + samesite = self.get_cookie_samesite(app) + httponly = self.get_cookie_httponly(app) + + # Add a "Vary: Cookie" header if the session was accessed at all. + if session.accessed: + response.vary.add("Cookie") + + # If the session is modified to be empty, remove the cookie. + # If the session is empty, return without setting the cookie. + if not session: + if session.modified: + response.delete_cookie( + name, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + httponly=httponly, + ) + response.vary.add("Cookie") + + return + + if not self.should_set_cookie(app, session): + return + + expires = self.get_expiration_time(app, session) + val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore + response.set_cookie( + name, + val, # type: ignore + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + ) + response.vary.add("Cookie") diff --git a/lib/python3.12/site-packages/flask/signals.py b/lib/python3.12/site-packages/flask/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..d79f21f9396026c18428b149edd67d1debbeb07b --- /dev/null +++ b/lib/python3.12/site-packages/flask/signals.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import typing as t +import warnings + +from blinker import Namespace + +# This namespace is only for signals provided by Flask itself. +_signals = Namespace() + +template_rendered = _signals.signal("template-rendered") +before_render_template = _signals.signal("before-render-template") +request_started = _signals.signal("request-started") +request_finished = _signals.signal("request-finished") +request_tearing_down = _signals.signal("request-tearing-down") +got_request_exception = _signals.signal("got-request-exception") +appcontext_tearing_down = _signals.signal("appcontext-tearing-down") +appcontext_pushed = _signals.signal("appcontext-pushed") +appcontext_popped = _signals.signal("appcontext-popped") +message_flashed = _signals.signal("message-flashed") + + +def __getattr__(name: str) -> t.Any: + if name == "signals_available": + warnings.warn( + "The 'signals_available' attribute is deprecated and will be removed in" + " Flask 2.4. Signals are always available.", + DeprecationWarning, + stacklevel=2, + ) + return True + + raise AttributeError(name) diff --git a/lib/python3.12/site-packages/flask/templating.py b/lib/python3.12/site-packages/flask/templating.py new file mode 100644 index 0000000000000000000000000000000000000000..769108f755229f6e5785718e2e48836f9b58223c --- /dev/null +++ b/lib/python3.12/site-packages/flask/templating.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +import typing as t + +from jinja2 import BaseLoader +from jinja2 import Environment as BaseEnvironment +from jinja2 import Template +from jinja2 import TemplateNotFound + +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .helpers import stream_with_context +from .signals import before_render_template +from .signals import template_rendered + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .scaffold import Scaffold + + +def _default_template_ctx_processor() -> dict[str, t.Any]: + """Default template context processor. Injects `request`, + `session` and `g`. + """ + appctx = _cv_app.get(None) + reqctx = _cv_request.get(None) + rv: dict[str, t.Any] = {} + if appctx is not None: + rv["g"] = appctx.g + if reqctx is not None: + rv["request"] = reqctx.request + rv["session"] = reqctx.session + return rv + + +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app: Flask, **options: t.Any) -> None: + if "loader" not in options: + options["loader"] = app.create_global_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + +class DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the blueprint folders. + """ + + def __init__(self, app: Flask) -> None: + self.app = app + + def get_source( # type: ignore + self, environment: Environment, template: str + ) -> tuple[str, str | None, t.Callable | None]: + if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: + return self._get_source_explained(environment, template) + return self._get_source_fast(environment, template) + + def _get_source_explained( + self, environment: Environment, template: str + ) -> tuple[str, str | None, t.Callable | None]: + attempts = [] + rv: tuple[str, str | None, t.Callable[[], bool] | None] | None + trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None + + for srcobj, loader in self._iter_loaders(template): + try: + rv = loader.get_source(environment, template) + if trv is None: + trv = rv + except TemplateNotFound: + rv = None + attempts.append((loader, srcobj, rv)) + + from .debughelpers import explain_template_loading_attempts + + explain_template_loading_attempts(self.app, template, attempts) + + if trv is not None: + return trv + raise TemplateNotFound(template) + + def _get_source_fast( + self, environment: Environment, template: str + ) -> tuple[str, str | None, t.Callable | None]: + for _srcobj, loader in self._iter_loaders(template): + try: + return loader.get_source(environment, template) + except TemplateNotFound: + continue + raise TemplateNotFound(template) + + def _iter_loaders( + self, template: str + ) -> t.Generator[tuple[Scaffold, BaseLoader], None, None]: + loader = self.app.jinja_loader + if loader is not None: + yield self.app, loader + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + yield blueprint, loader + + def list_templates(self) -> list[str]: + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + result.add(template) + + return list(result) + + +def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + rv = template.render(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + return rv + + +def render_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> str: + """Render a template by name with the given context. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, template, context) + + +def render_template_string(source: str, **context: t.Any) -> str: + """Render a template from the given source string with the given + context. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) + + +def _stream( + app: Flask, template: Template, context: dict[str, t.Any] +) -> t.Iterator[str]: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + def generate() -> t.Iterator[str]: + yield from template.generate(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + rv = generate() + + # If a request context is active, keep it while generating. + if request: + rv = stream_with_context(rv) + + return rv + + +def stream_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> t.Iterator[str]: + """Render a template by name with the given context as a stream. + This returns an iterator of strings, which can be used as a + streaming response from a view. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _stream(app, template, context) + + +def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: + """Render a template from the given source string with the given + context as a stream. This returns an iterator of strings, which can + be used as a streaming response from a view. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _stream(app, template, context) diff --git a/lib/python3.12/site-packages/flask/testing.py b/lib/python3.12/site-packages/flask/testing.py new file mode 100644 index 0000000000000000000000000000000000000000..69aa7851889fe02cbb5ee3649c32ed8a950a5280 --- /dev/null +++ b/lib/python3.12/site-packages/flask/testing.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +import importlib.metadata +import typing as t +from contextlib import contextmanager +from contextlib import ExitStack +from copy import copy +from types import TracebackType +from urllib.parse import urlsplit + +import werkzeug.test +from click.testing import CliRunner +from werkzeug.test import Client +from werkzeug.wrappers import Request as BaseRequest + +from .cli import ScriptInfo +from .sessions import SessionMixin + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.test import TestResponse + + from .app import Flask + + +class EnvironBuilder(werkzeug.test.EnvironBuilder): + """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the + application. + + :param app: The Flask application to configure the environment from. + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + + def __init__( + self, + app: Flask, + path: str = "/", + base_url: str | None = None, + subdomain: str | None = None, + url_scheme: str | None = None, + *args: t.Any, + **kwargs: t.Any, + ) -> None: + assert not (base_url or subdomain or url_scheme) or ( + base_url is not None + ) != bool( + subdomain or url_scheme + ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + + if base_url is None: + http_host = app.config.get("SERVER_NAME") or "localhost" + app_root = app.config["APPLICATION_ROOT"] + + if subdomain: + http_host = f"{subdomain}.{http_host}" + + if url_scheme is None: + url_scheme = app.config["PREFERRED_URL_SCHEME"] + + url = urlsplit(path) + base_url = ( + f"{url.scheme or url_scheme}://{url.netloc or http_host}" + f"/{app_root.lstrip('/')}" + ) + path = url.path + + if url.query: + sep = b"?" if isinstance(url.query, bytes) else "?" + path += sep + url.query + + self.app = app + super().__init__(path, base_url, *args, **kwargs) + + def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore + """Serialize ``obj`` to a JSON-formatted string. + + The serialization will be configured according to the config associated + with this EnvironBuilder's ``app``. + """ + return self.app.json.dumps(obj, **kwargs) + + +_werkzeug_version = "" + + +def _get_werkzeug_version() -> str: + global _werkzeug_version + + if not _werkzeug_version: + _werkzeug_version = importlib.metadata.version("werkzeug") + + return _werkzeug_version + + +class FlaskClient(Client): + """Works like a regular Werkzeug test client but has knowledge about + Flask's contexts to defer the cleanup of the request context until + the end of a ``with`` block. For general information about how to + use this class refer to :class:`werkzeug.test.Client`. + + .. versionchanged:: 0.12 + `app.test_client()` includes preset default environment, which can be + set after instantiation of the `app.test_client()` object in + `client.environ_base`. + + Basic usage is outlined in the :doc:`/testing` chapter. + """ + + application: Flask + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + super().__init__(*args, **kwargs) + self.preserve_context = False + self._new_contexts: list[t.ContextManager[t.Any]] = [] + self._context_stack = ExitStack() + self.environ_base = { + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", + } + + @contextmanager + def session_transaction( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Generator[SessionMixin, None, None]: + """When used in combination with a ``with`` statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the ``with`` block is left the session is + stored back. + + :: + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + app = self.application + ctx = app.test_request_context(*args, **kwargs) + self._add_cookies_to_wsgi(ctx.request.environ) + + with ctx: + sess = app.session_interface.open_session(app, ctx.request) + + if sess is None: + raise RuntimeError("Session backend did not open a session.") + + yield sess + resp = app.response_class() + + if app.session_interface.is_null_session(sess): + return + + with ctx: + app.session_interface.save_session(app, sess, resp) + + self._update_cookies_from_response( + ctx.request.host.partition(":")[0], + ctx.request.path, + resp.headers.getlist("Set-Cookie"), + ) + + def _copy_environ(self, other): + out = {**self.environ_base, **other} + + if self.preserve_context: + out["werkzeug.debug.preserve_context"] = self._new_contexts.append + + return out + + def _request_from_builder_args(self, args, kwargs): + kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) + builder = EnvironBuilder(self.application, *args, **kwargs) + + try: + return builder.get_request() + finally: + builder.close() + + def open( + self, + *args: t.Any, + buffered: bool = False, + follow_redirects: bool = False, + **kwargs: t.Any, + ) -> TestResponse: + if args and isinstance( + args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) + ): + if isinstance(args[0], werkzeug.test.EnvironBuilder): + builder = copy(args[0]) + builder.environ_base = self._copy_environ(builder.environ_base or {}) + request = builder.get_request() + elif isinstance(args[0], dict): + request = EnvironBuilder.from_environ( + args[0], app=self.application, environ_base=self._copy_environ({}) + ).get_request() + else: + # isinstance(args[0], BaseRequest) + request = copy(args[0]) + request.environ = self._copy_environ(request.environ) + else: + # request is None + request = self._request_from_builder_args(args, kwargs) + + # Pop any previously preserved contexts. This prevents contexts + # from being preserved across redirects or multiple requests + # within a single block. + self._context_stack.close() + + response = super().open( + request, + buffered=buffered, + follow_redirects=follow_redirects, + ) + response.json_module = self.application.json # type: ignore[assignment] + + # Re-push contexts that were preserved during the request. + while self._new_contexts: + cm = self._new_contexts.pop() + self._context_stack.enter_context(cm) + + return response + + def __enter__(self) -> FlaskClient: + if self.preserve_context: + raise RuntimeError("Cannot nest client invocations") + self.preserve_context = True + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.preserve_context = False + self._context_stack.close() + + +class FlaskCliRunner(CliRunner): + """A :class:`~click.testing.CliRunner` for testing a Flask app's + CLI commands. Typically created using + :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. + """ + + def __init__(self, app: Flask, **kwargs: t.Any) -> None: + self.app = app + super().__init__(**kwargs) + + def invoke( # type: ignore + self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any + ) -> t.Any: + """Invokes a CLI command in an isolated environment. See + :meth:`CliRunner.invoke ` for + full method documentation. See :ref:`testing-cli` for examples. + + If the ``obj`` argument is not given, passes an instance of + :class:`~flask.cli.ScriptInfo` that knows how to load the Flask + app being tested. + + :param cli: Command object to invoke. Default is the app's + :attr:`~flask.app.Flask.cli` group. + :param args: List of strings to invoke the command with. + + :return: a :class:`~click.testing.Result` object. + """ + if cli is None: + cli = self.app.cli # type: ignore + + if "obj" not in kwargs: + kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) + + return super().invoke(cli, args, **kwargs) diff --git a/lib/python3.12/site-packages/flask/typing.py b/lib/python3.12/site-packages/flask/typing.py new file mode 100644 index 0000000000000000000000000000000000000000..50aef7f463361f2f2d10096e5472ef9768cab743 --- /dev/null +++ b/lib/python3.12/site-packages/flask/typing.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import typing as t + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIApplication # noqa: F401 + from werkzeug.datastructures import Headers # noqa: F401 + from werkzeug.wrappers import Response # noqa: F401 + +# The possible types that are directly convertible or are a Response object. +ResponseValue = t.Union[ + "Response", + str, + bytes, + t.List[t.Any], + # Only dict is actually accepted, but Mapping allows for TypedDict. + t.Mapping[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], +] + +# the possible types for an individual HTTP header +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. +HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] + +# the possible types for HTTP headers +HeadersValue = t.Union[ + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[t.Tuple[str, HeaderValue]], +] + +# The possible types returned by a route function. +ResponseReturnValue = t.Union[ + ResponseValue, + t.Tuple[ResponseValue, HeadersValue], + t.Tuple[ResponseValue, int], + t.Tuple[ResponseValue, int, HeadersValue], + "WSGIApplication", +] + +# Allow any subclass of werkzeug.Response, such as the one from Flask, +# as a callback argument. Using werkzeug.Response directly makes a +# callback annotated with flask.Response fail type checking. +ResponseClass = t.TypeVar("ResponseClass", bound="Response") + +AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named +AfterRequestCallable = t.Union[ + t.Callable[[ResponseClass], ResponseClass], + t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], +] +BeforeFirstRequestCallable = t.Union[ + t.Callable[[], None], t.Callable[[], t.Awaitable[None]] +] +BeforeRequestCallable = t.Union[ + t.Callable[[], t.Optional[ResponseReturnValue]], + t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], +] +ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] +TeardownCallable = t.Union[ + t.Callable[[t.Optional[BaseException]], None], + t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], +] +TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] +TemplateFilterCallable = t.Callable[..., t.Any] +TemplateGlobalCallable = t.Callable[..., t.Any] +TemplateTestCallable = t.Callable[..., bool] +URLDefaultCallable = t.Callable[[str, dict], None] +URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] + +# This should take Exception, but that either breaks typing the argument +# with a specific exception, or decorating multiple times with different +# exceptions (and using a union type on the argument). +# https://github.com/pallets/flask/issues/4095 +# https://github.com/pallets/flask/issues/4295 +# https://github.com/pallets/flask/issues/4297 +ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue] + +RouteCallable = t.Union[ + t.Callable[..., ResponseReturnValue], + t.Callable[..., t.Awaitable[ResponseReturnValue]], +] diff --git a/lib/python3.12/site-packages/flask/views.py b/lib/python3.12/site-packages/flask/views.py new file mode 100644 index 0000000000000000000000000000000000000000..c7a2b621c89604cdd2f1278404c2d01ff1932083 --- /dev/null +++ b/lib/python3.12/site-packages/flask/views.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +import typing as t + +from . import typing as ft +from .globals import current_app +from .globals import request + + +http_method_funcs = frozenset( + ["get", "post", "head", "options", "delete", "put", "trace", "patch"] +) + + +class View: + """Subclass this class and override :meth:`dispatch_request` to + create a generic class-based view. Call :meth:`as_view` to create a + view function that creates an instance of the class with the given + arguments and calls its ``dispatch_request`` method with any URL + variables. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class Hello(View): + init_every_request = False + + def dispatch_request(self, name): + return f"Hello, {name}!" + + app.add_url_rule( + "/hello/", view_func=Hello.as_view("hello") + ) + + Set :attr:`methods` on the class to change what methods the view + accepts. + + Set :attr:`decorators` on the class to apply a list of decorators to + the generated view function. Decorators applied to the class itself + will not be applied to the generated view function! + + Set :attr:`init_every_request` to ``False`` for efficiency, unless + you need to store request-global data on ``self``. + """ + + #: The methods this view is registered for. Uses the same default + #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and + #: ``add_url_rule`` by default. + methods: t.ClassVar[t.Collection[str] | None] = None + + #: Control whether the ``OPTIONS`` method is handled automatically. + #: Uses the same default (``True``) as ``route`` and + #: ``add_url_rule`` by default. + provide_automatic_options: t.ClassVar[bool | None] = None + + #: A list of decorators to apply, in order, to the generated view + #: function. Remember that ``@decorator`` syntax is applied bottom + #: to top, so the first decorator in the list would be the bottom + #: decorator. + #: + #: .. versionadded:: 0.8 + decorators: t.ClassVar[list[t.Callable]] = [] + + #: Create a new instance of this view class for every request by + #: default. If a view subclass sets this to ``False``, the same + #: instance is used for every request. + #: + #: A single instance is more efficient, especially if complex setup + #: is done during init. However, storing data on ``self`` is no + #: longer safe across requests, and :data:`~flask.g` should be used + #: instead. + #: + #: .. versionadded:: 2.2 + init_every_request: t.ClassVar[bool] = True + + def dispatch_request(self) -> ft.ResponseReturnValue: + """The actual view function behavior. Subclasses must override + this and return a valid response. Any variables from the URL + rule are passed as keyword arguments. + """ + raise NotImplementedError() + + @classmethod + def as_view( + cls, name: str, *class_args: t.Any, **class_kwargs: t.Any + ) -> ft.RouteCallable: + """Convert the class into a view function that can be registered + for a route. + + By default, the generated view will create a new instance of the + view class for every request and call its + :meth:`dispatch_request` method. If the view class sets + :attr:`init_every_request` to ``False``, the same instance will + be used for every request. + + Except for ``name``, all other arguments passed to this method + are forwarded to the view class ``__init__`` method. + + .. versionchanged:: 2.2 + Added the ``init_every_request`` class attribute. + """ + if cls.init_every_request: + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + self = view.view_class( # type: ignore[attr-defined] + *class_args, **class_kwargs + ) + return current_app.ensure_sync(self.dispatch_request)(**kwargs) + + else: + self = cls(*class_args, **class_kwargs) + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + return current_app.ensure_sync(self.dispatch_request)(**kwargs) + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + # We attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class-based + # view this thing came from, secondly it's also used for instantiating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls # type: ignore + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods # type: ignore + view.provide_automatic_options = cls.provide_automatic_options # type: ignore + return view + + +class MethodView(View): + """Dispatches request methods to the corresponding instance methods. + For example, if you implement a ``get`` method, it will be used to + handle ``GET`` requests. + + This can be useful for defining a REST API. + + :attr:`methods` is automatically set based on the methods defined on + the class. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class CounterAPI(MethodView): + def get(self): + return str(session.get("counter", 0)) + + def post(self): + session["counter"] = session.get("counter", 0) + 1 + return redirect(url_for("counter")) + + app.add_url_rule( + "/counter", view_func=CounterAPI.as_view("counter") + ) + """ + + def __init_subclass__(cls, **kwargs: t.Any) -> None: + super().__init_subclass__(**kwargs) + + if "methods" not in cls.__dict__: + methods = set() + + for base in cls.__bases__: + if getattr(base, "methods", None): + methods.update(base.methods) # type: ignore[attr-defined] + + for key in http_method_funcs: + if hasattr(cls, key): + methods.add(key.upper()) + + if methods: + cls.methods = methods + + def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: + meth = getattr(self, request.method.lower(), None) + + # If the request method is HEAD and we don't have a handler for it + # retry with GET. + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + + assert meth is not None, f"Unimplemented method {request.method!r}" + return current_app.ensure_sync(meth)(**kwargs) diff --git a/lib/python3.12/site-packages/flask/wrappers.py b/lib/python3.12/site-packages/flask/wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..ef7aa38c0ebe8617d8015ec6b9ee7f4b30596424 --- /dev/null +++ b/lib/python3.12/site-packages/flask/wrappers.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import typing as t + +from werkzeug.exceptions import BadRequest +from werkzeug.wrappers import Request as RequestBase +from werkzeug.wrappers import Response as ResponseBase + +from . import json +from .globals import current_app +from .helpers import _split_blueprint_path + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.routing import Rule + + +class Request(RequestBase): + """The request object used by default in Flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. + """ + + json_module: t.Any = json + + #: The internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: Though if the request's method was invalid for the URL rule, + #: the valid list is available in ``routing_exception.valid_methods`` + #: instead (an attribute of the Werkzeug exception + #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) + #: because the request was never internally bound. + #: + #: .. versionadded:: 0.6 + url_rule: Rule | None = None + + #: A dict of view arguments that matched the request. If an exception + #: happened when matching, this will be ``None``. + view_args: dict[str, t.Any] | None = None + + #: If matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception: Exception | None = None + + @property + def max_content_length(self) -> int | None: # type: ignore + """Read-only view of the ``MAX_CONTENT_LENGTH`` config key.""" + if current_app: + return current_app.config["MAX_CONTENT_LENGTH"] + else: + return None + + @property + def endpoint(self) -> str | None: + """The endpoint that matched the request URL. + + This will be ``None`` if matching failed or has not been + performed yet. + + This in combination with :attr:`view_args` can be used to + reconstruct the same URL or a modified URL. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + + return None + + @property + def blueprint(self) -> str | None: + """The registered name of the current blueprint. + + This will be ``None`` if the endpoint is not part of a + blueprint, or if URL matching failed or has not been performed + yet. + + This does not necessarily match the name the blueprint was + created with. It may have been nested, or registered with a + different name. + """ + endpoint = self.endpoint + + if endpoint is not None and "." in endpoint: + return endpoint.rpartition(".")[0] + + return None + + @property + def blueprints(self) -> list[str]: + """The registered names of the current blueprint upwards through + parent blueprints. + + This will be an empty list if there is no current blueprint, or + if URL matching failed. + + .. versionadded:: 2.0.1 + """ + name = self.blueprint + + if name is None: + return [] + + return _split_blueprint_path(name) + + def _load_form_data(self) -> None: + super()._load_form_data() + + # In debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + if ( + current_app + and current_app.debug + and self.mimetype != "multipart/form-data" + and not self.files + ): + from .debughelpers import attach_enctype_error_multidict + + attach_enctype_error_multidict(self) + + def on_json_loading_failed(self, e: ValueError | None) -> t.Any: + try: + return super().on_json_loading_failed(e) + except BadRequest as e: + if current_app and current_app.debug: + raise + + raise BadRequest() from e + + +class Response(ResponseBase): + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + + .. versionchanged:: 1.0 + JSON support is added to the response, like the request. This is useful + when testing to get the test client response data as JSON. + + .. versionchanged:: 1.0 + + Added :attr:`max_cookie_size`. + """ + + default_mimetype: str | None = "text/html" + + json_module = json + + autocorrect_location_header = False + + @property + def max_cookie_size(self) -> int: # type: ignore + """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. + + See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in + Werkzeug's docs. + """ + if current_app: + return current_app.config["MAX_COOKIE_SIZE"] + + # return Werkzeug's default when not in an app context + return super().max_cookie_size diff --git a/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/INSTALLER b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/METADATA b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..30d66530bc93801fc3385006ce179e72b42644df --- /dev/null +++ b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/METADATA @@ -0,0 +1,672 @@ +Metadata-Version: 2.4 +Name: frozenlist +Version: 1.8.0 +Summary: A list-like structure which implements collections.abc.MutableSequence +Home-page: https://github.com/aio-libs/frozenlist +Maintainer: aiohttp team +Maintainer-email: team@aiohttp.org +License: Apache-2.0 +Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org +Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org +Project-URL: CI: Github Actions, https://github.com/aio-libs/frozenlist/actions +Project-URL: Code of Conduct, https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md +Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/frozenlist +Project-URL: Docs: Changelog, https://github.com/aio-libs/frozenlist/blob/master/CHANGES.rst#changelog +Project-URL: Docs: RTD, https://frozenlist.aio-libs.org +Project-URL: GitHub: issues, https://github.com/aio-libs/frozenlist/issues +Project-URL: GitHub: repo, https://github.com/aio-libs/frozenlist +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: POSIX +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Dynamic: license-file + +frozenlist +========== + +.. image:: https://github.com/aio-libs/frozenlist/workflows/CI/badge.svg + :target: https://github.com/aio-libs/frozenlist/actions + :alt: GitHub status for master branch + +.. image:: https://codecov.io/gh/aio-libs/frozenlist/branch/master/graph/badge.svg?flag=pytest + :target: https://codecov.io/gh/aio-libs/frozenlist?flags[]=pytest + :alt: codecov.io status for master branch + +.. image:: https://img.shields.io/pypi/v/frozenlist.svg?logo=Python&logoColor=white + :target: https://pypi.org/project/frozenlist + :alt: frozenlist @ PyPI + +.. image:: https://readthedocs.org/projects/frozenlist/badge/?version=latest + :target: https://frozenlist.aio-libs.org + :alt: Read The Docs build status badge + +.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs:matrix.org + :alt: Matrix Room — #aio-libs:matrix.org + +.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs-space:matrix.org + :alt: Matrix Space — #aio-libs-space:matrix.org + +Introduction +------------ + +``frozenlist.FrozenList`` is a list-like structure which implements +``collections.abc.MutableSequence``. The list is *mutable* until ``FrozenList.freeze`` +is called, after which list modifications raise ``RuntimeError``: + + +>>> from frozenlist import FrozenList +>>> fl = FrozenList([17, 42]) +>>> fl.append('spam') +>>> fl.append('Vikings') +>>> fl + +>>> fl.freeze() +>>> fl + +>>> fl.frozen +True +>>> fl.append("Monty") +Traceback (most recent call last): + File "", line 1, in + File "frozenlist/_frozenlist.pyx", line 97, in frozenlist._frozenlist.FrozenList.append + self._check_frozen() + File "frozenlist/_frozenlist.pyx", line 19, in frozenlist._frozenlist.FrozenList._check_frozen + raise RuntimeError("Cannot modify frozen list.") +RuntimeError: Cannot modify frozen list. + + +FrozenList is also hashable, but only when frozen. Otherwise it also throws a RuntimeError: + + +>>> fl = FrozenList([17, 42, 'spam']) +>>> hash(fl) +Traceback (most recent call last): + File "", line 1, in + File "frozenlist/_frozenlist.pyx", line 111, in frozenlist._frozenlist.FrozenList.__hash__ + raise RuntimeError("Cannot hash unfrozen list.") +RuntimeError: Cannot hash unfrozen list. +>>> fl.freeze() +>>> hash(fl) +3713081631934410656 +>>> dictionary = {fl: 'Vikings'} # frozen fl can be a dict key +>>> dictionary +{: 'Vikings'} + + +Installation +------------ + +:: + + $ pip install frozenlist + + +Documentation +------------- + +https://frozenlist.aio-libs.org + +Communication channels +---------------------- + +We have a *Matrix Space* `#aio-libs-space:matrix.org +`_ which is +also accessible via Gitter. + +License +------- + +``frozenlist`` is offered under the Apache 2 license. + +Source code +----------- + +The project is hosted on GitHub_ + +Please file an issue in the `bug tracker +`_ if you have found a bug +or have some suggestions to improve the library. + +.. _GitHub: https://github.com/aio-libs/frozenlist + +========= +Changelog +========= + +.. + You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/contributing/#news-entries + we named the news folder "changes". + + WARNING: Don't drop the next directive! + +.. towncrier release notes start + +v1.8.0 +====== + +*(2025-10-05)* + + +Contributor-facing changes +-------------------------- + +- The ``reusable-cibuildwheel.yml`` workflow has been refactored to + be more generic and ``ci-cd.yml`` now holds all the configuration + toggles -- by `@webknjaz `__. + + *Related issues and pull requests on GitHub:* + `#668 `__. + +- When building wheels, the source distribution is now passed directly + to the ``cibuildwheel`` invocation -- by `@webknjaz `__. + + *Related issues and pull requests on GitHub:* + `#669 `__. + +- Builds and tests have been added to + ``ci-cd.yml`` for arm64 Windows wheels -- by `@finnagin `__. + + *Related issues and pull requests on GitHub:* + `#677 `__. + +- Started building wheels for CPython 3.14 -- by `@kumaraditya303 `__. + + *Related issues and pull requests on GitHub:* + `#681 `__, `#682 `__. + +- Removed ``--config-settings=pure-python=false`` from ``requirements/dev.txt``. + Developers on CPython still get accelerated builds by default. To explicitly build + a pure Python wheel, use ``pip install -e . --config-settings=pure-python=true`` + -- by `@bdraco `__. + + *Related issues and pull requests on GitHub:* + `#687 `__. + + +---- + + +v1.7.0 +====== + +*(2025-06-09)* + + +Features +-------- + +- Added deepcopy support to FrozenList -- by `@bdraco `__. + + *Related issues and pull requests on GitHub:* + `#659 `__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Fixed an issue where ``frozenlist`` binary wheels would be built with debugging symbols and line tracing enabled, which significantly impacted performance. Line tracing is now disabled by default and can only be enabled explicitly -- by `@bdraco `__. + + This change ensures that production builds are optimized for performance. Developers who need line tracing for debugging purposes can still enable it by: + + 1. Setting the ``FROZENLIST_CYTHON_TRACING`` environment variable + 2. Using the ``--config-setting=with-cython-tracing=true`` option with pip + + *Related issues and pull requests on GitHub:* + `#660 `__. + +- Enabled ``PIP_CONSTRAINT`` environment variable in the build configuration to ensure the pinned Cython version from ``requirements/cython.txt`` is used during wheel builds. + + *Related issues and pull requests on GitHub:* + `#661 `__. + + +---- + + +v1.6.2 +====== + +*(2025-06-03)* + + +No significant changes. + + +---- + + +v1.6.1 +====== + +*(2025-06-02)* + + +Bug fixes +--------- + +- Correctly use ``cimport`` for including ``PyBool_FromLong`` -- by `@lysnikolaou `__. + + *Related issues and pull requests on GitHub:* + `#653 `__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Exclude ``_frozenlist.cpp`` from bdists/wheels -- by `@musicinmybrain `__. + + *Related issues and pull requests on GitHub:* + `#649 `__. + +- Updated to use Cython 3.1 universally across the build path -- by `@lysnikolaou `__. + + *Related issues and pull requests on GitHub:* + `#654 `__. + + +---- + + +v1.6.0 +====== + +*(2025-04-17)* + + +Bug fixes +--------- + +- Stopped implicitly allowing the use of Cython pre-release versions when + building the distribution package -- by `@ajsanchezsanz `__ and + `@markgreene74 `__. + + *Related commits on GitHub:* + `41591f2 `__. + + +Features +-------- + +- Implemented support for the free-threaded build of CPython 3.13 -- by `@lysnikolaou `__. + + *Related issues and pull requests on GitHub:* + `#618 `__. + +- Started building armv7l wheels -- by `@bdraco `__. + + *Related issues and pull requests on GitHub:* + `#642 `__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Stopped implicitly allowing the use of Cython pre-release versions when + building the distribution package -- by `@ajsanchezsanz `__ and + `@markgreene74 `__. + + *Related commits on GitHub:* + `41591f2 `__. + +- Started building wheels for the free-threaded build of CPython 3.13 -- by `@lysnikolaou `__. + + *Related issues and pull requests on GitHub:* + `#618 `__. + +- The packaging metadata switched to including an SPDX license identifier introduced in `PEP 639 `__ -- by `@cdce8p `__. + + *Related issues and pull requests on GitHub:* + `#639 `__. + + +Contributor-facing changes +-------------------------- + +- GitHub Actions CI/CD is now configured to manage caching pip-ecosystem + dependencies using `re-actors/cache-python-deps`_ -- an action by + `@webknjaz `__ that takes into account ABI stability and the exact + version of Python runtime. + + .. _`re-actors/cache-python-deps`: + https://github.com/marketplace/actions/cache-python-deps + + *Related issues and pull requests on GitHub:* + `#633 `__. + +- Organized dependencies into test and lint dependencies so that no + unnecessary ones are installed during CI runs -- by `@lysnikolaou `__. + + *Related issues and pull requests on GitHub:* + `#636 `__. + + +---- + + +1.5.0 (2024-10-22) +================== + +Bug fixes +--------- + +- An incorrect signature of the ``__class_getitem__`` class method + has been fixed, adding a missing ``class_item`` argument under + Python 3.8 and older. + + This change also improves the code coverage of this method that + was previously missing -- by `@webknjaz `__. + + + *Related issues and pull requests on GitHub:* + `#567 `__, `#571 `__. + + +Improved documentation +---------------------- + +- Rendered issue, PR, and commit links now lead to + ``frozenlist``'s repo instead of ``yarl``'s repo. + + + *Related issues and pull requests on GitHub:* + `#573 `__. + +- On the ``Contributing docs`` page, + a link to the ``Towncrier philosophy`` has been fixed. + + + *Related issues and pull requests on GitHub:* + `#574 `__. + + +Packaging updates and notes for downstreams +------------------------------------------- + +- A name of a temporary building directory now reflects + that it's related to ``frozenlist``, not ``yarl``. + + + *Related issues and pull requests on GitHub:* + `#573 `__. + +- Declared Python 3.13 supported officially in the distribution package metadata. + + + *Related issues and pull requests on GitHub:* + `#595 `__. + + +---- + + +1.4.1 (2023-12-15) +================== + +Packaging updates and notes for downstreams +------------------------------------------- + +- Declared Python 3.12 and PyPy 3.8-3.10 supported officially + in the distribution package metadata. + + + *Related issues and pull requests on GitHub:* + `#553 `__. + +- Replaced the packaging is replaced from an old-fashioned ``setup.py`` to an + in-tree `PEP 517 `__ build backend -- by `@webknjaz `__. + + Whenever the end-users or downstream packagers need to build ``frozenlist`` + from source (a Git checkout or an sdist), they may pass a ``config_settings`` + flag ``pure-python``. If this flag is not set, a C-extension will be built + and included into the distribution. + + Here is how this can be done with ``pip``: + + .. code-block:: console + + $ python3 -m pip install . --config-settings=pure-python= + + This will also work with ``-e | --editable``. + + The same can be achieved via ``pypa/build``: + + .. code-block:: console + + $ python3 -m build --config-setting=pure-python= + + Adding ``-w | --wheel`` can force ``pypa/build`` produce a wheel from source + directly, as opposed to building an ``sdist`` and then building from it. + + + *Related issues and pull requests on GitHub:* + `#560 `__. + + +Contributor-facing changes +-------------------------- + +- It is now possible to request line tracing in Cython builds using the + ``with-cython-tracing`` `PEP 517 `__ config setting + -- `@webknjaz `__. + + This can be used in CI and development environment to measure coverage + on Cython modules, but is not normally useful to the end-users or + downstream packagers. + + Here's a usage example: + + .. code-block:: console + + $ python3 -Im pip install . --config-settings=with-cython-tracing=true + + For editable installs, this setting is on by default. Otherwise, it's + off unless requested explicitly. + + The following produces C-files required for the Cython coverage + plugin to map the measurements back to the PYX-files: + + .. code-block:: console + + $ python -Im pip install -e . + + Alternatively, the ``FROZENLIST_CYTHON_TRACING=1`` environment variable + can be set to do the same as the `PEP 517 `__ config setting. + + + *Related issues and pull requests on GitHub:* + `#560 `__. + +- Coverage collection has been implemented for the Cython modules + -- by `@webknjaz `__. + + It will also be reported to Codecov from any non-release CI jobs. + + + *Related issues and pull requests on GitHub:* + `#561 `__. + +- A step-by-step ``Release Guide`` guide has + been added, describing how to release *frozenlist* -- by `@webknjaz `__. + + This is primarily targeting the maintainers. + + + *Related issues and pull requests on GitHub:* + `#563 `__. + +- Detailed ``Contributing Guidelines`` on + authoring the changelog fragments have been published in the + documentation -- by `@webknjaz `__. + + + *Related issues and pull requests on GitHub:* + `#564 `__. + + +---- + + +1.4.0 (2023-07-12) +================== + +The published source distribution package became buildable +under Python 3.12. + + +---- + + +Bugfixes +-------- + +- Removed an unused ``typing.Tuple`` import + `#411 `_ + + +Deprecations and Removals +------------------------- + +- Dropped Python 3.7 support. + `#413 `_ + + +Misc +---- + +- `#410 `_, `#433 `_ + + +---- + + +1.3.3 (2022-11-08) +================== + +- Fixed CI runs when creating a new release, where new towncrier versions + fail when the current version section is already present. + + +---- + + +1.3.2 (2022-11-08) +================== + +Misc +---- + +- Updated the CI runs to better check for test results and to avoid deprecated syntax. `#327 `_ + + +---- + + +1.3.1 (2022-08-02) +================== + +The published source distribution package became buildable +under Python 3.11. + + +---- + + +1.3.0 (2022-01-18) +================== + +Bugfixes +-------- + +- Do not install C sources with binary distributions. + `#250 `_ + + +Deprecations and Removals +------------------------- + +- Dropped Python 3.6 support + `#274 `_ + + +---- + + +1.2.0 (2021-10-16) +================== + +Features +-------- + +- ``FrozenList`` now supports being used as a generic type as per PEP 585, e.g. ``frozen_int_list: FrozenList[int]`` (requires Python 3.9 or newer). + `#172 `_ +- Added support for Python 3.10. + `#227 `_ +- Started shipping platform-specific wheels with the ``musl`` tag targeting typical Alpine Linux runtimes. + `#227 `_ +- Started shipping platform-specific arm64 wheels for Apple Silicon. + `#227 `_ + + +---- + + +1.1.1 (2020-11-14) +================== + +Bugfixes +-------- + +- Provide x86 Windows wheels. + `#169 `_ + + +---- + + +1.1.0 (2020-10-13) +================== + +Features +-------- + +- Add support for hashing of a frozen list. + `#136 `_ + +- Support Python 3.8 and 3.9. + +- Provide wheels for ``aarch64``, ``i686``, ``ppc64le``, ``s390x`` architectures on + Linux as well as ``x86_64``. + + +---- + + +1.0.0 (2019-11-09) +================== + +Deprecations and Removals +------------------------- + +- Dropped support for Python 3.5; only 3.6, 3.7 and 3.8 are supported going forward. + `#24 `_ diff --git a/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/RECORD b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..cdf56a00940ec29d8d6ac0f7d728bbf9fa5880c9 --- /dev/null +++ b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/RECORD @@ -0,0 +1,12 @@ +frozenlist-1.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +frozenlist-1.8.0.dist-info/METADATA,sha256=lGwi3J9-LHby1pjwCkMG2-Sagg4rPxoOIv5ShsvsNfE,20333 +frozenlist-1.8.0.dist-info/RECORD,, +frozenlist-1.8.0.dist-info/WHEEL,sha256=mX4U4odf6w47aVjwZUmTYd1MF9BbrhVLKlaWSvZwHEk,186 +frozenlist-1.8.0.dist-info/licenses/LICENSE,sha256=b9UkPpLdf5jsacesN3co50kFcJ_1J6W_mNbQJjwE9bY,11332 +frozenlist-1.8.0.dist-info/top_level.txt,sha256=jivtxsPXA3nK3WBWW2LW5Mtu_GHt8UZA13NeCs2cKuA,11 +frozenlist/__init__.py,sha256=xAIE2u9ncAbjATGIPfno_OJfe8AQ-1h7z_uc73dYsEA,2108 +frozenlist/__init__.pyi,sha256=vMEoES1xGegPtVXoCi9XydEeHsyuIq-KdeXwP5PdsaA,1470 +frozenlist/__pycache__/__init__.cpython-312.pyc,, +frozenlist/_frozenlist.cpython-312-x86_64-linux-gnu.so,sha256=UzFjN2uN24Tz86SdErp07aJh4LOp8A0w9EhxkDP4i3E,786872 +frozenlist/_frozenlist.pyx,sha256=t-aGjuEiVt_MZPBJ0RnraavVmPBK6arz3i48ZvXuYsU,3708 +frozenlist/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 diff --git a/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/WHEEL b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..9921a02fb6f1f01a39d88a07edea51e0d980e0dd --- /dev/null +++ b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_5_x86_64 +Tag: cp312-cp312-manylinux1_x86_64 +Tag: cp312-cp312-manylinux_2_28_x86_64 + diff --git a/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/licenses/LICENSE b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7082a2d5b9047bfc09589f387053e24ea490bc54 --- /dev/null +++ b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/licenses/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2013-2019 Nikolay Kim and Andrew Svetlov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/top_level.txt b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..52f13fc459edf8f3def6f792c432f0b64f313176 --- /dev/null +++ b/lib/python3.12/site-packages/frozenlist-1.8.0.dist-info/top_level.txt @@ -0,0 +1 @@ +frozenlist diff --git a/lib/python3.12/site-packages/httpx/__init__.py b/lib/python3.12/site-packages/httpx/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e9addde071f81758baf350c4ab6bde2556340131 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/__init__.py @@ -0,0 +1,105 @@ +from .__version__ import __description__, __title__, __version__ +from ._api import * +from ._auth import * +from ._client import * +from ._config import * +from ._content import * +from ._exceptions import * +from ._models import * +from ._status_codes import * +from ._transports import * +from ._types import * +from ._urls import * + +try: + from ._main import main +except ImportError: # pragma: no cover + + def main() -> None: # type: ignore + import sys + + print( + "The httpx command line client could not run because the required " + "dependencies were not installed.\nMake sure you've installed " + "everything with: pip install 'httpx[cli]'" + ) + sys.exit(1) + + +__all__ = [ + "__description__", + "__title__", + "__version__", + "ASGITransport", + "AsyncBaseTransport", + "AsyncByteStream", + "AsyncClient", + "AsyncHTTPTransport", + "Auth", + "BaseTransport", + "BasicAuth", + "ByteStream", + "Client", + "CloseError", + "codes", + "ConnectError", + "ConnectTimeout", + "CookieConflict", + "Cookies", + "create_ssl_context", + "DecodingError", + "delete", + "DigestAuth", + "get", + "head", + "Headers", + "HTTPError", + "HTTPStatusError", + "HTTPTransport", + "InvalidURL", + "Limits", + "LocalProtocolError", + "main", + "MockTransport", + "NetRCAuth", + "NetworkError", + "options", + "patch", + "PoolTimeout", + "post", + "ProtocolError", + "Proxy", + "ProxyError", + "put", + "QueryParams", + "ReadError", + "ReadTimeout", + "RemoteProtocolError", + "request", + "Request", + "RequestError", + "RequestNotRead", + "Response", + "ResponseNotRead", + "stream", + "StreamClosed", + "StreamConsumed", + "StreamError", + "SyncByteStream", + "Timeout", + "TimeoutException", + "TooManyRedirects", + "TransportError", + "UnsupportedProtocol", + "URL", + "USE_CLIENT_DEFAULT", + "WriteError", + "WriteTimeout", + "WSGITransport", +] + + +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + setattr(__locals[__name], "__module__", "httpx") # noqa diff --git a/lib/python3.12/site-packages/httpx/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58eae17e28d8f4b74c851c7e8c5d6268dd4a344e Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/__version__.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/__version__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eeb3131613a5ab819d43d68c7def10030a9a0a94 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/__version__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_api.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b45741603ea6413377b787c3d9bfd81f230c1600 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_api.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_auth.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0848529c661bad7887870a9cb25a340e086e70c Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_auth.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_client.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d962aa83e689d061a2c50d0bc5636a579c98f29 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_client.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_config.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7409c8e805e690c2c843e8c02b113406db0c01b4 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_content.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_content.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17ccbce617c8a8a39a91b00a00191874e199e78b Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_content.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_decoders.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_decoders.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8353710709fe5076d475959d7537ed6cba3d76d Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_decoders.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_exceptions.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b28842dbdf905ed911cfc95210625ad14f31e81 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_exceptions.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_main.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..793c1cd5f41e74ac0bc56bbf9581c72e4a209995 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_main.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_models.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d24eb1274806cfa93fe90dff846374a3038b3969 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_models.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_multipart.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_multipart.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd489527c45e1f9707757fd2d0f067ec584fb380 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_multipart.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_status_codes.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_status_codes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a4bae98478ee2bfcc05e690a557440459805e90 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_status_codes.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_types.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_types.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a175bbf26d56729e81dc1b3c48f15e354daa0aa5 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_types.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_urlparse.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_urlparse.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66fcfc46ec4039a2b33ea69e891b65a73fc48881 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_urlparse.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_urls.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_urls.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c95dacc8c279f2cc84bf40df7ae248ea97864d32 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_urls.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__pycache__/_utils.cpython-312.pyc b/lib/python3.12/site-packages/httpx/__pycache__/_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a03127453ad1de9d48640bbca282f188cd5f28b6 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/__pycache__/_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/__version__.py b/lib/python3.12/site-packages/httpx/__version__.py new file mode 100644 index 0000000000000000000000000000000000000000..801bfacf671017cfbebf1ac26ec385daa02ed260 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/__version__.py @@ -0,0 +1,3 @@ +__title__ = "httpx" +__description__ = "A next generation HTTP client, for Python 3." +__version__ = "0.28.1" diff --git a/lib/python3.12/site-packages/httpx/_api.py b/lib/python3.12/site-packages/httpx/_api.py new file mode 100644 index 0000000000000000000000000000000000000000..c3cda1ecda8629edbdca2e3bc04bc51dba5e1430 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_api.py @@ -0,0 +1,438 @@ +from __future__ import annotations + +import typing +from contextlib import contextmanager + +from ._client import Client +from ._config import DEFAULT_TIMEOUT_CONFIG +from ._models import Response +from ._types import ( + AuthTypes, + CookieTypes, + HeaderTypes, + ProxyTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestFiles, + TimeoutTypes, +) +from ._urls import URL + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + + +__all__ = [ + "delete", + "get", + "head", + "options", + "patch", + "post", + "put", + "request", + "stream", +] + + +def request( + method: str, + url: URL | str, + *, + params: QueryParamTypes | None = None, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + trust_env: bool = True, +) -> Response: + """ + Sends an HTTP request. + + **Parameters:** + + * **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`, + `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`. + * **url** - URL for the new `Request` object. + * **params** - *(optional)* Query parameters to include in the URL, as a + string, dictionary, or sequence of two-tuples. + * **content** - *(optional)* Binary content to include in the body of the + request, as bytes or a byte iterator. + * **data** - *(optional)* Form data to include in the body of the request, + as a dictionary. + * **files** - *(optional)* A dictionary of upload files to include in the + body of the request. + * **json** - *(optional)* A JSON serializable object to include in the body + of the request. + * **headers** - *(optional)* Dictionary of HTTP headers to include in the + request. + * **cookies** - *(optional)* Dictionary of Cookie items to include in the + request. + * **auth** - *(optional)* An authentication class to use when sending the + request. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. + * **timeout** - *(optional)* The timeout configuration to use when sending + the request. + * **follow_redirects** - *(optional)* Enables or disables HTTP redirects. + * **verify** - *(optional)* Either `True` to use an SSL context with the + default CA bundle, `False` to disable verification, or an instance of + `ssl.SSLContext` to use a custom context. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + + **Returns:** `Response` + + Usage: + + ``` + >>> import httpx + >>> response = httpx.request('GET', 'https://httpbin.org/get') + >>> response + + ``` + """ + with Client( + cookies=cookies, + proxy=proxy, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) as client: + return client.request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + auth=auth, + follow_redirects=follow_redirects, + ) + + +@contextmanager +def stream( + method: str, + url: URL | str, + *, + params: QueryParamTypes | None = None, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + trust_env: bool = True, +) -> typing.Iterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + with Client( + cookies=cookies, + proxy=proxy, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) as client: + with client.stream( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + auth=auth, + follow_redirects=follow_redirects, + ) as response: + yield response + + +def get( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `GET` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `GET` requests should not include a request body. + """ + return request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def options( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `OPTIONS` requests should not include a request body. + """ + return request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def head( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `HEAD` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `HEAD` requests should not include a request body. + """ + return request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def post( + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def put( + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def patch( + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + verify: ssl.SSLContext | str | bool = True, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + trust_env: bool = True, +) -> Response: + """ + Sends a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) + + +def delete( + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | None = None, + proxy: ProxyTypes | None = None, + follow_redirects: bool = False, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + verify: ssl.SSLContext | str | bool = True, + trust_env: bool = True, +) -> Response: + """ + Sends a `DELETE` request. + + **Parameters**: See `httpx.request`. + + Note that the `data`, `files`, `json` and `content` parameters are not available + on this function, as `DELETE` requests should not include a request body. + """ + return request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + proxy=proxy, + follow_redirects=follow_redirects, + verify=verify, + timeout=timeout, + trust_env=trust_env, + ) diff --git a/lib/python3.12/site-packages/httpx/_auth.py b/lib/python3.12/site-packages/httpx/_auth.py new file mode 100644 index 0000000000000000000000000000000000000000..b03971ab4b311d60790dc22ca24d9966426ec0a4 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_auth.py @@ -0,0 +1,348 @@ +from __future__ import annotations + +import hashlib +import os +import re +import time +import typing +from base64 import b64encode +from urllib.request import parse_http_list + +from ._exceptions import ProtocolError +from ._models import Cookies, Request, Response +from ._utils import to_bytes, to_str, unquote + +if typing.TYPE_CHECKING: # pragma: no cover + from hashlib import _Hash + + +__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"] + + +class Auth: + """ + Base class for all authentication schemes. + + To implement a custom authentication scheme, subclass `Auth` and override + the `.auth_flow()` method. + + If the authentication scheme does I/O such as disk access or network calls, or uses + synchronization primitives such as locks, you should override `.sync_auth_flow()` + and/or `.async_auth_flow()` instead of `.auth_flow()` to provide specialized + implementations that will be used by `Client` and `AsyncClient` respectively. + """ + + requires_request_body = False + requires_response_body = False + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + """ + Execute the authentication flow. + + To dispatch a request, `yield` it: + + ``` + yield request + ``` + + The client will `.send()` the response back into the flow generator. You can + access it like so: + + ``` + response = yield request + ``` + + A `return` (or reaching the end of the generator) will result in the + client returning the last response obtained from the server. + + You can dispatch as many requests as is necessary. + """ + yield request + + def sync_auth_flow( + self, request: Request + ) -> typing.Generator[Request, Response, None]: + """ + Execute the authentication flow synchronously. + + By default, this defers to `.auth_flow()`. You should override this method + when the authentication scheme does I/O and/or uses concurrency primitives. + """ + if self.requires_request_body: + request.read() + + flow = self.auth_flow(request) + request = next(flow) + + while True: + response = yield request + if self.requires_response_body: + response.read() + + try: + request = flow.send(response) + except StopIteration: + break + + async def async_auth_flow( + self, request: Request + ) -> typing.AsyncGenerator[Request, Response]: + """ + Execute the authentication flow asynchronously. + + By default, this defers to `.auth_flow()`. You should override this method + when the authentication scheme does I/O and/or uses concurrency primitives. + """ + if self.requires_request_body: + await request.aread() + + flow = self.auth_flow(request) + request = next(flow) + + while True: + response = yield request + if self.requires_response_body: + await response.aread() + + try: + request = flow.send(response) + except StopIteration: + break + + +class FunctionAuth(Auth): + """ + Allows the 'auth' argument to be passed as a simple callable function, + that takes the request, and returns a new, modified request. + """ + + def __init__(self, func: typing.Callable[[Request], Request]) -> None: + self._func = func + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + yield self._func(request) + + +class BasicAuth(Auth): + """ + Allows the 'auth' argument to be passed as a (username, password) pair, + and uses HTTP Basic authentication. + """ + + def __init__(self, username: str | bytes, password: str | bytes) -> None: + self._auth_header = self._build_auth_header(username, password) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + request.headers["Authorization"] = self._auth_header + yield request + + def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str: + userpass = b":".join((to_bytes(username), to_bytes(password))) + token = b64encode(userpass).decode() + return f"Basic {token}" + + +class NetRCAuth(Auth): + """ + Use a 'netrc' file to lookup basic auth credentials based on the url host. + """ + + def __init__(self, file: str | None = None) -> None: + # Lazily import 'netrc'. + # There's no need for us to load this module unless 'NetRCAuth' is being used. + import netrc + + self._netrc_info = netrc.netrc(file) + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + auth_info = self._netrc_info.authenticators(request.url.host) + if auth_info is None or not auth_info[2]: + # The netrc file did not have authentication credentials for this host. + yield request + else: + # Build a basic auth header with credentials from the netrc file. + request.headers["Authorization"] = self._build_auth_header( + username=auth_info[0], password=auth_info[2] + ) + yield request + + def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str: + userpass = b":".join((to_bytes(username), to_bytes(password))) + token = b64encode(userpass).decode() + return f"Basic {token}" + + +class DigestAuth(Auth): + _ALGORITHM_TO_HASH_FUNCTION: dict[str, typing.Callable[[bytes], _Hash]] = { + "MD5": hashlib.md5, + "MD5-SESS": hashlib.md5, + "SHA": hashlib.sha1, + "SHA-SESS": hashlib.sha1, + "SHA-256": hashlib.sha256, + "SHA-256-SESS": hashlib.sha256, + "SHA-512": hashlib.sha512, + "SHA-512-SESS": hashlib.sha512, + } + + def __init__(self, username: str | bytes, password: str | bytes) -> None: + self._username = to_bytes(username) + self._password = to_bytes(password) + self._last_challenge: _DigestAuthChallenge | None = None + self._nonce_count = 1 + + def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]: + if self._last_challenge: + request.headers["Authorization"] = self._build_auth_header( + request, self._last_challenge + ) + + response = yield request + + if response.status_code != 401 or "www-authenticate" not in response.headers: + # If the response is not a 401 then we don't + # need to build an authenticated request. + return + + for auth_header in response.headers.get_list("www-authenticate"): + if auth_header.lower().startswith("digest "): + break + else: + # If the response does not include a 'WWW-Authenticate: Digest ...' + # header, then we don't need to build an authenticated request. + return + + self._last_challenge = self._parse_challenge(request, response, auth_header) + self._nonce_count = 1 + + request.headers["Authorization"] = self._build_auth_header( + request, self._last_challenge + ) + if response.cookies: + Cookies(response.cookies).set_cookie_header(request=request) + yield request + + def _parse_challenge( + self, request: Request, response: Response, auth_header: str + ) -> _DigestAuthChallenge: + """ + Returns a challenge from a Digest WWW-Authenticate header. + These take the form of: + `Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"` + """ + scheme, _, fields = auth_header.partition(" ") + + # This method should only ever have been called with a Digest auth header. + assert scheme.lower() == "digest" + + header_dict: dict[str, str] = {} + for field in parse_http_list(fields): + key, value = field.strip().split("=", 1) + header_dict[key] = unquote(value) + + try: + realm = header_dict["realm"].encode() + nonce = header_dict["nonce"].encode() + algorithm = header_dict.get("algorithm", "MD5") + opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None + qop = header_dict["qop"].encode() if "qop" in header_dict else None + return _DigestAuthChallenge( + realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop + ) + except KeyError as exc: + message = "Malformed Digest WWW-Authenticate header" + raise ProtocolError(message, request=request) from exc + + def _build_auth_header( + self, request: Request, challenge: _DigestAuthChallenge + ) -> str: + hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()] + + def digest(data: bytes) -> bytes: + return hash_func(data).hexdigest().encode() + + A1 = b":".join((self._username, challenge.realm, self._password)) + + path = request.url.raw_path + A2 = b":".join((request.method.encode(), path)) + # TODO: implement auth-int + HA2 = digest(A2) + + nc_value = b"%08x" % self._nonce_count + cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce) + self._nonce_count += 1 + + HA1 = digest(A1) + if challenge.algorithm.lower().endswith("-sess"): + HA1 = digest(b":".join((HA1, challenge.nonce, cnonce))) + + qop = self._resolve_qop(challenge.qop, request=request) + if qop is None: + # Following RFC 2069 + digest_data = [HA1, challenge.nonce, HA2] + else: + # Following RFC 2617/7616 + digest_data = [HA1, challenge.nonce, nc_value, cnonce, qop, HA2] + + format_args = { + "username": self._username, + "realm": challenge.realm, + "nonce": challenge.nonce, + "uri": path, + "response": digest(b":".join(digest_data)), + "algorithm": challenge.algorithm.encode(), + } + if challenge.opaque: + format_args["opaque"] = challenge.opaque + if qop: + format_args["qop"] = b"auth" + format_args["nc"] = nc_value + format_args["cnonce"] = cnonce + + return "Digest " + self._get_header_value(format_args) + + def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes: + s = str(nonce_count).encode() + s += nonce + s += time.ctime().encode() + s += os.urandom(8) + + return hashlib.sha1(s).hexdigest()[:16].encode() + + def _get_header_value(self, header_fields: dict[str, bytes]) -> str: + NON_QUOTED_FIELDS = ("algorithm", "qop", "nc") + QUOTED_TEMPLATE = '{}="{}"' + NON_QUOTED_TEMPLATE = "{}={}" + + header_value = "" + for i, (field, value) in enumerate(header_fields.items()): + if i > 0: + header_value += ", " + template = ( + QUOTED_TEMPLATE + if field not in NON_QUOTED_FIELDS + else NON_QUOTED_TEMPLATE + ) + header_value += template.format(field, to_str(value)) + + return header_value + + def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None: + if qop is None: + return None + qops = re.split(b", ?", qop) + if b"auth" in qops: + return b"auth" + + if qops == [b"auth-int"]: + raise NotImplementedError("Digest auth-int support is not yet implemented") + + message = f'Unexpected qop value "{qop!r}" in digest auth' + raise ProtocolError(message, request=request) + + +class _DigestAuthChallenge(typing.NamedTuple): + realm: bytes + nonce: bytes + algorithm: str + opaque: bytes | None + qop: bytes | None diff --git a/lib/python3.12/site-packages/httpx/_client.py b/lib/python3.12/site-packages/httpx/_client.py new file mode 100644 index 0000000000000000000000000000000000000000..2249231f8c3b912c731ff160344d3672e2f11738 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_client.py @@ -0,0 +1,2019 @@ +from __future__ import annotations + +import datetime +import enum +import logging +import time +import typing +import warnings +from contextlib import asynccontextmanager, contextmanager +from types import TracebackType + +from .__version__ import __version__ +from ._auth import Auth, BasicAuth, FunctionAuth +from ._config import ( + DEFAULT_LIMITS, + DEFAULT_MAX_REDIRECTS, + DEFAULT_TIMEOUT_CONFIG, + Limits, + Proxy, + Timeout, +) +from ._decoders import SUPPORTED_DECODERS +from ._exceptions import ( + InvalidURL, + RemoteProtocolError, + TooManyRedirects, + request_context, +) +from ._models import Cookies, Headers, Request, Response +from ._status_codes import codes +from ._transports.base import AsyncBaseTransport, BaseTransport +from ._transports.default import AsyncHTTPTransport, HTTPTransport +from ._types import ( + AsyncByteStream, + AuthTypes, + CertTypes, + CookieTypes, + HeaderTypes, + ProxyTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestExtensions, + RequestFiles, + SyncByteStream, + TimeoutTypes, +) +from ._urls import URL, QueryParams +from ._utils import URLPattern, get_environment_proxies + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + +__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"] + +# The type annotation for @classmethod and context managers here follows PEP 484 +# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods +T = typing.TypeVar("T", bound="Client") +U = typing.TypeVar("U", bound="AsyncClient") + + +def _is_https_redirect(url: URL, location: URL) -> bool: + """ + Return 'True' if 'location' is a HTTPS upgrade of 'url' + """ + if url.host != location.host: + return False + + return ( + url.scheme == "http" + and _port_or_default(url) == 80 + and location.scheme == "https" + and _port_or_default(location) == 443 + ) + + +def _port_or_default(url: URL) -> int | None: + if url.port is not None: + return url.port + return {"http": 80, "https": 443}.get(url.scheme) + + +def _same_origin(url: URL, other: URL) -> bool: + """ + Return 'True' if the given URLs share the same origin. + """ + return ( + url.scheme == other.scheme + and url.host == other.host + and _port_or_default(url) == _port_or_default(other) + ) + + +class UseClientDefault: + """ + For some parameters such as `auth=...` and `timeout=...` we need to be able + to indicate the default "unset" state, in a way that is distinctly different + to using `None`. + + The default "unset" state indicates that whatever default is set on the + client should be used. This is different to setting `None`, which + explicitly disables the parameter, possibly overriding a client default. + + For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature. + Omitting the `timeout` parameter will send a request using whatever default + timeout has been configured on the client. Including `timeout=None` will + ensure no timeout is used. + + Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant, + but it is used internally when a parameter is not included. + """ + + +USE_CLIENT_DEFAULT = UseClientDefault() + + +logger = logging.getLogger("httpx") + +USER_AGENT = f"python-httpx/{__version__}" +ACCEPT_ENCODING = ", ".join( + [key for key in SUPPORTED_DECODERS.keys() if key != "identity"] +) + + +class ClientState(enum.Enum): + # UNOPENED: + # The client has been instantiated, but has not been used to send a request, + # or been opened by entering the context of a `with` block. + UNOPENED = 1 + # OPENED: + # The client has either sent a request, or is within a `with` block. + OPENED = 2 + # CLOSED: + # The client has either exited the `with` block, or `close()` has + # been called explicitly. + CLOSED = 3 + + +class BoundSyncStream(SyncByteStream): + """ + A byte stream that is bound to a given response instance, and that + ensures the `response.elapsed` is set once the response is closed. + """ + + def __init__( + self, stream: SyncByteStream, response: Response, start: float + ) -> None: + self._stream = stream + self._response = response + self._start = start + + def __iter__(self) -> typing.Iterator[bytes]: + for chunk in self._stream: + yield chunk + + def close(self) -> None: + elapsed = time.perf_counter() - self._start + self._response.elapsed = datetime.timedelta(seconds=elapsed) + self._stream.close() + + +class BoundAsyncStream(AsyncByteStream): + """ + An async byte stream that is bound to a given response instance, and that + ensures the `response.elapsed` is set once the response is closed. + """ + + def __init__( + self, stream: AsyncByteStream, response: Response, start: float + ) -> None: + self._stream = stream + self._response = response + self._start = start + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + async for chunk in self._stream: + yield chunk + + async def aclose(self) -> None: + elapsed = time.perf_counter() - self._start + self._response.elapsed = datetime.timedelta(seconds=elapsed) + await self._stream.aclose() + + +EventHook = typing.Callable[..., typing.Any] + + +class BaseClient: + def __init__( + self, + *, + auth: AuthTypes | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, + base_url: URL | str = "", + trust_env: bool = True, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + event_hooks = {} if event_hooks is None else event_hooks + + self._base_url = self._enforce_trailing_slash(URL(base_url)) + + self._auth = self._build_auth(auth) + self._params = QueryParams(params) + self.headers = Headers(headers) + self._cookies = Cookies(cookies) + self._timeout = Timeout(timeout) + self.follow_redirects = follow_redirects + self.max_redirects = max_redirects + self._event_hooks = { + "request": list(event_hooks.get("request", [])), + "response": list(event_hooks.get("response", [])), + } + self._trust_env = trust_env + self._default_encoding = default_encoding + self._state = ClientState.UNOPENED + + @property + def is_closed(self) -> bool: + """ + Check if the client being closed + """ + return self._state == ClientState.CLOSED + + @property + def trust_env(self) -> bool: + return self._trust_env + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _get_proxy_map( + self, proxy: ProxyTypes | None, allow_env_proxies: bool + ) -> dict[str, Proxy | None]: + if proxy is None: + if allow_env_proxies: + return { + key: None if url is None else Proxy(url=url) + for key, url in get_environment_proxies().items() + } + return {} + else: + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy + return {"all://": proxy} + + @property + def timeout(self) -> Timeout: + return self._timeout + + @timeout.setter + def timeout(self, timeout: TimeoutTypes) -> None: + self._timeout = Timeout(timeout) + + @property + def event_hooks(self) -> dict[str, list[EventHook]]: + return self._event_hooks + + @event_hooks.setter + def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None: + self._event_hooks = { + "request": list(event_hooks.get("request", [])), + "response": list(event_hooks.get("response", [])), + } + + @property + def auth(self) -> Auth | None: + """ + Authentication class used when none is passed at the request-level. + + See also [Authentication][0]. + + [0]: /quickstart/#authentication + """ + return self._auth + + @auth.setter + def auth(self, auth: AuthTypes) -> None: + self._auth = self._build_auth(auth) + + @property + def base_url(self) -> URL: + """ + Base URL to use when sending requests with relative URLs. + """ + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(URL(url)) + + @property + def headers(self) -> Headers: + """ + HTTP headers to include when sending requests. + """ + return self._headers + + @headers.setter + def headers(self, headers: HeaderTypes) -> None: + client_headers = Headers( + { + b"Accept": b"*/*", + b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"), + b"Connection": b"keep-alive", + b"User-Agent": USER_AGENT.encode("ascii"), + } + ) + client_headers.update(headers) + self._headers = client_headers + + @property + def cookies(self) -> Cookies: + """ + Cookie values to include when sending requests. + """ + return self._cookies + + @cookies.setter + def cookies(self, cookies: CookieTypes) -> None: + self._cookies = Cookies(cookies) + + @property + def params(self) -> QueryParams: + """ + Query parameters to include in the URL when sending requests. + """ + return self._params + + @params.setter + def params(self, params: QueryParamTypes) -> None: + self._params = QueryParams(params) + + def build_request( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Request: + """ + Build and return a request instance. + + * The `params`, `headers` and `cookies` arguments + are merged with any values set on the client. + * The `url` argument is merged with any `base_url` set on the client. + + See also: [Request instances][0] + + [0]: /advanced/clients/#request-instances + """ + url = self._merge_url(url) + headers = self._merge_headers(headers) + cookies = self._merge_cookies(cookies) + params = self._merge_queryparams(params) + extensions = {} if extensions is None else extensions + if "timeout" not in extensions: + timeout = ( + self.timeout + if isinstance(timeout, UseClientDefault) + else Timeout(timeout) + ) + extensions = dict(**extensions, timeout=timeout.as_dict()) + return Request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + extensions=extensions, + ) + + def _merge_url(self, url: URL | str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + merge_url = URL(url) + if merge_url.is_relative_url: + # To merge URLs we always append to the base URL. To get this + # behaviour correct we always ensure the base URL ends in a '/' + # separator, and strip any leading '/' from the merge URL. + # + # So, eg... + # + # >>> client = Client(base_url="https://www.example.com/subpath") + # >>> client.base_url + # URL('https://www.example.com/subpath/') + # >>> client.build_request("GET", "/path").url + # URL('https://www.example.com/subpath/path') + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + return merge_url + + def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None: + """ + Merge a cookies argument together with any cookies on the client, + to create the cookies used for the outgoing request. + """ + if cookies or self.cookies: + merged_cookies = Cookies(self.cookies) + merged_cookies.update(cookies) + return merged_cookies + return cookies + + def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None: + """ + Merge a headers argument together with any headers on the client, + to create the headers used for the outgoing request. + """ + merged_headers = Headers(self.headers) + merged_headers.update(headers) + return merged_headers + + def _merge_queryparams( + self, params: QueryParamTypes | None = None + ) -> QueryParamTypes | None: + """ + Merge a queryparams argument together with any queryparams on the client, + to create the queryparams used for the outgoing request. + """ + if params or self.params: + merged_queryparams = QueryParams(self.params) + return merged_queryparams.merge(params) + return params + + def _build_auth(self, auth: AuthTypes | None) -> Auth | None: + if auth is None: + return None + elif isinstance(auth, tuple): + return BasicAuth(username=auth[0], password=auth[1]) + elif isinstance(auth, Auth): + return auth + elif callable(auth): + return FunctionAuth(func=auth) + else: + raise TypeError(f'Invalid "auth" argument: {auth!r}') + + def _build_request_auth( + self, + request: Request, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + ) -> Auth: + auth = ( + self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth) + ) + + if auth is not None: + return auth + + username, password = request.url.username, request.url.password + if username or password: + return BasicAuth(username=username, password=password) + + return Auth() + + def _build_redirect_request(self, request: Request, response: Response) -> Request: + """ + Given a request and a redirect response, return a new request that + should be used to effect the redirect. + """ + method = self._redirect_method(request, response) + url = self._redirect_url(request, response) + headers = self._redirect_headers(request, url, method) + stream = self._redirect_stream(request, method) + cookies = Cookies(self.cookies) + return Request( + method=method, + url=url, + headers=headers, + cookies=cookies, + stream=stream, + extensions=request.extensions, + ) + + def _redirect_method(self, request: Request, response: Response) -> str: + """ + When being redirected we may want to change the method of the request + based on certain specs or browser behavior. + """ + method = request.method + + # https://tools.ietf.org/html/rfc7231#section-6.4.4 + if response.status_code == codes.SEE_OTHER and method != "HEAD": + method = "GET" + + # Do what the browsers do, despite standards... + # Turn 302s into GETs. + if response.status_code == codes.FOUND and method != "HEAD": + method = "GET" + + # If a POST is responded to with a 301, turn it into a GET. + # This bizarre behaviour is explained in 'requests' issue 1704. + if response.status_code == codes.MOVED_PERMANENTLY and method == "POST": + method = "GET" + + return method + + def _redirect_url(self, request: Request, response: Response) -> URL: + """ + Return the URL for the redirect to follow. + """ + location = response.headers["Location"] + + try: + url = URL(location) + except InvalidURL as exc: + raise RemoteProtocolError( + f"Invalid URL in location header: {exc}.", request=request + ) from None + + # Handle malformed 'Location' headers that are "absolute" form, have no host. + # See: https://github.com/encode/httpx/issues/771 + if url.scheme and not url.host: + url = url.copy_with(host=request.url.host) + + # Facilitate relative 'Location' headers, as allowed by RFC 7231. + # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') + if url.is_relative_url: + url = request.url.join(url) + + # Attach previous fragment if needed (RFC 7231 7.1.2) + if request.url.fragment and not url.fragment: + url = url.copy_with(fragment=request.url.fragment) + + return url + + def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: + """ + Return the headers that should be used for the redirect request. + """ + headers = Headers(request.headers) + + if not _same_origin(url, request.url): + if not _is_https_redirect(request.url, url): + # Strip Authorization headers when responses are redirected + # away from the origin. (Except for direct HTTP to HTTPS redirects.) + headers.pop("Authorization", None) + + # Update the Host header. + headers["Host"] = url.netloc.decode("ascii") + + if method != request.method and method == "GET": + # If we've switch to a 'GET' request, then strip any headers which + # are only relevant to the request body. + headers.pop("Content-Length", None) + headers.pop("Transfer-Encoding", None) + + # We should use the client cookie store to determine any cookie header, + # rather than whatever was on the original outgoing request. + headers.pop("Cookie", None) + + return headers + + def _redirect_stream( + self, request: Request, method: str + ) -> SyncByteStream | AsyncByteStream | None: + """ + Return the body that should be used for the redirect request. + """ + if method != request.method and method == "GET": + return None + + return request.stream + + def _set_timeout(self, request: Request) -> None: + if "timeout" not in request.extensions: + timeout = ( + self.timeout + if isinstance(self.timeout, UseClientDefault) + else Timeout(self.timeout) + ) + request.extensions = dict(**request.extensions, timeout=timeout.as_dict()) + + +class Client(BaseClient): + """ + An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc. + + It can be shared between threads. + + Usage: + + ```python + >>> client = httpx.Client() + >>> response = client.get('https://example.org') + ``` + + **Parameters:** + + * **auth** - *(optional)* An authentication class to use when sending + requests. + * **params** - *(optional)* Query parameters to include in request URLs, as + a string, dictionary, or sequence of two-tuples. + * **headers** - *(optional)* Dictionary of HTTP headers to include when + sending requests. + * **cookies** - *(optional)* Dictionary of Cookie items to include when + sending requests. + * **verify** - *(optional)* Either `True` to use an SSL context with the + default CA bundle, `False` to disable verification, or an instance of + `ssl.SSLContext` to use a custom context. + * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be + enabled. Defaults to `False`. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. + * **timeout** - *(optional)* The timeout configuration to use when sending + requests. + * **limits** - *(optional)* The limits configuration to use. + * **max_redirects** - *(optional)* The maximum number of redirect responses + that should be followed. + * **base_url** - *(optional)* A URL to use as the base when building + request URLs. + * **transport** - *(optional)* A transport class to use for sending requests + over the network. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + * **default_encoding** - *(optional)* The default encoding to use for decoding + response text, if no charset information is included in a response Content-Type + header. Set to a callable for automatic character set detection. Default: "utf-8". + """ + + def __init__( + self, + *, + auth: AuthTypes | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + proxy: ProxyTypes | None = None, + mounts: None | (typing.Mapping[str, BaseTransport | None]) = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + limits: Limits = DEFAULT_LIMITS, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, + base_url: URL | str = "", + transport: BaseTransport | None = None, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + super().__init__( + auth=auth, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + follow_redirects=follow_redirects, + max_redirects=max_redirects, + event_hooks=event_hooks, + base_url=base_url, + trust_env=trust_env, + default_encoding=default_encoding, + ) + + if http2: + try: + import h2 # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using http2=True, but the 'h2' package is not installed. " + "Make sure to install httpx using `pip install httpx[http2]`." + ) from None + + allow_env_proxies = trust_env and transport is None + proxy_map = self._get_proxy_map(proxy, allow_env_proxies) + + self._transport = self._init_transport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + transport=transport, + ) + self._mounts: dict[URLPattern, BaseTransport | None] = { + URLPattern(key): None + if proxy is None + else self._init_proxy_transport( + proxy, + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + for key, proxy in proxy_map.items() + } + if mounts is not None: + self._mounts.update( + {URLPattern(key): transport for key, transport in mounts.items()} + ) + + self._mounts = dict(sorted(self._mounts.items())) + + def _init_transport( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + transport: BaseTransport | None = None, + ) -> BaseTransport: + if transport is not None: + return transport + + return HTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + + def _init_proxy_transport( + self, + proxy: Proxy, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + ) -> BaseTransport: + return HTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + proxy=proxy, + ) + + def _transport_for_url(self, url: URL) -> BaseTransport: + """ + Returns the transport instance that should be used for a given URL. + This will either be the standard connection pool, or a proxy. + """ + for pattern, transport in self._mounts.items(): + if pattern.matches(url): + return self._transport if transport is None else transport + + return self._transport + + def request( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Build and send a request. + + Equivalent to: + + ```python + request = client.build_request(...) + response = client.send(request, ...) + ``` + + See `Client.build_request()`, `Client.send()` and + [Merging of configuration][0] for how the various parameters + are merged with client-level configuration. + + [0]: /advanced/clients/#merging-of-configuration + """ + if cookies is not None: + message = ( + "Setting per-request cookies=<...> is being deprecated, because " + "the expected behaviour on cookie persistence is ambiguous. Set " + "cookies directly on the client instance instead." + ) + warnings.warn(message, DeprecationWarning, stacklevel=2) + + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + return self.send(request, auth=auth, follow_redirects=follow_redirects) + + @contextmanager + def stream( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> typing.Iterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + response = self.send( + request=request, + auth=auth, + follow_redirects=follow_redirects, + stream=True, + ) + try: + yield response + finally: + response.close() + + def send( + self, + request: Request, + *, + stream: bool = False, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + ) -> Response: + """ + Send a request. + + The request is sent as-is, unmodified. + + Typically you'll want to build one with `Client.build_request()` + so that any client-level configuration is merged into the request, + but passing an explicit `httpx.Request()` is supported as well. + + See also: [Request instances][0] + + [0]: /advanced/clients/#request-instances + """ + if self._state == ClientState.CLOSED: + raise RuntimeError("Cannot send a request, as the client has been closed.") + + self._state = ClientState.OPENED + follow_redirects = ( + self.follow_redirects + if isinstance(follow_redirects, UseClientDefault) + else follow_redirects + ) + + self._set_timeout(request) + + auth = self._build_request_auth(request, auth) + + response = self._send_handling_auth( + request, + auth=auth, + follow_redirects=follow_redirects, + history=[], + ) + try: + if not stream: + response.read() + + return response + + except BaseException as exc: + response.close() + raise exc + + def _send_handling_auth( + self, + request: Request, + auth: Auth, + follow_redirects: bool, + history: list[Response], + ) -> Response: + auth_flow = auth.sync_auth_flow(request) + try: + request = next(auth_flow) + + while True: + response = self._send_handling_redirects( + request, + follow_redirects=follow_redirects, + history=history, + ) + try: + try: + next_request = auth_flow.send(response) + except StopIteration: + return response + + response.history = list(history) + response.read() + request = next_request + history.append(response) + + except BaseException as exc: + response.close() + raise exc + finally: + auth_flow.close() + + def _send_handling_redirects( + self, + request: Request, + follow_redirects: bool, + history: list[Response], + ) -> Response: + while True: + if len(history) > self.max_redirects: + raise TooManyRedirects( + "Exceeded maximum allowed redirects.", request=request + ) + + for hook in self._event_hooks["request"]: + hook(request) + + response = self._send_single_request(request) + try: + for hook in self._event_hooks["response"]: + hook(response) + response.history = list(history) + + if not response.has_redirect_location: + return response + + request = self._build_redirect_request(request, response) + history = history + [response] + + if follow_redirects: + response.read() + else: + response.next_request = request + return response + + except BaseException as exc: + response.close() + raise exc + + def _send_single_request(self, request: Request) -> Response: + """ + Sends a single request, without handling any redirections. + """ + transport = self._transport_for_url(request.url) + start = time.perf_counter() + + if not isinstance(request.stream, SyncByteStream): + raise RuntimeError( + "Attempted to send an async request with a sync Client instance." + ) + + with request_context(request=request): + response = transport.handle_request(request) + + assert isinstance(response.stream, SyncByteStream) + + response.request = request + response.stream = BoundSyncStream( + response.stream, response=response, start=start + ) + self.cookies.extract_cookies(response) + response.default_encoding = self._default_encoding + + logger.info( + 'HTTP Request: %s %s "%s %d %s"', + request.method, + request.url, + response.http_version, + response.status_code, + response.reason_phrase, + ) + + return response + + def get( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `GET` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def options( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def head( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `HEAD` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def post( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def put( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def patch( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def delete( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `DELETE` request. + + **Parameters**: See `httpx.request`. + """ + return self.request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + def close(self) -> None: + """ + Close transport and proxies. + """ + if self._state != ClientState.CLOSED: + self._state = ClientState.CLOSED + + self._transport.close() + for transport in self._mounts.values(): + if transport is not None: + transport.close() + + def __enter__(self: T) -> T: + if self._state != ClientState.UNOPENED: + msg = { + ClientState.OPENED: "Cannot open a client instance more than once.", + ClientState.CLOSED: ( + "Cannot reopen a client instance, once it has been closed." + ), + }[self._state] + raise RuntimeError(msg) + + self._state = ClientState.OPENED + + self._transport.__enter__() + for transport in self._mounts.values(): + if transport is not None: + transport.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self._state = ClientState.CLOSED + + self._transport.__exit__(exc_type, exc_value, traceback) + for transport in self._mounts.values(): + if transport is not None: + transport.__exit__(exc_type, exc_value, traceback) + + +class AsyncClient(BaseClient): + """ + An asynchronous HTTP client, with connection pooling, HTTP/2, redirects, + cookie persistence, etc. + + It can be shared between tasks. + + Usage: + + ```python + >>> async with httpx.AsyncClient() as client: + >>> response = await client.get('https://example.org') + ``` + + **Parameters:** + + * **auth** - *(optional)* An authentication class to use when sending + requests. + * **params** - *(optional)* Query parameters to include in request URLs, as + a string, dictionary, or sequence of two-tuples. + * **headers** - *(optional)* Dictionary of HTTP headers to include when + sending requests. + * **cookies** - *(optional)* Dictionary of Cookie items to include when + sending requests. + * **verify** - *(optional)* Either `True` to use an SSL context with the + default CA bundle, `False` to disable verification, or an instance of + `ssl.SSLContext` to use a custom context. + * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be + enabled. Defaults to `False`. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. + * **timeout** - *(optional)* The timeout configuration to use when sending + requests. + * **limits** - *(optional)* The limits configuration to use. + * **max_redirects** - *(optional)* The maximum number of redirect responses + that should be followed. + * **base_url** - *(optional)* A URL to use as the base when building + request URLs. + * **transport** - *(optional)* A transport class to use for sending requests + over the network. + * **trust_env** - *(optional)* Enables or disables usage of environment + variables for configuration. + * **default_encoding** - *(optional)* The default encoding to use for decoding + response text, if no charset information is included in a response Content-Type + header. Set to a callable for automatic character set detection. Default: "utf-8". + """ + + def __init__( + self, + *, + auth: AuthTypes | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + http1: bool = True, + http2: bool = False, + proxy: ProxyTypes | None = None, + mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None, + timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, + follow_redirects: bool = False, + limits: Limits = DEFAULT_LIMITS, + max_redirects: int = DEFAULT_MAX_REDIRECTS, + event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, + base_url: URL | str = "", + transport: AsyncBaseTransport | None = None, + trust_env: bool = True, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + super().__init__( + auth=auth, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + follow_redirects=follow_redirects, + max_redirects=max_redirects, + event_hooks=event_hooks, + base_url=base_url, + trust_env=trust_env, + default_encoding=default_encoding, + ) + + if http2: + try: + import h2 # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using http2=True, but the 'h2' package is not installed. " + "Make sure to install httpx using `pip install httpx[http2]`." + ) from None + + allow_env_proxies = trust_env and transport is None + proxy_map = self._get_proxy_map(proxy, allow_env_proxies) + + self._transport = self._init_transport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + transport=transport, + ) + + self._mounts: dict[URLPattern, AsyncBaseTransport | None] = { + URLPattern(key): None + if proxy is None + else self._init_proxy_transport( + proxy, + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + for key, proxy in proxy_map.items() + } + if mounts is not None: + self._mounts.update( + {URLPattern(key): transport for key, transport in mounts.items()} + ) + self._mounts = dict(sorted(self._mounts.items())) + + def _init_transport( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + transport: AsyncBaseTransport | None = None, + ) -> AsyncBaseTransport: + if transport is not None: + return transport + + return AsyncHTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + ) + + def _init_proxy_transport( + self, + proxy: Proxy, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + ) -> AsyncBaseTransport: + return AsyncHTTPTransport( + verify=verify, + cert=cert, + trust_env=trust_env, + http1=http1, + http2=http2, + limits=limits, + proxy=proxy, + ) + + def _transport_for_url(self, url: URL) -> AsyncBaseTransport: + """ + Returns the transport instance that should be used for a given URL. + This will either be the standard connection pool, or a proxy. + """ + for pattern, transport in self._mounts.items(): + if pattern.matches(url): + return self._transport if transport is None else transport + + return self._transport + + async def request( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Build and send a request. + + Equivalent to: + + ```python + request = client.build_request(...) + response = await client.send(request, ...) + ``` + + See `AsyncClient.build_request()`, `AsyncClient.send()` + and [Merging of configuration][0] for how the various parameters + are merged with client-level configuration. + + [0]: /advanced/clients/#merging-of-configuration + """ + + if cookies is not None: # pragma: no cover + message = ( + "Setting per-request cookies=<...> is being deprecated, because " + "the expected behaviour on cookie persistence is ambiguous. Set " + "cookies directly on the client instance instead." + ) + warnings.warn(message, DeprecationWarning, stacklevel=2) + + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + return await self.send(request, auth=auth, follow_redirects=follow_redirects) + + @asynccontextmanager + async def stream( + self, + method: str, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> typing.AsyncIterator[Response]: + """ + Alternative to `httpx.request()` that streams the response body + instead of loading it into memory at once. + + **Parameters**: See `httpx.request`. + + See also: [Streaming Responses][0] + + [0]: /quickstart#streaming-responses + """ + request = self.build_request( + method=method, + url=url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + response = await self.send( + request=request, + auth=auth, + follow_redirects=follow_redirects, + stream=True, + ) + try: + yield response + finally: + await response.aclose() + + async def send( + self, + request: Request, + *, + stream: bool = False, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + ) -> Response: + """ + Send a request. + + The request is sent as-is, unmodified. + + Typically you'll want to build one with `AsyncClient.build_request()` + so that any client-level configuration is merged into the request, + but passing an explicit `httpx.Request()` is supported as well. + + See also: [Request instances][0] + + [0]: /advanced/clients/#request-instances + """ + if self._state == ClientState.CLOSED: + raise RuntimeError("Cannot send a request, as the client has been closed.") + + self._state = ClientState.OPENED + follow_redirects = ( + self.follow_redirects + if isinstance(follow_redirects, UseClientDefault) + else follow_redirects + ) + + self._set_timeout(request) + + auth = self._build_request_auth(request, auth) + + response = await self._send_handling_auth( + request, + auth=auth, + follow_redirects=follow_redirects, + history=[], + ) + try: + if not stream: + await response.aread() + + return response + + except BaseException as exc: + await response.aclose() + raise exc + + async def _send_handling_auth( + self, + request: Request, + auth: Auth, + follow_redirects: bool, + history: list[Response], + ) -> Response: + auth_flow = auth.async_auth_flow(request) + try: + request = await auth_flow.__anext__() + + while True: + response = await self._send_handling_redirects( + request, + follow_redirects=follow_redirects, + history=history, + ) + try: + try: + next_request = await auth_flow.asend(response) + except StopAsyncIteration: + return response + + response.history = list(history) + await response.aread() + request = next_request + history.append(response) + + except BaseException as exc: + await response.aclose() + raise exc + finally: + await auth_flow.aclose() + + async def _send_handling_redirects( + self, + request: Request, + follow_redirects: bool, + history: list[Response], + ) -> Response: + while True: + if len(history) > self.max_redirects: + raise TooManyRedirects( + "Exceeded maximum allowed redirects.", request=request + ) + + for hook in self._event_hooks["request"]: + await hook(request) + + response = await self._send_single_request(request) + try: + for hook in self._event_hooks["response"]: + await hook(response) + + response.history = list(history) + + if not response.has_redirect_location: + return response + + request = self._build_redirect_request(request, response) + history = history + [response] + + if follow_redirects: + await response.aread() + else: + response.next_request = request + return response + + except BaseException as exc: + await response.aclose() + raise exc + + async def _send_single_request(self, request: Request) -> Response: + """ + Sends a single request, without handling any redirections. + """ + transport = self._transport_for_url(request.url) + start = time.perf_counter() + + if not isinstance(request.stream, AsyncByteStream): + raise RuntimeError( + "Attempted to send an sync request with an AsyncClient instance." + ) + + with request_context(request=request): + response = await transport.handle_async_request(request) + + assert isinstance(response.stream, AsyncByteStream) + response.request = request + response.stream = BoundAsyncStream( + response.stream, response=response, start=start + ) + self.cookies.extract_cookies(response) + response.default_encoding = self._default_encoding + + logger.info( + 'HTTP Request: %s %s "%s %d %s"', + request.method, + request.url, + response.http_version, + response.status_code, + response.reason_phrase, + ) + + return response + + async def get( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `GET` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def options( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send an `OPTIONS` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "OPTIONS", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def head( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `HEAD` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "HEAD", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def post( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `POST` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "POST", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def put( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PUT` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "PUT", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def patch( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `PATCH` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "PATCH", + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def delete( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, + follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, + extensions: RequestExtensions | None = None, + ) -> Response: + """ + Send a `DELETE` request. + + **Parameters**: See `httpx.request`. + """ + return await self.request( + "DELETE", + url, + params=params, + headers=headers, + cookies=cookies, + auth=auth, + follow_redirects=follow_redirects, + timeout=timeout, + extensions=extensions, + ) + + async def aclose(self) -> None: + """ + Close transport and proxies. + """ + if self._state != ClientState.CLOSED: + self._state = ClientState.CLOSED + + await self._transport.aclose() + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.aclose() + + async def __aenter__(self: U) -> U: + if self._state != ClientState.UNOPENED: + msg = { + ClientState.OPENED: "Cannot open a client instance more than once.", + ClientState.CLOSED: ( + "Cannot reopen a client instance, once it has been closed." + ), + }[self._state] + raise RuntimeError(msg) + + self._state = ClientState.OPENED + + await self._transport.__aenter__() + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self._state = ClientState.CLOSED + + await self._transport.__aexit__(exc_type, exc_value, traceback) + for proxy in self._mounts.values(): + if proxy is not None: + await proxy.__aexit__(exc_type, exc_value, traceback) diff --git a/lib/python3.12/site-packages/httpx/_config.py b/lib/python3.12/site-packages/httpx/_config.py new file mode 100644 index 0000000000000000000000000000000000000000..467a6c90ae269babe3af7963d9d7c78b9f012268 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_config.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +import os +import typing + +from ._models import Headers +from ._types import CertTypes, HeaderTypes, TimeoutTypes +from ._urls import URL + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + +__all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"] + + +class UnsetType: + pass # pragma: no cover + + +UNSET = UnsetType() + + +def create_ssl_context( + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, +) -> ssl.SSLContext: + import ssl + import warnings + + import certifi + + if verify is True: + if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover + ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"]) + elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover + ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"]) + else: + # Default case... + ctx = ssl.create_default_context(cafile=certifi.where()) + elif verify is False: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + elif isinstance(verify, str): # pragma: nocover + message = ( + "`verify=` is deprecated. " + "Use `verify=ssl.create_default_context(cafile=...)` " + "or `verify=ssl.create_default_context(capath=...)` instead." + ) + warnings.warn(message, DeprecationWarning) + if os.path.isdir(verify): + return ssl.create_default_context(capath=verify) + return ssl.create_default_context(cafile=verify) + else: + ctx = verify + + if cert: # pragma: nocover + message = ( + "`cert=...` is deprecated. Use `verify=` instead," + "with `.load_cert_chain()` to configure the certificate chain." + ) + warnings.warn(message, DeprecationWarning) + if isinstance(cert, str): + ctx.load_cert_chain(cert) + else: + ctx.load_cert_chain(*cert) + + return ctx + + +class Timeout: + """ + Timeout configuration. + + **Usage**: + + Timeout(None) # No timeouts. + Timeout(5.0) # 5s timeout on all operations. + Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts. + Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere. + Timeout(5.0, pool=None) # No timeout on acquiring connection from pool. + # 5s timeout elsewhere. + """ + + def __init__( + self, + timeout: TimeoutTypes | UnsetType = UNSET, + *, + connect: None | float | UnsetType = UNSET, + read: None | float | UnsetType = UNSET, + write: None | float | UnsetType = UNSET, + pool: None | float | UnsetType = UNSET, + ) -> None: + if isinstance(timeout, Timeout): + # Passed as a single explicit Timeout. + assert connect is UNSET + assert read is UNSET + assert write is UNSET + assert pool is UNSET + self.connect = timeout.connect # type: typing.Optional[float] + self.read = timeout.read # type: typing.Optional[float] + self.write = timeout.write # type: typing.Optional[float] + self.pool = timeout.pool # type: typing.Optional[float] + elif isinstance(timeout, tuple): + # Passed as a tuple. + self.connect = timeout[0] + self.read = timeout[1] + self.write = None if len(timeout) < 3 else timeout[2] + self.pool = None if len(timeout) < 4 else timeout[3] + elif not ( + isinstance(connect, UnsetType) + or isinstance(read, UnsetType) + or isinstance(write, UnsetType) + or isinstance(pool, UnsetType) + ): + self.connect = connect + self.read = read + self.write = write + self.pool = pool + else: + if isinstance(timeout, UnsetType): + raise ValueError( + "httpx.Timeout must either include a default, or set all " + "four parameters explicitly." + ) + self.connect = timeout if isinstance(connect, UnsetType) else connect + self.read = timeout if isinstance(read, UnsetType) else read + self.write = timeout if isinstance(write, UnsetType) else write + self.pool = timeout if isinstance(pool, UnsetType) else pool + + def as_dict(self) -> dict[str, float | None]: + return { + "connect": self.connect, + "read": self.read, + "write": self.write, + "pool": self.pool, + } + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, self.__class__) + and self.connect == other.connect + and self.read == other.read + and self.write == other.write + and self.pool == other.pool + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + if len({self.connect, self.read, self.write, self.pool}) == 1: + return f"{class_name}(timeout={self.connect})" + return ( + f"{class_name}(connect={self.connect}, " + f"read={self.read}, write={self.write}, pool={self.pool})" + ) + + +class Limits: + """ + Configuration for limits to various client behaviors. + + **Parameters:** + + * **max_connections** - The maximum number of concurrent connections that may be + established. + * **max_keepalive_connections** - Allow the connection pool to maintain + keep-alive connections below this point. Should be less than or equal + to `max_connections`. + * **keepalive_expiry** - Time limit on idle keep-alive connections in seconds. + """ + + def __init__( + self, + *, + max_connections: int | None = None, + max_keepalive_connections: int | None = None, + keepalive_expiry: float | None = 5.0, + ) -> None: + self.max_connections = max_connections + self.max_keepalive_connections = max_keepalive_connections + self.keepalive_expiry = keepalive_expiry + + def __eq__(self, other: typing.Any) -> bool: + return ( + isinstance(other, self.__class__) + and self.max_connections == other.max_connections + and self.max_keepalive_connections == other.max_keepalive_connections + and self.keepalive_expiry == other.keepalive_expiry + ) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return ( + f"{class_name}(max_connections={self.max_connections}, " + f"max_keepalive_connections={self.max_keepalive_connections}, " + f"keepalive_expiry={self.keepalive_expiry})" + ) + + +class Proxy: + def __init__( + self, + url: URL | str, + *, + ssl_context: ssl.SSLContext | None = None, + auth: tuple[str, str] | None = None, + headers: HeaderTypes | None = None, + ) -> None: + url = URL(url) + headers = Headers(headers) + + if url.scheme not in ("http", "https", "socks5", "socks5h"): + raise ValueError(f"Unknown scheme for proxy URL {url!r}") + + if url.username or url.password: + # Remove any auth credentials from the URL. + auth = (url.username, url.password) + url = url.copy_with(username=None, password=None) + + self.url = url + self.auth = auth + self.headers = headers + self.ssl_context = ssl_context + + @property + def raw_auth(self) -> tuple[bytes, bytes] | None: + # The proxy authentication as raw bytes. + return ( + None + if self.auth is None + else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8")) + ) + + def __repr__(self) -> str: + # The authentication is represented with the password component masked. + auth = (self.auth[0], "********") if self.auth else None + + # Build a nice concise representation. + url_str = f"{str(self.url)!r}" + auth_str = f", auth={auth!r}" if auth else "" + headers_str = f", headers={dict(self.headers)!r}" if self.headers else "" + return f"Proxy({url_str}{auth_str}{headers_str})" + + +DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0) +DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20) +DEFAULT_MAX_REDIRECTS = 20 diff --git a/lib/python3.12/site-packages/httpx/_content.py b/lib/python3.12/site-packages/httpx/_content.py new file mode 100644 index 0000000000000000000000000000000000000000..6f479a0885f723b7395843d41164a87041820776 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_content.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import inspect +import warnings +from json import dumps as json_dumps +from typing import ( + Any, + AsyncIterable, + AsyncIterator, + Iterable, + Iterator, + Mapping, +) +from urllib.parse import urlencode + +from ._exceptions import StreamClosed, StreamConsumed +from ._multipart import MultipartStream +from ._types import ( + AsyncByteStream, + RequestContent, + RequestData, + RequestFiles, + ResponseContent, + SyncByteStream, +) +from ._utils import peek_filelike_length, primitive_value_to_str + +__all__ = ["ByteStream"] + + +class ByteStream(AsyncByteStream, SyncByteStream): + def __init__(self, stream: bytes) -> None: + self._stream = stream + + def __iter__(self) -> Iterator[bytes]: + yield self._stream + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield self._stream + + +class IteratorByteStream(SyncByteStream): + CHUNK_SIZE = 65_536 + + def __init__(self, stream: Iterable[bytes]) -> None: + self._stream = stream + self._is_stream_consumed = False + self._is_generator = inspect.isgenerator(stream) + + def __iter__(self) -> Iterator[bytes]: + if self._is_stream_consumed and self._is_generator: + raise StreamConsumed() + + self._is_stream_consumed = True + if hasattr(self._stream, "read"): + # File-like interfaces should use 'read' directly. + chunk = self._stream.read(self.CHUNK_SIZE) + while chunk: + yield chunk + chunk = self._stream.read(self.CHUNK_SIZE) + else: + # Otherwise iterate. + for part in self._stream: + yield part + + +class AsyncIteratorByteStream(AsyncByteStream): + CHUNK_SIZE = 65_536 + + def __init__(self, stream: AsyncIterable[bytes]) -> None: + self._stream = stream + self._is_stream_consumed = False + self._is_generator = inspect.isasyncgen(stream) + + async def __aiter__(self) -> AsyncIterator[bytes]: + if self._is_stream_consumed and self._is_generator: + raise StreamConsumed() + + self._is_stream_consumed = True + if hasattr(self._stream, "aread"): + # File-like interfaces should use 'aread' directly. + chunk = await self._stream.aread(self.CHUNK_SIZE) + while chunk: + yield chunk + chunk = await self._stream.aread(self.CHUNK_SIZE) + else: + # Otherwise iterate. + async for part in self._stream: + yield part + + +class UnattachedStream(AsyncByteStream, SyncByteStream): + """ + If a request or response is serialized using pickle, then it is no longer + attached to a stream for I/O purposes. Any stream operations should result + in `httpx.StreamClosed`. + """ + + def __iter__(self) -> Iterator[bytes]: + raise StreamClosed() + + async def __aiter__(self) -> AsyncIterator[bytes]: + raise StreamClosed() + yield b"" # pragma: no cover + + +def encode_content( + content: str | bytes | Iterable[bytes] | AsyncIterable[bytes], +) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: + if isinstance(content, (bytes, str)): + body = content.encode("utf-8") if isinstance(content, str) else content + content_length = len(body) + headers = {"Content-Length": str(content_length)} if body else {} + return headers, ByteStream(body) + + elif isinstance(content, Iterable) and not isinstance(content, dict): + # `not isinstance(content, dict)` is a bit oddly specific, but it + # catches a case that's easy for users to make in error, and would + # otherwise pass through here, like any other bytes-iterable, + # because `dict` happens to be iterable. See issue #2491. + content_length_or_none = peek_filelike_length(content) + + if content_length_or_none is None: + headers = {"Transfer-Encoding": "chunked"} + else: + headers = {"Content-Length": str(content_length_or_none)} + return headers, IteratorByteStream(content) # type: ignore + + elif isinstance(content, AsyncIterable): + headers = {"Transfer-Encoding": "chunked"} + return headers, AsyncIteratorByteStream(content) + + raise TypeError(f"Unexpected type for 'content', {type(content)!r}") + + +def encode_urlencoded_data( + data: RequestData, +) -> tuple[dict[str, str], ByteStream]: + plain_data = [] + for key, value in data.items(): + if isinstance(value, (list, tuple)): + plain_data.extend([(key, primitive_value_to_str(item)) for item in value]) + else: + plain_data.append((key, primitive_value_to_str(value))) + body = urlencode(plain_data, doseq=True).encode("utf-8") + content_length = str(len(body)) + content_type = "application/x-www-form-urlencoded" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_multipart_data( + data: RequestData, files: RequestFiles, boundary: bytes | None +) -> tuple[dict[str, str], MultipartStream]: + multipart = MultipartStream(data=data, files=files, boundary=boundary) + headers = multipart.get_headers() + return headers, multipart + + +def encode_text(text: str) -> tuple[dict[str, str], ByteStream]: + body = text.encode("utf-8") + content_length = str(len(body)) + content_type = "text/plain; charset=utf-8" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_html(html: str) -> tuple[dict[str, str], ByteStream]: + body = html.encode("utf-8") + content_length = str(len(body)) + content_type = "text/html; charset=utf-8" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]: + body = json_dumps( + json, ensure_ascii=False, separators=(",", ":"), allow_nan=False + ).encode("utf-8") + content_length = str(len(body)) + content_type = "application/json" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, ByteStream(body) + + +def encode_request( + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: Any | None = None, + boundary: bytes | None = None, +) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: + """ + Handles encoding the given `content`, `data`, `files`, and `json`, + returning a two-tuple of (, ). + """ + if data is not None and not isinstance(data, Mapping): + # We prefer to separate `content=` + # for raw request content, and `data=
      ` for url encoded or + # multipart form content. + # + # However for compat with requests, we *do* still support + # `data=` usages. We deal with that case here, treating it + # as if `content=<...>` had been supplied instead. + message = "Use 'content=<...>' to upload raw bytes/text content." + warnings.warn(message, DeprecationWarning, stacklevel=2) + return encode_content(data) + + if content is not None: + return encode_content(content) + elif files: + return encode_multipart_data(data or {}, files, boundary) + elif data: + return encode_urlencoded_data(data) + elif json is not None: + return encode_json(json) + + return {}, ByteStream(b"") + + +def encode_response( + content: ResponseContent | None = None, + text: str | None = None, + html: str | None = None, + json: Any | None = None, +) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: + """ + Handles encoding the given `content`, returning a two-tuple of + (, ). + """ + if content is not None: + return encode_content(content) + elif text is not None: + return encode_text(text) + elif html is not None: + return encode_html(html) + elif json is not None: + return encode_json(json) + + return {}, ByteStream(b"") diff --git a/lib/python3.12/site-packages/httpx/_decoders.py b/lib/python3.12/site-packages/httpx/_decoders.py new file mode 100644 index 0000000000000000000000000000000000000000..899dfada878e1181fca6d3c75a79526a076abb9e --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_decoders.py @@ -0,0 +1,393 @@ +""" +Handlers for Content-Encoding. + +See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding +""" + +from __future__ import annotations + +import codecs +import io +import typing +import zlib + +from ._exceptions import DecodingError + +# Brotli support is optional +try: + # The C bindings in `brotli` are recommended for CPython. + import brotli +except ImportError: # pragma: no cover + try: + # The CFFI bindings in `brotlicffi` are recommended for PyPy + # and other environments. + import brotlicffi as brotli + except ImportError: + brotli = None + + +# Zstandard support is optional +try: + import zstandard +except ImportError: # pragma: no cover + zstandard = None # type: ignore + + +class ContentDecoder: + def decode(self, data: bytes) -> bytes: + raise NotImplementedError() # pragma: no cover + + def flush(self) -> bytes: + raise NotImplementedError() # pragma: no cover + + +class IdentityDecoder(ContentDecoder): + """ + Handle unencoded data. + """ + + def decode(self, data: bytes) -> bytes: + return data + + def flush(self) -> bytes: + return b"" + + +class DeflateDecoder(ContentDecoder): + """ + Handle 'deflate' decoding. + + See: https://stackoverflow.com/questions/1838699 + """ + + def __init__(self) -> None: + self.first_attempt = True + self.decompressor = zlib.decompressobj() + + def decode(self, data: bytes) -> bytes: + was_first_attempt = self.first_attempt + self.first_attempt = False + try: + return self.decompressor.decompress(data) + except zlib.error as exc: + if was_first_attempt: + self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS) + return self.decode(data) + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + try: + return self.decompressor.flush() + except zlib.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class GZipDecoder(ContentDecoder): + """ + Handle 'gzip' decoding. + + See: https://stackoverflow.com/questions/1838699 + """ + + def __init__(self) -> None: + self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16) + + def decode(self, data: bytes) -> bytes: + try: + return self.decompressor.decompress(data) + except zlib.error as exc: + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + try: + return self.decompressor.flush() + except zlib.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class BrotliDecoder(ContentDecoder): + """ + Handle 'brotli' decoding. + + Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/ + or `pip install brotli`. See https://github.com/google/brotli + Supports both 'brotlipy' and 'Brotli' packages since they share an import + name. The top branches are for 'brotlipy' and bottom branches for 'Brotli' + """ + + def __init__(self) -> None: + if brotli is None: # pragma: no cover + raise ImportError( + "Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' " + "packages have been installed. " + "Make sure to install httpx using `pip install httpx[brotli]`." + ) from None + + self.decompressor = brotli.Decompressor() + self.seen_data = False + self._decompress: typing.Callable[[bytes], bytes] + if hasattr(self.decompressor, "decompress"): + # The 'brotlicffi' package. + self._decompress = self.decompressor.decompress # pragma: no cover + else: + # The 'brotli' package. + self._decompress = self.decompressor.process # pragma: no cover + + def decode(self, data: bytes) -> bytes: + if not data: + return b"" + self.seen_data = True + try: + return self._decompress(data) + except brotli.error as exc: + raise DecodingError(str(exc)) from exc + + def flush(self) -> bytes: + if not self.seen_data: + return b"" + try: + if hasattr(self.decompressor, "finish"): + # Only available in the 'brotlicffi' package. + + # As the decompressor decompresses eagerly, this + # will never actually emit any data. However, it will potentially throw + # errors if a truncated or damaged data stream has been used. + self.decompressor.finish() # pragma: no cover + return b"" + except brotli.error as exc: # pragma: no cover + raise DecodingError(str(exc)) from exc + + +class ZStandardDecoder(ContentDecoder): + """ + Handle 'zstd' RFC 8878 decoding. + + Requires `pip install zstandard`. + Can be installed as a dependency of httpx using `pip install httpx[zstd]`. + """ + + # inspired by the ZstdDecoder implementation in urllib3 + def __init__(self) -> None: + if zstandard is None: # pragma: no cover + raise ImportError( + "Using 'ZStandardDecoder', ..." + "Make sure to install httpx using `pip install httpx[zstd]`." + ) from None + + self.decompressor = zstandard.ZstdDecompressor().decompressobj() + self.seen_data = False + + def decode(self, data: bytes) -> bytes: + assert zstandard is not None + self.seen_data = True + output = io.BytesIO() + try: + output.write(self.decompressor.decompress(data)) + while self.decompressor.eof and self.decompressor.unused_data: + unused_data = self.decompressor.unused_data + self.decompressor = zstandard.ZstdDecompressor().decompressobj() + output.write(self.decompressor.decompress(unused_data)) + except zstandard.ZstdError as exc: + raise DecodingError(str(exc)) from exc + return output.getvalue() + + def flush(self) -> bytes: + if not self.seen_data: + return b"" + ret = self.decompressor.flush() # note: this is a no-op + if not self.decompressor.eof: + raise DecodingError("Zstandard data is incomplete") # pragma: no cover + return bytes(ret) + + +class MultiDecoder(ContentDecoder): + """ + Handle the case where multiple encodings have been applied. + """ + + def __init__(self, children: typing.Sequence[ContentDecoder]) -> None: + """ + 'children' should be a sequence of decoders in the order in which + each was applied. + """ + # Note that we reverse the order for decoding. + self.children = list(reversed(children)) + + def decode(self, data: bytes) -> bytes: + for child in self.children: + data = child.decode(data) + return data + + def flush(self) -> bytes: + data = b"" + for child in self.children: + data = child.decode(data) + child.flush() + return data + + +class ByteChunker: + """ + Handles returning byte content in fixed-size chunks. + """ + + def __init__(self, chunk_size: int | None = None) -> None: + self._buffer = io.BytesIO() + self._chunk_size = chunk_size + + def decode(self, content: bytes) -> list[bytes]: + if self._chunk_size is None: + return [content] if content else [] + + self._buffer.write(content) + if self._buffer.tell() >= self._chunk_size: + value = self._buffer.getvalue() + chunks = [ + value[i : i + self._chunk_size] + for i in range(0, len(value), self._chunk_size) + ] + if len(chunks[-1]) == self._chunk_size: + self._buffer.seek(0) + self._buffer.truncate() + return chunks + else: + self._buffer.seek(0) + self._buffer.write(chunks[-1]) + self._buffer.truncate() + return chunks[:-1] + else: + return [] + + def flush(self) -> list[bytes]: + value = self._buffer.getvalue() + self._buffer.seek(0) + self._buffer.truncate() + return [value] if value else [] + + +class TextChunker: + """ + Handles returning text content in fixed-size chunks. + """ + + def __init__(self, chunk_size: int | None = None) -> None: + self._buffer = io.StringIO() + self._chunk_size = chunk_size + + def decode(self, content: str) -> list[str]: + if self._chunk_size is None: + return [content] if content else [] + + self._buffer.write(content) + if self._buffer.tell() >= self._chunk_size: + value = self._buffer.getvalue() + chunks = [ + value[i : i + self._chunk_size] + for i in range(0, len(value), self._chunk_size) + ] + if len(chunks[-1]) == self._chunk_size: + self._buffer.seek(0) + self._buffer.truncate() + return chunks + else: + self._buffer.seek(0) + self._buffer.write(chunks[-1]) + self._buffer.truncate() + return chunks[:-1] + else: + return [] + + def flush(self) -> list[str]: + value = self._buffer.getvalue() + self._buffer.seek(0) + self._buffer.truncate() + return [value] if value else [] + + +class TextDecoder: + """ + Handles incrementally decoding bytes into text + """ + + def __init__(self, encoding: str = "utf-8") -> None: + self.decoder = codecs.getincrementaldecoder(encoding)(errors="replace") + + def decode(self, data: bytes) -> str: + return self.decoder.decode(data) + + def flush(self) -> str: + return self.decoder.decode(b"", True) + + +class LineDecoder: + """ + Handles incrementally reading lines from text. + + Has the same behaviour as the stdllib splitlines, + but handling the input iteratively. + """ + + def __init__(self) -> None: + self.buffer: list[str] = [] + self.trailing_cr: bool = False + + def decode(self, text: str) -> list[str]: + # See https://docs.python.org/3/library/stdtypes.html#str.splitlines + NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029" + + # We always push a trailing `\r` into the next decode iteration. + if self.trailing_cr: + text = "\r" + text + self.trailing_cr = False + if text.endswith("\r"): + self.trailing_cr = True + text = text[:-1] + + if not text: + # NOTE: the edge case input of empty text doesn't occur in practice, + # because other httpx internals filter out this value + return [] # pragma: no cover + + trailing_newline = text[-1] in NEWLINE_CHARS + lines = text.splitlines() + + if len(lines) == 1 and not trailing_newline: + # No new lines, buffer the input and continue. + self.buffer.append(lines[0]) + return [] + + if self.buffer: + # Include any existing buffer in the first portion of the + # splitlines result. + lines = ["".join(self.buffer) + lines[0]] + lines[1:] + self.buffer = [] + + if not trailing_newline: + # If the last segment of splitlines is not newline terminated, + # then drop it from our output and start a new buffer. + self.buffer = [lines.pop()] + + return lines + + def flush(self) -> list[str]: + if not self.buffer and not self.trailing_cr: + return [] + + lines = ["".join(self.buffer)] + self.buffer = [] + self.trailing_cr = False + return lines + + +SUPPORTED_DECODERS = { + "identity": IdentityDecoder, + "gzip": GZipDecoder, + "deflate": DeflateDecoder, + "br": BrotliDecoder, + "zstd": ZStandardDecoder, +} + + +if brotli is None: + SUPPORTED_DECODERS.pop("br") # pragma: no cover +if zstandard is None: + SUPPORTED_DECODERS.pop("zstd") # pragma: no cover diff --git a/lib/python3.12/site-packages/httpx/_exceptions.py b/lib/python3.12/site-packages/httpx/_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..77f45a6d3986d15626fc8a5fd459d6a3e0fbe466 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_exceptions.py @@ -0,0 +1,379 @@ +""" +Our exception hierarchy: + +* HTTPError + x RequestError + + TransportError + - TimeoutException + · ConnectTimeout + · ReadTimeout + · WriteTimeout + · PoolTimeout + - NetworkError + · ConnectError + · ReadError + · WriteError + · CloseError + - ProtocolError + · LocalProtocolError + · RemoteProtocolError + - ProxyError + - UnsupportedProtocol + + DecodingError + + TooManyRedirects + x HTTPStatusError +* InvalidURL +* CookieConflict +* StreamError + x StreamConsumed + x StreamClosed + x ResponseNotRead + x RequestNotRead +""" + +from __future__ import annotations + +import contextlib +import typing + +if typing.TYPE_CHECKING: + from ._models import Request, Response # pragma: no cover + +__all__ = [ + "CloseError", + "ConnectError", + "ConnectTimeout", + "CookieConflict", + "DecodingError", + "HTTPError", + "HTTPStatusError", + "InvalidURL", + "LocalProtocolError", + "NetworkError", + "PoolTimeout", + "ProtocolError", + "ProxyError", + "ReadError", + "ReadTimeout", + "RemoteProtocolError", + "RequestError", + "RequestNotRead", + "ResponseNotRead", + "StreamClosed", + "StreamConsumed", + "StreamError", + "TimeoutException", + "TooManyRedirects", + "TransportError", + "UnsupportedProtocol", + "WriteError", + "WriteTimeout", +] + + +class HTTPError(Exception): + """ + Base class for `RequestError` and `HTTPStatusError`. + + Useful for `try...except` blocks when issuing a request, + and then calling `.raise_for_status()`. + + For example: + + ``` + try: + response = httpx.get("https://www.example.com") + response.raise_for_status() + except httpx.HTTPError as exc: + print(f"HTTP Exception for {exc.request.url} - {exc}") + ``` + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + self._request: Request | None = None + + @property + def request(self) -> Request: + if self._request is None: + raise RuntimeError("The .request property has not been set.") + return self._request + + @request.setter + def request(self, request: Request) -> None: + self._request = request + + +class RequestError(HTTPError): + """ + Base class for all exceptions that may occur when issuing a `.request()`. + """ + + def __init__(self, message: str, *, request: Request | None = None) -> None: + super().__init__(message) + # At the point an exception is raised we won't typically have a request + # instance to associate it with. + # + # The 'request_context' context manager is used within the Client and + # Response methods in order to ensure that any raised exceptions + # have a `.request` property set on them. + self._request = request + + +class TransportError(RequestError): + """ + Base class for all exceptions that occur at the level of the Transport API. + """ + + +# Timeout exceptions... + + +class TimeoutException(TransportError): + """ + The base class for timeout errors. + + An operation has timed out. + """ + + +class ConnectTimeout(TimeoutException): + """ + Timed out while connecting to the host. + """ + + +class ReadTimeout(TimeoutException): + """ + Timed out while receiving data from the host. + """ + + +class WriteTimeout(TimeoutException): + """ + Timed out while sending data to the host. + """ + + +class PoolTimeout(TimeoutException): + """ + Timed out waiting to acquire a connection from the pool. + """ + + +# Core networking exceptions... + + +class NetworkError(TransportError): + """ + The base class for network-related errors. + + An error occurred while interacting with the network. + """ + + +class ReadError(NetworkError): + """ + Failed to receive data from the network. + """ + + +class WriteError(NetworkError): + """ + Failed to send data through the network. + """ + + +class ConnectError(NetworkError): + """ + Failed to establish a connection. + """ + + +class CloseError(NetworkError): + """ + Failed to close a connection. + """ + + +# Other transport exceptions... + + +class ProxyError(TransportError): + """ + An error occurred while establishing a proxy connection. + """ + + +class UnsupportedProtocol(TransportError): + """ + Attempted to make a request to an unsupported protocol. + + For example issuing a request to `ftp://www.example.com`. + """ + + +class ProtocolError(TransportError): + """ + The protocol was violated. + """ + + +class LocalProtocolError(ProtocolError): + """ + A protocol was violated by the client. + + For example if the user instantiated a `Request` instance explicitly, + failed to include the mandatory `Host:` header, and then issued it directly + using `client.send()`. + """ + + +class RemoteProtocolError(ProtocolError): + """ + The protocol was violated by the server. + + For example, returning malformed HTTP. + """ + + +# Other request exceptions... + + +class DecodingError(RequestError): + """ + Decoding of the response failed, due to a malformed encoding. + """ + + +class TooManyRedirects(RequestError): + """ + Too many redirects. + """ + + +# Client errors + + +class HTTPStatusError(HTTPError): + """ + The response had an error HTTP status of 4xx or 5xx. + + May be raised when calling `response.raise_for_status()` + """ + + def __init__(self, message: str, *, request: Request, response: Response) -> None: + super().__init__(message) + self.request = request + self.response = response + + +class InvalidURL(Exception): + """ + URL is improperly formed or cannot be parsed. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class CookieConflict(Exception): + """ + Attempted to lookup a cookie by name, but multiple cookies existed. + + Can occur when calling `response.cookies.get(...)`. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +# Stream exceptions... + +# These may occur as the result of a programming error, by accessing +# the request/response stream in an invalid manner. + + +class StreamError(RuntimeError): + """ + The base class for stream exceptions. + + The developer made an error in accessing the request stream in + an invalid way. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class StreamConsumed(StreamError): + """ + Attempted to read or stream content, but the content has already + been streamed. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. For requests, this could be due to passing " + "a generator as request content, and then receiving a redirect " + "response or a secondary request as part of an authentication flow." + "For responses, this could be due to attempting to stream the response " + "content more than once." + ) + super().__init__(message) + + +class StreamClosed(StreamError): + """ + Attempted to read or stream response content, but the request has been + closed. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream content, but the stream has " "been closed." + ) + super().__init__(message) + + +class ResponseNotRead(StreamError): + """ + Attempted to access streaming response content, without having called `read()`. + """ + + def __init__(self) -> None: + message = ( + "Attempted to access streaming response content," + " without having called `read()`." + ) + super().__init__(message) + + +class RequestNotRead(StreamError): + """ + Attempted to access streaming request content, without having called `read()`. + """ + + def __init__(self) -> None: + message = ( + "Attempted to access streaming request content," + " without having called `read()`." + ) + super().__init__(message) + + +@contextlib.contextmanager +def request_context( + request: Request | None = None, +) -> typing.Iterator[None]: + """ + A context manager that can be used to attach the given request context + to any `RequestError` exceptions that are raised within the block. + """ + try: + yield + except RequestError as exc: + if request is not None: + exc.request = request + raise exc diff --git a/lib/python3.12/site-packages/httpx/_main.py b/lib/python3.12/site-packages/httpx/_main.py new file mode 100644 index 0000000000000000000000000000000000000000..cffa4bb7db0f930f4db56653a061c4d7400ba4e6 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_main.py @@ -0,0 +1,506 @@ +from __future__ import annotations + +import functools +import json +import sys +import typing + +import click +import pygments.lexers +import pygments.util +import rich.console +import rich.markup +import rich.progress +import rich.syntax +import rich.table + +from ._client import Client +from ._exceptions import RequestError +from ._models import Response +from ._status_codes import codes + +if typing.TYPE_CHECKING: + import httpcore # pragma: no cover + + +def print_help() -> None: + console = rich.console.Console() + + console.print("[bold]HTTPX :butterfly:", justify="center") + console.print() + console.print("A next generation HTTP client.", justify="center") + console.print() + console.print( + "Usage: [bold]httpx[/bold] [cyan] [OPTIONS][/cyan] ", justify="left" + ) + console.print() + + table = rich.table.Table.grid(padding=1, pad_edge=True) + table.add_column("Parameter", no_wrap=True, justify="left", style="bold") + table.add_column("Description") + table.add_row( + "-m, --method [cyan]METHOD", + "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n" + "[Default: GET, or POST if a request body is included]", + ) + table.add_row( + "-p, --params [cyan] ...", + "Query parameters to include in the request URL.", + ) + table.add_row( + "-c, --content [cyan]TEXT", "Byte content to include in the request body." + ) + table.add_row( + "-d, --data [cyan] ...", "Form data to include in the request body." + ) + table.add_row( + "-f, --files [cyan] ...", + "Form files to include in the request body.", + ) + table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.") + table.add_row( + "-h, --headers [cyan] ...", + "Include additional HTTP headers in the request.", + ) + table.add_row( + "--cookies [cyan] ...", "Cookies to include in the request." + ) + table.add_row( + "--auth [cyan]", + "Username and password to include in the request. Specify '-' for the password" + " to use a password prompt. Note that using --verbose/-v will expose" + " the Authorization header, including the password encoding" + " in a trivially reversible format.", + ) + + table.add_row( + "--proxy [cyan]URL", + "Send the request via a proxy. Should be the URL giving the proxy address.", + ) + + table.add_row( + "--timeout [cyan]FLOAT", + "Timeout value to use for network operations, such as establishing the" + " connection, reading some data, etc... [Default: 5.0]", + ) + + table.add_row("--follow-redirects", "Automatically follow redirects.") + table.add_row("--no-verify", "Disable SSL verification.") + table.add_row( + "--http2", "Send the request using HTTP/2, if the remote server supports it." + ) + + table.add_row( + "--download [cyan]FILE", + "Save the response content as a file, rather than displaying it.", + ) + + table.add_row("-v, --verbose", "Verbose output. Show request as well as response.") + table.add_row("--help", "Show this message and exit.") + console.print(table) + + +def get_lexer_for_response(response: Response) -> str: + content_type = response.headers.get("Content-Type") + if content_type is not None: + mime_type, _, _ = content_type.partition(";") + try: + return typing.cast( + str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name + ) + except pygments.util.ClassNotFound: # pragma: no cover + pass + return "" # pragma: no cover + + +def format_request_headers(request: httpcore.Request, http2: bool = False) -> str: + version = "HTTP/2" if http2 else "HTTP/1.1" + headers = [ + (name.lower() if http2 else name, value) for name, value in request.headers + ] + method = request.method.decode("ascii") + target = request.url.target.decode("ascii") + lines = [f"{method} {target} {version}"] + [ + f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers + ] + return "\n".join(lines) + + +def format_response_headers( + http_version: bytes, + status: int, + reason_phrase: bytes | None, + headers: list[tuple[bytes, bytes]], +) -> str: + version = http_version.decode("ascii") + reason = ( + codes.get_reason_phrase(status) + if reason_phrase is None + else reason_phrase.decode("ascii") + ) + lines = [f"{version} {status} {reason}"] + [ + f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers + ] + return "\n".join(lines) + + +def print_request_headers(request: httpcore.Request, http2: bool = False) -> None: + console = rich.console.Console() + http_text = format_request_headers(request, http2=http2) + syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + + +def print_response_headers( + http_version: bytes, + status: int, + reason_phrase: bytes | None, + headers: list[tuple[bytes, bytes]], +) -> None: + console = rich.console.Console() + http_text = format_response_headers(http_version, status, reason_phrase, headers) + syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) + console.print(syntax) + + +def print_response(response: Response) -> None: + console = rich.console.Console() + lexer_name = get_lexer_for_response(response) + if lexer_name: + if lexer_name.lower() == "json": + try: + data = response.json() + text = json.dumps(data, indent=4) + except ValueError: # pragma: no cover + text = response.text + else: + text = response.text + + syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True) + console.print(syntax) + else: + console.print(f"<{len(response.content)} bytes of binary data>") + + +_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...] +_PCTRTTT = typing.Tuple[_PCTRTT, ...] +_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]] + + +def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover + lines = [] + for key, value in cert.items(): + if isinstance(value, (list, tuple)): + lines.append(f"* {key}:") + for item in value: + if key in ("subject", "issuer"): + for sub_item in item: + lines.append(f"* {sub_item[0]}: {sub_item[1]!r}") + elif isinstance(item, tuple) and len(item) == 2: + lines.append(f"* {item[0]}: {item[1]!r}") + else: + lines.append(f"* {item!r}") + else: + lines.append(f"* {key}: {value!r}") + return "\n".join(lines) + + +def trace( + name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False +) -> None: + console = rich.console.Console() + if name == "connection.connect_tcp.started" and verbose: + host = info["host"] + console.print(f"* Connecting to {host!r}") + elif name == "connection.connect_tcp.complete" and verbose: + stream = info["return_value"] + server_addr = stream.get_extra_info("server_addr") + console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}") + elif name == "connection.start_tls.complete" and verbose: # pragma: no cover + stream = info["return_value"] + ssl_object = stream.get_extra_info("ssl_object") + version = ssl_object.version() + cipher = ssl_object.cipher() + server_cert = ssl_object.getpeercert() + alpn = ssl_object.selected_alpn_protocol() + console.print(f"* SSL established using {version!r} / {cipher[0]!r}") + console.print(f"* Selected ALPN protocol: {alpn!r}") + if server_cert: + console.print("* Server certificate:") + console.print(format_certificate(server_cert)) + elif name == "http11.send_request_headers.started" and verbose: + request = info["request"] + print_request_headers(request, http2=False) + elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover + request = info["request"] + print_request_headers(request, http2=True) + elif name == "http11.receive_response_headers.complete": + http_version, status, reason_phrase, headers = info["return_value"] + print_response_headers(http_version, status, reason_phrase, headers) + elif name == "http2.receive_response_headers.complete": # pragma: no cover + status, headers = info["return_value"] + http_version = b"HTTP/2" + reason_phrase = None + print_response_headers(http_version, status, reason_phrase, headers) + + +def download_response(response: Response, download: typing.BinaryIO) -> None: + console = rich.console.Console() + console.print() + content_length = response.headers.get("Content-Length") + with rich.progress.Progress( + "[progress.description]{task.description}", + "[progress.percentage]{task.percentage:>3.0f}%", + rich.progress.BarColumn(bar_width=None), + rich.progress.DownloadColumn(), + rich.progress.TransferSpeedColumn(), + ) as progress: + description = f"Downloading [bold]{rich.markup.escape(download.name)}" + download_task = progress.add_task( + description, + total=int(content_length or 0), + start=content_length is not None, + ) + for chunk in response.iter_bytes(): + download.write(chunk) + progress.update(download_task, completed=response.num_bytes_downloaded) + + +def validate_json( + ctx: click.Context, + param: click.Option | click.Parameter, + value: typing.Any, +) -> typing.Any: + if value is None: + return None + + try: + return json.loads(value) + except json.JSONDecodeError: # pragma: no cover + raise click.BadParameter("Not valid JSON") + + +def validate_auth( + ctx: click.Context, + param: click.Option | click.Parameter, + value: typing.Any, +) -> typing.Any: + if value == (None, None): + return None + + username, password = value + if password == "-": # pragma: no cover + password = click.prompt("Password", hide_input=True) + return (username, password) + + +def handle_help( + ctx: click.Context, + param: click.Option | click.Parameter, + value: typing.Any, +) -> None: + if not value or ctx.resilient_parsing: + return + + print_help() + ctx.exit() + + +@click.command(add_help_option=False) +@click.argument("url", type=str) +@click.option( + "--method", + "-m", + "method", + type=str, + help=( + "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. " + "[Default: GET, or POST if a request body is included]" + ), +) +@click.option( + "--params", + "-p", + "params", + type=(str, str), + multiple=True, + help="Query parameters to include in the request URL.", +) +@click.option( + "--content", + "-c", + "content", + type=str, + help="Byte content to include in the request body.", +) +@click.option( + "--data", + "-d", + "data", + type=(str, str), + multiple=True, + help="Form data to include in the request body.", +) +@click.option( + "--files", + "-f", + "files", + type=(str, click.File(mode="rb")), + multiple=True, + help="Form files to include in the request body.", +) +@click.option( + "--json", + "-j", + "json", + type=str, + callback=validate_json, + help="JSON data to include in the request body.", +) +@click.option( + "--headers", + "-h", + "headers", + type=(str, str), + multiple=True, + help="Include additional HTTP headers in the request.", +) +@click.option( + "--cookies", + "cookies", + type=(str, str), + multiple=True, + help="Cookies to include in the request.", +) +@click.option( + "--auth", + "auth", + type=(str, str), + default=(None, None), + callback=validate_auth, + help=( + "Username and password to include in the request. " + "Specify '-' for the password to use a password prompt. " + "Note that using --verbose/-v will expose the Authorization header, " + "including the password encoding in a trivially reversible format." + ), +) +@click.option( + "--proxy", + "proxy", + type=str, + default=None, + help="Send the request via a proxy. Should be the URL giving the proxy address.", +) +@click.option( + "--timeout", + "timeout", + type=float, + default=5.0, + help=( + "Timeout value to use for network operations, such as establishing the " + "connection, reading some data, etc... [Default: 5.0]" + ), +) +@click.option( + "--follow-redirects", + "follow_redirects", + is_flag=True, + default=False, + help="Automatically follow redirects.", +) +@click.option( + "--no-verify", + "verify", + is_flag=True, + default=True, + help="Disable SSL verification.", +) +@click.option( + "--http2", + "http2", + type=bool, + is_flag=True, + default=False, + help="Send the request using HTTP/2, if the remote server supports it.", +) +@click.option( + "--download", + type=click.File("wb"), + help="Save the response content as a file, rather than displaying it.", +) +@click.option( + "--verbose", + "-v", + type=bool, + is_flag=True, + default=False, + help="Verbose. Show request as well as response.", +) +@click.option( + "--help", + is_flag=True, + is_eager=True, + expose_value=False, + callback=handle_help, + help="Show this message and exit.", +) +def main( + url: str, + method: str, + params: list[tuple[str, str]], + content: str, + data: list[tuple[str, str]], + files: list[tuple[str, click.File]], + json: str, + headers: list[tuple[str, str]], + cookies: list[tuple[str, str]], + auth: tuple[str, str] | None, + proxy: str, + timeout: float, + follow_redirects: bool, + verify: bool, + http2: bool, + download: typing.BinaryIO | None, + verbose: bool, +) -> None: + """ + An HTTP command line client. + Sends a request and displays the response. + """ + if not method: + method = "POST" if content or data or files or json else "GET" + + try: + with Client(proxy=proxy, timeout=timeout, http2=http2, verify=verify) as client: + with client.stream( + method, + url, + params=list(params), + content=content, + data=dict(data), + files=files, # type: ignore + json=json, + headers=headers, + cookies=dict(cookies), + auth=auth, + follow_redirects=follow_redirects, + extensions={"trace": functools.partial(trace, verbose=verbose)}, + ) as response: + if download is not None: + download_response(response, download) + else: + response.read() + if response.content: + print_response(response) + + except RequestError as exc: + console = rich.console.Console() + console.print(f"[red]{type(exc).__name__}[/red]: {exc}") + sys.exit(1) + + sys.exit(0 if response.is_success else 1) diff --git a/lib/python3.12/site-packages/httpx/_models.py b/lib/python3.12/site-packages/httpx/_models.py new file mode 100644 index 0000000000000000000000000000000000000000..67d74bf86bfc80e22d9a4a3153572845accd9039 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_models.py @@ -0,0 +1,1277 @@ +from __future__ import annotations + +import codecs +import datetime +import email.message +import json as jsonlib +import re +import typing +import urllib.request +from collections.abc import Mapping +from http.cookiejar import Cookie, CookieJar + +from ._content import ByteStream, UnattachedStream, encode_request, encode_response +from ._decoders import ( + SUPPORTED_DECODERS, + ByteChunker, + ContentDecoder, + IdentityDecoder, + LineDecoder, + MultiDecoder, + TextChunker, + TextDecoder, +) +from ._exceptions import ( + CookieConflict, + HTTPStatusError, + RequestNotRead, + ResponseNotRead, + StreamClosed, + StreamConsumed, + request_context, +) +from ._multipart import get_multipart_boundary_from_content_type +from ._status_codes import codes +from ._types import ( + AsyncByteStream, + CookieTypes, + HeaderTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestExtensions, + RequestFiles, + ResponseContent, + ResponseExtensions, + SyncByteStream, +) +from ._urls import URL +from ._utils import to_bytes_or_str, to_str + +__all__ = ["Cookies", "Headers", "Request", "Response"] + +SENSITIVE_HEADERS = {"authorization", "proxy-authorization"} + + +def _is_known_encoding(encoding: str) -> bool: + """ + Return `True` if `encoding` is a known codec. + """ + try: + codecs.lookup(encoding) + except LookupError: + return False + return True + + +def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes: + """ + Coerce str/bytes into a strictly byte-wise HTTP header key. + """ + return key if isinstance(key, bytes) else key.encode(encoding or "ascii") + + +def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes: + """ + Coerce str/bytes into a strictly byte-wise HTTP header value. + """ + if isinstance(value, bytes): + return value + if not isinstance(value, str): + raise TypeError(f"Header value must be str or bytes, not {type(value)}") + return value.encode(encoding or "ascii") + + +def _parse_content_type_charset(content_type: str) -> str | None: + # We used to use `cgi.parse_header()` here, but `cgi` became a dead battery. + # See: https://peps.python.org/pep-0594/#cgi + msg = email.message.Message() + msg["content-type"] = content_type + return msg.get_content_charset(failobj=None) + + +def _parse_header_links(value: str) -> list[dict[str, str]]: + """ + Returns a list of parsed link headers, for more info see: + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + The generic syntax of those is: + Link: < uri-reference >; param1=value1; param2="value2" + So for instance: + Link; '; type="image/jpeg",;' + would return + [ + {"url": "http:/.../front.jpeg", "type": "image/jpeg"}, + {"url": "http://.../back.jpeg"}, + ] + :param value: HTTP Link entity-header field + :return: list of parsed link headers + """ + links: list[dict[str, str]] = [] + replace_chars = " '\"" + value = value.strip(replace_chars) + if not value: + return links + for val in re.split(", *<", value): + try: + url, params = val.split(";", 1) + except ValueError: + url, params = val, "" + link = {"url": url.strip("<> '\"")} + for param in params.split(";"): + try: + key, value = param.split("=") + except ValueError: + break + link[key.strip(replace_chars)] = value.strip(replace_chars) + links.append(link) + return links + + +def _obfuscate_sensitive_headers( + items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]], +) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]: + for k, v in items: + if to_str(k.lower()) in SENSITIVE_HEADERS: + v = to_bytes_or_str("[secure]", match_type_of=v) + yield k, v + + +class Headers(typing.MutableMapping[str, str]): + """ + HTTP headers, as a case-insensitive multi-dict. + """ + + def __init__( + self, + headers: HeaderTypes | None = None, + encoding: str | None = None, + ) -> None: + self._list = [] # type: typing.List[typing.Tuple[bytes, bytes, bytes]] + + if isinstance(headers, Headers): + self._list = list(headers._list) + elif isinstance(headers, Mapping): + for k, v in headers.items(): + bytes_key = _normalize_header_key(k, encoding) + bytes_value = _normalize_header_value(v, encoding) + self._list.append((bytes_key, bytes_key.lower(), bytes_value)) + elif headers is not None: + for k, v in headers: + bytes_key = _normalize_header_key(k, encoding) + bytes_value = _normalize_header_value(v, encoding) + self._list.append((bytes_key, bytes_key.lower(), bytes_value)) + + self._encoding = encoding + + @property + def encoding(self) -> str: + """ + Header encoding is mandated as ascii, but we allow fallbacks to utf-8 + or iso-8859-1. + """ + if self._encoding is None: + for encoding in ["ascii", "utf-8"]: + for key, value in self.raw: + try: + key.decode(encoding) + value.decode(encoding) + except UnicodeDecodeError: + break + else: + # The else block runs if 'break' did not occur, meaning + # all values fitted the encoding. + self._encoding = encoding + break + else: + # The ISO-8859-1 encoding covers all 256 code points in a byte, + # so will never raise decode errors. + self._encoding = "iso-8859-1" + return self._encoding + + @encoding.setter + def encoding(self, value: str) -> None: + self._encoding = value + + @property + def raw(self) -> list[tuple[bytes, bytes]]: + """ + Returns a list of the raw header items, as byte pairs. + """ + return [(raw_key, value) for raw_key, _, value in self._list] + + def keys(self) -> typing.KeysView[str]: + return {key.decode(self.encoding): None for _, key, value in self._list}.keys() + + def values(self) -> typing.ValuesView[str]: + values_dict: dict[str, str] = {} + for _, key, value in self._list: + str_key = key.decode(self.encoding) + str_value = value.decode(self.encoding) + if str_key in values_dict: + values_dict[str_key] += f", {str_value}" + else: + values_dict[str_key] = str_value + return values_dict.values() + + def items(self) -> typing.ItemsView[str, str]: + """ + Return `(key, value)` items of headers. Concatenate headers + into a single comma separated value when a key occurs multiple times. + """ + values_dict: dict[str, str] = {} + for _, key, value in self._list: + str_key = key.decode(self.encoding) + str_value = value.decode(self.encoding) + if str_key in values_dict: + values_dict[str_key] += f", {str_value}" + else: + values_dict[str_key] = str_value + return values_dict.items() + + def multi_items(self) -> list[tuple[str, str]]: + """ + Return a list of `(key, value)` pairs of headers. Allow multiple + occurrences of the same key without concatenating into a single + comma separated value. + """ + return [ + (key.decode(self.encoding), value.decode(self.encoding)) + for _, key, value in self._list + ] + + def get(self, key: str, default: typing.Any = None) -> typing.Any: + """ + Return a header value. If multiple occurrences of the header occur + then concatenate them together with commas. + """ + try: + return self[key] + except KeyError: + return default + + def get_list(self, key: str, split_commas: bool = False) -> list[str]: + """ + Return a list of all header values for a given key. + If `split_commas=True` is passed, then any comma separated header + values are split into multiple return strings. + """ + get_header_key = key.lower().encode(self.encoding) + + values = [ + item_value.decode(self.encoding) + for _, item_key, item_value in self._list + if item_key.lower() == get_header_key + ] + + if not split_commas: + return values + + split_values = [] + for value in values: + split_values.extend([item.strip() for item in value.split(",")]) + return split_values + + def update(self, headers: HeaderTypes | None = None) -> None: # type: ignore + headers = Headers(headers) + for key in headers.keys(): + if key in self: + self.pop(key) + self._list.extend(headers._list) + + def copy(self) -> Headers: + return Headers(self, encoding=self.encoding) + + def __getitem__(self, key: str) -> str: + """ + Return a single header value. + + If there are multiple headers with the same key, then we concatenate + them with commas. See: https://tools.ietf.org/html/rfc7230#section-3.2.2 + """ + normalized_key = key.lower().encode(self.encoding) + + items = [ + header_value.decode(self.encoding) + for _, header_key, header_value in self._list + if header_key == normalized_key + ] + + if items: + return ", ".join(items) + + raise KeyError(key) + + def __setitem__(self, key: str, value: str) -> None: + """ + Set the header `key` to `value`, removing any duplicate entries. + Retains insertion order. + """ + set_key = key.encode(self._encoding or "utf-8") + set_value = value.encode(self._encoding or "utf-8") + lookup_key = set_key.lower() + + found_indexes = [ + idx + for idx, (_, item_key, _) in enumerate(self._list) + if item_key == lookup_key + ] + + for idx in reversed(found_indexes[1:]): + del self._list[idx] + + if found_indexes: + idx = found_indexes[0] + self._list[idx] = (set_key, lookup_key, set_value) + else: + self._list.append((set_key, lookup_key, set_value)) + + def __delitem__(self, key: str) -> None: + """ + Remove the header `key`. + """ + del_key = key.lower().encode(self.encoding) + + pop_indexes = [ + idx + for idx, (_, item_key, _) in enumerate(self._list) + if item_key.lower() == del_key + ] + + if not pop_indexes: + raise KeyError(key) + + for idx in reversed(pop_indexes): + del self._list[idx] + + def __contains__(self, key: typing.Any) -> bool: + header_key = key.lower().encode(self.encoding) + return header_key in [key for _, key, _ in self._list] + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._list) + + def __eq__(self, other: typing.Any) -> bool: + try: + other_headers = Headers(other) + except ValueError: + return False + + self_list = [(key, value) for _, key, value in self._list] + other_list = [(key, value) for _, key, value in other_headers._list] + return sorted(self_list) == sorted(other_list) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + + encoding_str = "" + if self.encoding != "ascii": + encoding_str = f", encoding={self.encoding!r}" + + as_list = list(_obfuscate_sensitive_headers(self.multi_items())) + as_dict = dict(as_list) + + no_duplicate_keys = len(as_dict) == len(as_list) + if no_duplicate_keys: + return f"{class_name}({as_dict!r}{encoding_str})" + return f"{class_name}({as_list!r}{encoding_str})" + + +class Request: + def __init__( + self, + method: str, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: typing.Any | None = None, + stream: SyncByteStream | AsyncByteStream | None = None, + extensions: RequestExtensions | None = None, + ) -> None: + self.method = method.upper() + self.url = URL(url) if params is None else URL(url, params=params) + self.headers = Headers(headers) + self.extensions = {} if extensions is None else dict(extensions) + + if cookies: + Cookies(cookies).set_cookie_header(self) + + if stream is None: + content_type: str | None = self.headers.get("content-type") + headers, stream = encode_request( + content=content, + data=data, + files=files, + json=json, + boundary=get_multipart_boundary_from_content_type( + content_type=content_type.encode(self.headers.encoding) + if content_type + else None + ), + ) + self._prepare(headers) + self.stream = stream + # Load the request body, except for streaming content. + if isinstance(stream, ByteStream): + self.read() + else: + # There's an important distinction between `Request(content=...)`, + # and `Request(stream=...)`. + # + # Using `content=...` implies automatically populated `Host` and content + # headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`. + # + # Using `stream=...` will not automatically include *any* + # auto-populated headers. + # + # As an end-user you don't really need `stream=...`. It's only + # useful when: + # + # * Preserving the request stream when copying requests, eg for redirects. + # * Creating request instances on the *server-side* of the transport API. + self.stream = stream + + def _prepare(self, default_headers: dict[str, str]) -> None: + for key, value in default_headers.items(): + # Ignore Transfer-Encoding if the Content-Length has been set explicitly. + if key.lower() == "transfer-encoding" and "Content-Length" in self.headers: + continue + self.headers.setdefault(key, value) + + auto_headers: list[tuple[bytes, bytes]] = [] + + has_host = "Host" in self.headers + has_content_length = ( + "Content-Length" in self.headers or "Transfer-Encoding" in self.headers + ) + + if not has_host and self.url.host: + auto_headers.append((b"Host", self.url.netloc)) + if not has_content_length and self.method in ("POST", "PUT", "PATCH"): + auto_headers.append((b"Content-Length", b"0")) + + self.headers = Headers(auto_headers + self.headers.raw) + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + raise RequestNotRead() + return self._content + + def read(self) -> bytes: + """ + Read and return the request content. + """ + if not hasattr(self, "_content"): + assert isinstance(self.stream, typing.Iterable) + self._content = b"".join(self.stream) + if not isinstance(self.stream, ByteStream): + # If a streaming request has been read entirely into memory, then + # we can replace the stream with a raw bytes implementation, + # to ensure that any non-replayable streams can still be used. + self.stream = ByteStream(self._content) + return self._content + + async def aread(self) -> bytes: + """ + Read and return the request content. + """ + if not hasattr(self, "_content"): + assert isinstance(self.stream, typing.AsyncIterable) + self._content = b"".join([part async for part in self.stream]) + if not isinstance(self.stream, ByteStream): + # If a streaming request has been read entirely into memory, then + # we can replace the stream with a raw bytes implementation, + # to ensure that any non-replayable streams can still be used. + self.stream = ByteStream(self._content) + return self._content + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + url = str(self.url) + return f"<{class_name}({self.method!r}, {url!r})>" + + def __getstate__(self) -> dict[str, typing.Any]: + return { + name: value + for name, value in self.__dict__.items() + if name not in ["extensions", "stream"] + } + + def __setstate__(self, state: dict[str, typing.Any]) -> None: + for name, value in state.items(): + setattr(self, name, value) + self.extensions = {} + self.stream = UnattachedStream() + + +class Response: + def __init__( + self, + status_code: int, + *, + headers: HeaderTypes | None = None, + content: ResponseContent | None = None, + text: str | None = None, + html: str | None = None, + json: typing.Any = None, + stream: SyncByteStream | AsyncByteStream | None = None, + request: Request | None = None, + extensions: ResponseExtensions | None = None, + history: list[Response] | None = None, + default_encoding: str | typing.Callable[[bytes], str] = "utf-8", + ) -> None: + self.status_code = status_code + self.headers = Headers(headers) + + self._request: Request | None = request + + # When follow_redirects=False and a redirect is received, + # the client will set `response.next_request`. + self.next_request: Request | None = None + + self.extensions = {} if extensions is None else dict(extensions) + self.history = [] if history is None else list(history) + + self.is_closed = False + self.is_stream_consumed = False + + self.default_encoding = default_encoding + + if stream is None: + headers, stream = encode_response(content, text, html, json) + self._prepare(headers) + self.stream = stream + if isinstance(stream, ByteStream): + # Load the response body, except for streaming content. + self.read() + else: + # There's an important distinction between `Response(content=...)`, + # and `Response(stream=...)`. + # + # Using `content=...` implies automatically populated content headers, + # of either `Content-Length: ...` or `Transfer-Encoding: chunked`. + # + # Using `stream=...` will not automatically include any content headers. + # + # As an end-user you don't really need `stream=...`. It's only + # useful when creating response instances having received a stream + # from the transport API. + self.stream = stream + + self._num_bytes_downloaded = 0 + + def _prepare(self, default_headers: dict[str, str]) -> None: + for key, value in default_headers.items(): + # Ignore Transfer-Encoding if the Content-Length has been set explicitly. + if key.lower() == "transfer-encoding" and "content-length" in self.headers: + continue + self.headers.setdefault(key, value) + + @property + def elapsed(self) -> datetime.timedelta: + """ + Returns the time taken for the complete request/response + cycle to complete. + """ + if not hasattr(self, "_elapsed"): + raise RuntimeError( + "'.elapsed' may only be accessed after the response " + "has been read or closed." + ) + return self._elapsed + + @elapsed.setter + def elapsed(self, elapsed: datetime.timedelta) -> None: + self._elapsed = elapsed + + @property + def request(self) -> Request: + """ + Returns the request instance associated to the current response. + """ + if self._request is None: + raise RuntimeError( + "The request instance has not been set on this response." + ) + return self._request + + @request.setter + def request(self, value: Request) -> None: + self._request = value + + @property + def http_version(self) -> str: + try: + http_version: bytes = self.extensions["http_version"] + except KeyError: + return "HTTP/1.1" + else: + return http_version.decode("ascii", errors="ignore") + + @property + def reason_phrase(self) -> str: + try: + reason_phrase: bytes = self.extensions["reason_phrase"] + except KeyError: + return codes.get_reason_phrase(self.status_code) + else: + return reason_phrase.decode("ascii", errors="ignore") + + @property + def url(self) -> URL: + """ + Returns the URL for which the request was made. + """ + return self.request.url + + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + raise ResponseNotRead() + return self._content + + @property + def text(self) -> str: + if not hasattr(self, "_text"): + content = self.content + if not content: + self._text = "" + else: + decoder = TextDecoder(encoding=self.encoding or "utf-8") + self._text = "".join([decoder.decode(self.content), decoder.flush()]) + return self._text + + @property + def encoding(self) -> str | None: + """ + Return an encoding to use for decoding the byte content into text. + The priority for determining this is given by... + + * `.encoding = <>` has been set explicitly. + * The encoding as specified by the charset parameter in the Content-Type header. + * The encoding as determined by `default_encoding`, which may either be + a string like "utf-8" indicating the encoding to use, or may be a callable + which enables charset autodetection. + """ + if not hasattr(self, "_encoding"): + encoding = self.charset_encoding + if encoding is None or not _is_known_encoding(encoding): + if isinstance(self.default_encoding, str): + encoding = self.default_encoding + elif hasattr(self, "_content"): + encoding = self.default_encoding(self._content) + self._encoding = encoding or "utf-8" + return self._encoding + + @encoding.setter + def encoding(self, value: str) -> None: + """ + Set the encoding to use for decoding the byte content into text. + + If the `text` attribute has been accessed, attempting to set the + encoding will throw a ValueError. + """ + if hasattr(self, "_text"): + raise ValueError( + "Setting encoding after `text` has been accessed is not allowed." + ) + self._encoding = value + + @property + def charset_encoding(self) -> str | None: + """ + Return the encoding, as specified by the Content-Type header. + """ + content_type = self.headers.get("Content-Type") + if content_type is None: + return None + + return _parse_content_type_charset(content_type) + + def _get_content_decoder(self) -> ContentDecoder: + """ + Returns a decoder instance which can be used to decode the raw byte + content, depending on the Content-Encoding used in the response. + """ + if not hasattr(self, "_decoder"): + decoders: list[ContentDecoder] = [] + values = self.headers.get_list("content-encoding", split_commas=True) + for value in values: + value = value.strip().lower() + try: + decoder_cls = SUPPORTED_DECODERS[value] + decoders.append(decoder_cls()) + except KeyError: + continue + + if len(decoders) == 1: + self._decoder = decoders[0] + elif len(decoders) > 1: + self._decoder = MultiDecoder(children=decoders) + else: + self._decoder = IdentityDecoder() + + return self._decoder + + @property + def is_informational(self) -> bool: + """ + A property which is `True` for 1xx status codes, `False` otherwise. + """ + return codes.is_informational(self.status_code) + + @property + def is_success(self) -> bool: + """ + A property which is `True` for 2xx status codes, `False` otherwise. + """ + return codes.is_success(self.status_code) + + @property + def is_redirect(self) -> bool: + """ + A property which is `True` for 3xx status codes, `False` otherwise. + + Note that not all responses with a 3xx status code indicate a URL redirect. + + Use `response.has_redirect_location` to determine responses with a properly + formed URL redirection. + """ + return codes.is_redirect(self.status_code) + + @property + def is_client_error(self) -> bool: + """ + A property which is `True` for 4xx status codes, `False` otherwise. + """ + return codes.is_client_error(self.status_code) + + @property + def is_server_error(self) -> bool: + """ + A property which is `True` for 5xx status codes, `False` otherwise. + """ + return codes.is_server_error(self.status_code) + + @property + def is_error(self) -> bool: + """ + A property which is `True` for 4xx and 5xx status codes, `False` otherwise. + """ + return codes.is_error(self.status_code) + + @property + def has_redirect_location(self) -> bool: + """ + Returns True for 3xx responses with a properly formed URL redirection, + `False` otherwise. + """ + return ( + self.status_code + in ( + # 301 (Cacheable redirect. Method may change to GET.) + codes.MOVED_PERMANENTLY, + # 302 (Uncacheable redirect. Method may change to GET.) + codes.FOUND, + # 303 (Client should make a GET or HEAD request.) + codes.SEE_OTHER, + # 307 (Equiv. 302, but retain method) + codes.TEMPORARY_REDIRECT, + # 308 (Equiv. 301, but retain method) + codes.PERMANENT_REDIRECT, + ) + and "Location" in self.headers + ) + + def raise_for_status(self) -> Response: + """ + Raise the `HTTPStatusError` if one occurred. + """ + request = self._request + if request is None: + raise RuntimeError( + "Cannot call `raise_for_status` as the request " + "instance has not been set on this response." + ) + + if self.is_success: + return self + + if self.has_redirect_location: + message = ( + "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n" + "Redirect location: '{0.headers[location]}'\n" + "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}" + ) + else: + message = ( + "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n" + "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}" + ) + + status_class = self.status_code // 100 + error_types = { + 1: "Informational response", + 3: "Redirect response", + 4: "Client error", + 5: "Server error", + } + error_type = error_types.get(status_class, "Invalid status code") + message = message.format(self, error_type=error_type) + raise HTTPStatusError(message, request=request, response=self) + + def json(self, **kwargs: typing.Any) -> typing.Any: + return jsonlib.loads(self.content, **kwargs) + + @property + def cookies(self) -> Cookies: + if not hasattr(self, "_cookies"): + self._cookies = Cookies() + self._cookies.extract_cookies(self) + return self._cookies + + @property + def links(self) -> dict[str | None, dict[str, str]]: + """ + Returns the parsed header links of the response, if any + """ + header = self.headers.get("link") + if header is None: + return {} + + return { + (link.get("rel") or link.get("url")): link + for link in _parse_header_links(header) + } + + @property + def num_bytes_downloaded(self) -> int: + return self._num_bytes_downloaded + + def __repr__(self) -> str: + return f"" + + def __getstate__(self) -> dict[str, typing.Any]: + return { + name: value + for name, value in self.__dict__.items() + if name not in ["extensions", "stream", "is_closed", "_decoder"] + } + + def __setstate__(self, state: dict[str, typing.Any]) -> None: + for name, value in state.items(): + setattr(self, name, value) + self.is_closed = True + self.extensions = {} + self.stream = UnattachedStream() + + def read(self) -> bytes: + """ + Read and return the response content. + """ + if not hasattr(self, "_content"): + self._content = b"".join(self.iter_bytes()) + return self._content + + def iter_bytes(self, chunk_size: int | None = None) -> typing.Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + This allows us to handle gzip, deflate, brotli, and zstd encoded responses. + """ + if hasattr(self, "_content"): + chunk_size = len(self._content) if chunk_size is None else chunk_size + for i in range(0, len(self._content), max(chunk_size, 1)): + yield self._content[i : i + chunk_size] + else: + decoder = self._get_content_decoder() + chunker = ByteChunker(chunk_size=chunk_size) + with request_context(request=self._request): + for raw_bytes in self.iter_raw(): + decoded = decoder.decode(raw_bytes) + for chunk in chunker.decode(decoded): + yield chunk + decoded = decoder.flush() + for chunk in chunker.decode(decoded): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> typing.Iterator[str]: + """ + A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + decoder = TextDecoder(encoding=self.encoding or "utf-8") + chunker = TextChunker(chunk_size=chunk_size) + with request_context(request=self._request): + for byte_content in self.iter_bytes(): + text_content = decoder.decode(byte_content) + for chunk in chunker.decode(text_content): + yield chunk + text_content = decoder.flush() + for chunk in chunker.decode(text_content): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + def iter_lines(self) -> typing.Iterator[str]: + decoder = LineDecoder() + with request_context(request=self._request): + for text in self.iter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + def iter_raw(self, chunk_size: int | None = None) -> typing.Iterator[bytes]: + """ + A byte-iterator over the raw response content. + """ + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise StreamClosed() + if not isinstance(self.stream, SyncByteStream): + raise RuntimeError("Attempted to call a sync iterator on an async stream.") + + self.is_stream_consumed = True + self._num_bytes_downloaded = 0 + chunker = ByteChunker(chunk_size=chunk_size) + + with request_context(request=self._request): + for raw_stream_bytes in self.stream: + self._num_bytes_downloaded += len(raw_stream_bytes) + for chunk in chunker.decode(raw_stream_bytes): + yield chunk + + for chunk in chunker.flush(): + yield chunk + + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + Automatically called if the response body is read to completion. + """ + if not isinstance(self.stream, SyncByteStream): + raise RuntimeError("Attempted to call an sync close on an async stream.") + + if not self.is_closed: + self.is_closed = True + with request_context(request=self._request): + self.stream.close() + + async def aread(self) -> bytes: + """ + Read and return the response content. + """ + if not hasattr(self, "_content"): + self._content = b"".join([part async for part in self.aiter_bytes()]) + return self._content + + async def aiter_bytes( + self, chunk_size: int | None = None + ) -> typing.AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + This allows us to handle gzip, deflate, brotli, and zstd encoded responses. + """ + if hasattr(self, "_content"): + chunk_size = len(self._content) if chunk_size is None else chunk_size + for i in range(0, len(self._content), max(chunk_size, 1)): + yield self._content[i : i + chunk_size] + else: + decoder = self._get_content_decoder() + chunker = ByteChunker(chunk_size=chunk_size) + with request_context(request=self._request): + async for raw_bytes in self.aiter_raw(): + decoded = decoder.decode(raw_bytes) + for chunk in chunker.decode(decoded): + yield chunk + decoded = decoder.flush() + for chunk in chunker.decode(decoded): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + async def aiter_text( + self, chunk_size: int | None = None + ) -> typing.AsyncIterator[str]: + """ + A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + decoder = TextDecoder(encoding=self.encoding or "utf-8") + chunker = TextChunker(chunk_size=chunk_size) + with request_context(request=self._request): + async for byte_content in self.aiter_bytes(): + text_content = decoder.decode(byte_content) + for chunk in chunker.decode(text_content): + yield chunk + text_content = decoder.flush() + for chunk in chunker.decode(text_content): + yield chunk # pragma: no cover + for chunk in chunker.flush(): + yield chunk + + async def aiter_lines(self) -> typing.AsyncIterator[str]: + decoder = LineDecoder() + with request_context(request=self._request): + async for text in self.aiter_text(): + for line in decoder.decode(text): + yield line + for line in decoder.flush(): + yield line + + async def aiter_raw( + self, chunk_size: int | None = None + ) -> typing.AsyncIterator[bytes]: + """ + A byte-iterator over the raw response content. + """ + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise StreamClosed() + if not isinstance(self.stream, AsyncByteStream): + raise RuntimeError("Attempted to call an async iterator on an sync stream.") + + self.is_stream_consumed = True + self._num_bytes_downloaded = 0 + chunker = ByteChunker(chunk_size=chunk_size) + + with request_context(request=self._request): + async for raw_stream_bytes in self.stream: + self._num_bytes_downloaded += len(raw_stream_bytes) + for chunk in chunker.decode(raw_stream_bytes): + yield chunk + + for chunk in chunker.flush(): + yield chunk + + await self.aclose() + + async def aclose(self) -> None: + """ + Close the response and release the connection. + Automatically called if the response body is read to completion. + """ + if not isinstance(self.stream, AsyncByteStream): + raise RuntimeError("Attempted to call an async close on an sync stream.") + + if not self.is_closed: + self.is_closed = True + with request_context(request=self._request): + await self.stream.aclose() + + +class Cookies(typing.MutableMapping[str, str]): + """ + HTTP Cookies, as a mutable mapping. + """ + + def __init__(self, cookies: CookieTypes | None = None) -> None: + if cookies is None or isinstance(cookies, dict): + self.jar = CookieJar() + if isinstance(cookies, dict): + for key, value in cookies.items(): + self.set(key, value) + elif isinstance(cookies, list): + self.jar = CookieJar() + for key, value in cookies: + self.set(key, value) + elif isinstance(cookies, Cookies): + self.jar = CookieJar() + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + else: + self.jar = cookies + + def extract_cookies(self, response: Response) -> None: + """ + Loads any cookies based on the response `Set-Cookie` headers. + """ + urllib_response = self._CookieCompatResponse(response) + urllib_request = self._CookieCompatRequest(response.request) + + self.jar.extract_cookies(urllib_response, urllib_request) # type: ignore + + def set_cookie_header(self, request: Request) -> None: + """ + Sets an appropriate 'Cookie:' HTTP header on the `Request`. + """ + urllib_request = self._CookieCompatRequest(request) + self.jar.add_cookie_header(urllib_request) + + def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: + """ + Set a cookie value by name. May optionally include domain and path. + """ + kwargs = { + "version": 0, + "name": name, + "value": value, + "port": None, + "port_specified": False, + "domain": domain, + "domain_specified": bool(domain), + "domain_initial_dot": domain.startswith("."), + "path": path, + "path_specified": bool(path), + "secure": False, + "expires": None, + "discard": True, + "comment": None, + "comment_url": None, + "rest": {"HttpOnly": None}, + "rfc2109": False, + } + cookie = Cookie(**kwargs) # type: ignore + self.jar.set_cookie(cookie) + + def get( # type: ignore + self, + name: str, + default: str | None = None, + domain: str | None = None, + path: str | None = None, + ) -> str | None: + """ + Get a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to retrieve. + """ + value = None + for cookie in self.jar: + if cookie.name == name: + if domain is None or cookie.domain == domain: + if path is None or cookie.path == path: + if value is not None: + message = f"Multiple cookies exist with name={name}" + raise CookieConflict(message) + value = cookie.value + + if value is None: + return default + return value + + def delete( + self, + name: str, + domain: str | None = None, + path: str | None = None, + ) -> None: + """ + Delete a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to delete. + """ + if domain is not None and path is not None: + return self.jar.clear(domain, path, name) + + remove = [ + cookie + for cookie in self.jar + if cookie.name == name + and (domain is None or cookie.domain == domain) + and (path is None or cookie.path == path) + ] + + for cookie in remove: + self.jar.clear(cookie.domain, cookie.path, cookie.name) + + def clear(self, domain: str | None = None, path: str | None = None) -> None: + """ + Delete all cookies. Optionally include a domain and path in + order to only delete a subset of all the cookies. + """ + args = [] + if domain is not None: + args.append(domain) + if path is not None: + assert domain is not None + args.append(path) + self.jar.clear(*args) + + def update(self, cookies: CookieTypes | None = None) -> None: # type: ignore + cookies = Cookies(cookies) + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + + def __setitem__(self, name: str, value: str) -> None: + return self.set(name, value) + + def __getitem__(self, name: str) -> str: + value = self.get(name) + if value is None: + raise KeyError(name) + return value + + def __delitem__(self, name: str) -> None: + return self.delete(name) + + def __len__(self) -> int: + return len(self.jar) + + def __iter__(self) -> typing.Iterator[str]: + return (cookie.name for cookie in self.jar) + + def __bool__(self) -> bool: + for _ in self.jar: + return True + return False + + def __repr__(self) -> str: + cookies_repr = ", ".join( + [ + f"" + for cookie in self.jar + ] + ) + + return f"" + + class _CookieCompatRequest(urllib.request.Request): + """ + Wraps a `Request` instance up in a compatibility interface suitable + for use with `CookieJar` operations. + """ + + def __init__(self, request: Request) -> None: + super().__init__( + url=str(request.url), + headers=dict(request.headers), + method=request.method, + ) + self.request = request + + def add_unredirected_header(self, key: str, value: str) -> None: + super().add_unredirected_header(key, value) + self.request.headers[key] = value + + class _CookieCompatResponse: + """ + Wraps a `Request` instance up in a compatibility interface suitable + for use with `CookieJar` operations. + """ + + def __init__(self, response: Response) -> None: + self.response = response + + def info(self) -> email.message.Message: + info = email.message.Message() + for key, value in self.response.headers.multi_items(): + # Note that setting `info[key]` here is an "append" operation, + # not a "replace" operation. + # https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__ + info[key] = value + return info diff --git a/lib/python3.12/site-packages/httpx/_multipart.py b/lib/python3.12/site-packages/httpx/_multipart.py new file mode 100644 index 0000000000000000000000000000000000000000..b4761af9b2cf384de5189269927d781a700dbe46 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_multipart.py @@ -0,0 +1,300 @@ +from __future__ import annotations + +import io +import mimetypes +import os +import re +import typing +from pathlib import Path + +from ._types import ( + AsyncByteStream, + FileContent, + FileTypes, + RequestData, + RequestFiles, + SyncByteStream, +) +from ._utils import ( + peek_filelike_length, + primitive_value_to_str, + to_bytes, +) + +_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"} +_HTML5_FORM_ENCODING_REPLACEMENTS.update( + {chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B} +) +_HTML5_FORM_ENCODING_RE = re.compile( + r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]) +) + + +def _format_form_param(name: str, value: str) -> bytes: + """ + Encode a name/value pair within a multipart form. + """ + + def replacer(match: typing.Match[str]) -> str: + return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)] + + value = _HTML5_FORM_ENCODING_RE.sub(replacer, value) + return f'{name}="{value}"'.encode() + + +def _guess_content_type(filename: str | None) -> str | None: + """ + Guesses the mimetype based on a filename. Defaults to `application/octet-stream`. + + Returns `None` if `filename` is `None` or empty. + """ + if filename: + return mimetypes.guess_type(filename)[0] or "application/octet-stream" + return None + + +def get_multipart_boundary_from_content_type( + content_type: bytes | None, +) -> bytes | None: + if not content_type or not content_type.startswith(b"multipart/form-data"): + return None + # parse boundary according to + # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1 + if b";" in content_type: + for section in content_type.split(b";"): + if section.strip().lower().startswith(b"boundary="): + return section.strip()[len(b"boundary=") :].strip(b'"') + return None + + +class DataField: + """ + A single form field item, within a multipart form field. + """ + + def __init__(self, name: str, value: str | bytes | int | float | None) -> None: + if not isinstance(name, str): + raise TypeError( + f"Invalid type for name. Expected str, got {type(name)}: {name!r}" + ) + if value is not None and not isinstance(value, (str, bytes, int, float)): + raise TypeError( + "Invalid type for value. Expected primitive type," + f" got {type(value)}: {value!r}" + ) + self.name = name + self.value: str | bytes = ( + value if isinstance(value, bytes) else primitive_value_to_str(value) + ) + + def render_headers(self) -> bytes: + if not hasattr(self, "_headers"): + name = _format_form_param("name", self.name) + self._headers = b"".join( + [b"Content-Disposition: form-data; ", name, b"\r\n\r\n"] + ) + + return self._headers + + def render_data(self) -> bytes: + if not hasattr(self, "_data"): + self._data = to_bytes(self.value) + + return self._data + + def get_length(self) -> int: + headers = self.render_headers() + data = self.render_data() + return len(headers) + len(data) + + def render(self) -> typing.Iterator[bytes]: + yield self.render_headers() + yield self.render_data() + + +class FileField: + """ + A single file field item, within a multipart form field. + """ + + CHUNK_SIZE = 64 * 1024 + + def __init__(self, name: str, value: FileTypes) -> None: + self.name = name + + fileobj: FileContent + + headers: dict[str, str] = {} + content_type: str | None = None + + # This large tuple based API largely mirror's requests' API + # It would be good to think of better APIs for this that we could + # include in httpx 2.0 since variable length tuples(especially of 4 elements) + # are quite unwieldly + if isinstance(value, tuple): + if len(value) == 2: + # neither the 3rd parameter (content_type) nor the 4th (headers) + # was included + filename, fileobj = value + elif len(value) == 3: + filename, fileobj, content_type = value + else: + # all 4 parameters included + filename, fileobj, content_type, headers = value # type: ignore + else: + filename = Path(str(getattr(value, "name", "upload"))).name + fileobj = value + + if content_type is None: + content_type = _guess_content_type(filename) + + has_content_type_header = any("content-type" in key.lower() for key in headers) + if content_type is not None and not has_content_type_header: + # note that unlike requests, we ignore the content_type provided in the 3rd + # tuple element if it is also included in the headers requests does + # the opposite (it overwrites the headerwith the 3rd tuple element) + headers["Content-Type"] = content_type + + if isinstance(fileobj, io.StringIO): + raise TypeError( + "Multipart file uploads require 'io.BytesIO', not 'io.StringIO'." + ) + if isinstance(fileobj, io.TextIOBase): + raise TypeError( + "Multipart file uploads must be opened in binary mode, not text mode." + ) + + self.filename = filename + self.file = fileobj + self.headers = headers + + def get_length(self) -> int | None: + headers = self.render_headers() + + if isinstance(self.file, (str, bytes)): + return len(headers) + len(to_bytes(self.file)) + + file_length = peek_filelike_length(self.file) + + # If we can't determine the filesize without reading it into memory, + # then return `None` here, to indicate an unknown file length. + if file_length is None: + return None + + return len(headers) + file_length + + def render_headers(self) -> bytes: + if not hasattr(self, "_headers"): + parts = [ + b"Content-Disposition: form-data; ", + _format_form_param("name", self.name), + ] + if self.filename: + filename = _format_form_param("filename", self.filename) + parts.extend([b"; ", filename]) + for header_name, header_value in self.headers.items(): + key, val = f"\r\n{header_name}: ".encode(), header_value.encode() + parts.extend([key, val]) + parts.append(b"\r\n\r\n") + self._headers = b"".join(parts) + + return self._headers + + def render_data(self) -> typing.Iterator[bytes]: + if isinstance(self.file, (str, bytes)): + yield to_bytes(self.file) + return + + if hasattr(self.file, "seek"): + try: + self.file.seek(0) + except io.UnsupportedOperation: + pass + + chunk = self.file.read(self.CHUNK_SIZE) + while chunk: + yield to_bytes(chunk) + chunk = self.file.read(self.CHUNK_SIZE) + + def render(self) -> typing.Iterator[bytes]: + yield self.render_headers() + yield from self.render_data() + + +class MultipartStream(SyncByteStream, AsyncByteStream): + """ + Request content as streaming multipart encoded form data. + """ + + def __init__( + self, + data: RequestData, + files: RequestFiles, + boundary: bytes | None = None, + ) -> None: + if boundary is None: + boundary = os.urandom(16).hex().encode("ascii") + + self.boundary = boundary + self.content_type = "multipart/form-data; boundary=%s" % boundary.decode( + "ascii" + ) + self.fields = list(self._iter_fields(data, files)) + + def _iter_fields( + self, data: RequestData, files: RequestFiles + ) -> typing.Iterator[FileField | DataField]: + for name, value in data.items(): + if isinstance(value, (tuple, list)): + for item in value: + yield DataField(name=name, value=item) + else: + yield DataField(name=name, value=value) + + file_items = files.items() if isinstance(files, typing.Mapping) else files + for name, value in file_items: + yield FileField(name=name, value=value) + + def iter_chunks(self) -> typing.Iterator[bytes]: + for field in self.fields: + yield b"--%s\r\n" % self.boundary + yield from field.render() + yield b"\r\n" + yield b"--%s--\r\n" % self.boundary + + def get_content_length(self) -> int | None: + """ + Return the length of the multipart encoded content, or `None` if + any of the files have a length that cannot be determined upfront. + """ + boundary_length = len(self.boundary) + length = 0 + + for field in self.fields: + field_length = field.get_length() + if field_length is None: + return None + + length += 2 + boundary_length + 2 # b"--{boundary}\r\n" + length += field_length + length += 2 # b"\r\n" + + length += 2 + boundary_length + 4 # b"--{boundary}--\r\n" + return length + + # Content stream interface. + + def get_headers(self) -> dict[str, str]: + content_length = self.get_content_length() + content_type = self.content_type + if content_length is None: + return {"Transfer-Encoding": "chunked", "Content-Type": content_type} + return {"Content-Length": str(content_length), "Content-Type": content_type} + + def __iter__(self) -> typing.Iterator[bytes]: + for chunk in self.iter_chunks(): + yield chunk + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + for chunk in self.iter_chunks(): + yield chunk diff --git a/lib/python3.12/site-packages/httpx/_status_codes.py b/lib/python3.12/site-packages/httpx/_status_codes.py new file mode 100644 index 0000000000000000000000000000000000000000..133a6231a5b53fd2f073799ca1bd07c50abe40ae --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_status_codes.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from enum import IntEnum + +__all__ = ["codes"] + + +class codes(IntEnum): + """HTTP status codes and reason phrases + + Status codes from the following RFCs are all observed: + + * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616 + * RFC 6585: Additional HTTP Status Codes + * RFC 3229: Delta encoding in HTTP + * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518 + * RFC 5842: Binding Extensions to WebDAV + * RFC 7238: Permanent Redirect + * RFC 2295: Transparent Content Negotiation in HTTP + * RFC 2774: An HTTP Extension Framework + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + * RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0) + * RFC 7725: An HTTP Status Code to Report Legal Obstacles + * RFC 8297: An HTTP Status Code for Indicating Hints + * RFC 8470: Using Early Data in HTTP + """ + + def __new__(cls, value: int, phrase: str = "") -> codes: + obj = int.__new__(cls, value) + obj._value_ = value + + obj.phrase = phrase # type: ignore[attr-defined] + return obj + + def __str__(self) -> str: + return str(self.value) + + @classmethod + def get_reason_phrase(cls, value: int) -> str: + try: + return codes(value).phrase # type: ignore + except ValueError: + return "" + + @classmethod + def is_informational(cls, value: int) -> bool: + """ + Returns `True` for 1xx status codes, `False` otherwise. + """ + return 100 <= value <= 199 + + @classmethod + def is_success(cls, value: int) -> bool: + """ + Returns `True` for 2xx status codes, `False` otherwise. + """ + return 200 <= value <= 299 + + @classmethod + def is_redirect(cls, value: int) -> bool: + """ + Returns `True` for 3xx status codes, `False` otherwise. + """ + return 300 <= value <= 399 + + @classmethod + def is_client_error(cls, value: int) -> bool: + """ + Returns `True` for 4xx status codes, `False` otherwise. + """ + return 400 <= value <= 499 + + @classmethod + def is_server_error(cls, value: int) -> bool: + """ + Returns `True` for 5xx status codes, `False` otherwise. + """ + return 500 <= value <= 599 + + @classmethod + def is_error(cls, value: int) -> bool: + """ + Returns `True` for 4xx or 5xx status codes, `False` otherwise. + """ + return 400 <= value <= 599 + + # informational + CONTINUE = 100, "Continue" + SWITCHING_PROTOCOLS = 101, "Switching Protocols" + PROCESSING = 102, "Processing" + EARLY_HINTS = 103, "Early Hints" + + # success + OK = 200, "OK" + CREATED = 201, "Created" + ACCEPTED = 202, "Accepted" + NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information" + NO_CONTENT = 204, "No Content" + RESET_CONTENT = 205, "Reset Content" + PARTIAL_CONTENT = 206, "Partial Content" + MULTI_STATUS = 207, "Multi-Status" + ALREADY_REPORTED = 208, "Already Reported" + IM_USED = 226, "IM Used" + + # redirection + MULTIPLE_CHOICES = 300, "Multiple Choices" + MOVED_PERMANENTLY = 301, "Moved Permanently" + FOUND = 302, "Found" + SEE_OTHER = 303, "See Other" + NOT_MODIFIED = 304, "Not Modified" + USE_PROXY = 305, "Use Proxy" + TEMPORARY_REDIRECT = 307, "Temporary Redirect" + PERMANENT_REDIRECT = 308, "Permanent Redirect" + + # client error + BAD_REQUEST = 400, "Bad Request" + UNAUTHORIZED = 401, "Unauthorized" + PAYMENT_REQUIRED = 402, "Payment Required" + FORBIDDEN = 403, "Forbidden" + NOT_FOUND = 404, "Not Found" + METHOD_NOT_ALLOWED = 405, "Method Not Allowed" + NOT_ACCEPTABLE = 406, "Not Acceptable" + PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required" + REQUEST_TIMEOUT = 408, "Request Timeout" + CONFLICT = 409, "Conflict" + GONE = 410, "Gone" + LENGTH_REQUIRED = 411, "Length Required" + PRECONDITION_FAILED = 412, "Precondition Failed" + REQUEST_ENTITY_TOO_LARGE = 413, "Request Entity Too Large" + REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long" + UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type" + REQUESTED_RANGE_NOT_SATISFIABLE = 416, "Requested Range Not Satisfiable" + EXPECTATION_FAILED = 417, "Expectation Failed" + IM_A_TEAPOT = 418, "I'm a teapot" + MISDIRECTED_REQUEST = 421, "Misdirected Request" + UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity" + LOCKED = 423, "Locked" + FAILED_DEPENDENCY = 424, "Failed Dependency" + TOO_EARLY = 425, "Too Early" + UPGRADE_REQUIRED = 426, "Upgrade Required" + PRECONDITION_REQUIRED = 428, "Precondition Required" + TOO_MANY_REQUESTS = 429, "Too Many Requests" + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large" + UNAVAILABLE_FOR_LEGAL_REASONS = 451, "Unavailable For Legal Reasons" + + # server errors + INTERNAL_SERVER_ERROR = 500, "Internal Server Error" + NOT_IMPLEMENTED = 501, "Not Implemented" + BAD_GATEWAY = 502, "Bad Gateway" + SERVICE_UNAVAILABLE = 503, "Service Unavailable" + GATEWAY_TIMEOUT = 504, "Gateway Timeout" + HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported" + VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates" + INSUFFICIENT_STORAGE = 507, "Insufficient Storage" + LOOP_DETECTED = 508, "Loop Detected" + NOT_EXTENDED = 510, "Not Extended" + NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required" + + +# Include lower-case styles for `requests` compatibility. +for code in codes: + setattr(codes, code._name_.lower(), int(code)) diff --git a/lib/python3.12/site-packages/httpx/_transports/__init__.py b/lib/python3.12/site-packages/httpx/_transports/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7a321053b29bcd48698cf2bd74a1d19c8556aefb --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_transports/__init__.py @@ -0,0 +1,15 @@ +from .asgi import * +from .base import * +from .default import * +from .mock import * +from .wsgi import * + +__all__ = [ + "ASGITransport", + "AsyncBaseTransport", + "BaseTransport", + "AsyncHTTPTransport", + "HTTPTransport", + "MockTransport", + "WSGITransport", +] diff --git a/lib/python3.12/site-packages/httpx/_transports/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/httpx/_transports/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20b947eeb896acbc0368c5437375a1cf101c64d2 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/_transports/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/_transports/__pycache__/asgi.cpython-312.pyc b/lib/python3.12/site-packages/httpx/_transports/__pycache__/asgi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3c1751a453d0452bb4c5ef8c151fc003f0693c3 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/_transports/__pycache__/asgi.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/_transports/__pycache__/base.cpython-312.pyc b/lib/python3.12/site-packages/httpx/_transports/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fc3f56551bf1cc67d482ff92e07933cf7a113b9 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/_transports/__pycache__/base.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/_transports/__pycache__/default.cpython-312.pyc b/lib/python3.12/site-packages/httpx/_transports/__pycache__/default.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2de0d62e68c33d3480af16765b8120da2ecc44b5 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/_transports/__pycache__/default.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/_transports/__pycache__/mock.cpython-312.pyc b/lib/python3.12/site-packages/httpx/_transports/__pycache__/mock.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7768c6bcb9b8ced7c38706b65f5a4e03a9256441 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/_transports/__pycache__/mock.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/_transports/__pycache__/wsgi.cpython-312.pyc b/lib/python3.12/site-packages/httpx/_transports/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea15734d34ea9234a10d8e0b0f3813ce299e2394 Binary files /dev/null and b/lib/python3.12/site-packages/httpx/_transports/__pycache__/wsgi.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/httpx/_transports/asgi.py b/lib/python3.12/site-packages/httpx/_transports/asgi.py new file mode 100644 index 0000000000000000000000000000000000000000..2bc4efae0e1b14620f75f712eb15ecf500d14eef --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_transports/asgi.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import typing + +from .._models import Request, Response +from .._types import AsyncByteStream +from .base import AsyncBaseTransport + +if typing.TYPE_CHECKING: # pragma: no cover + import asyncio + + import trio + + Event = typing.Union[asyncio.Event, trio.Event] + + +_Message = typing.MutableMapping[str, typing.Any] +_Receive = typing.Callable[[], typing.Awaitable[_Message]] +_Send = typing.Callable[ + [typing.MutableMapping[str, typing.Any]], typing.Awaitable[None] +] +_ASGIApp = typing.Callable[ + [typing.MutableMapping[str, typing.Any], _Receive, _Send], typing.Awaitable[None] +] + +__all__ = ["ASGITransport"] + + +def is_running_trio() -> bool: + try: + # sniffio is a dependency of trio. + + # See https://github.com/python-trio/trio/issues/2802 + import sniffio + + if sniffio.current_async_library() == "trio": + return True + except ImportError: # pragma: nocover + pass + + return False + + +def create_event() -> Event: + if is_running_trio(): + import trio + + return trio.Event() + + import asyncio + + return asyncio.Event() + + +class ASGIResponseStream(AsyncByteStream): + def __init__(self, body: list[bytes]) -> None: + self._body = body + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + yield b"".join(self._body) + + +class ASGITransport(AsyncBaseTransport): + """ + A custom AsyncTransport that handles sending requests directly to an ASGI app. + + ```python + transport = httpx.ASGITransport( + app=app, + root_path="/submount", + client=("1.2.3.4", 123) + ) + client = httpx.AsyncClient(transport=transport) + ``` + + Arguments: + + * `app` - The ASGI application. + * `raise_app_exceptions` - Boolean indicating if exceptions in the application + should be raised. Default to `True`. Can be set to `False` for use cases + such as testing the content of a client 500 response. + * `root_path` - The root path on which the ASGI application should be mounted. + * `client` - A two-tuple indicating the client IP and port of incoming requests. + ``` + """ + + def __init__( + self, + app: _ASGIApp, + raise_app_exceptions: bool = True, + root_path: str = "", + client: tuple[str, int] = ("127.0.0.1", 123), + ) -> None: + self.app = app + self.raise_app_exceptions = raise_app_exceptions + self.root_path = root_path + self.client = client + + async def handle_async_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, AsyncByteStream) + + # ASGI scope. + scope = { + "type": "http", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "method": request.method, + "headers": [(k.lower(), v) for (k, v) in request.headers.raw], + "scheme": request.url.scheme, + "path": request.url.path, + "raw_path": request.url.raw_path.split(b"?")[0], + "query_string": request.url.query, + "server": (request.url.host, request.url.port), + "client": self.client, + "root_path": self.root_path, + } + + # Request. + request_body_chunks = request.stream.__aiter__() + request_complete = False + + # Response. + status_code = None + response_headers = None + body_parts = [] + response_started = False + response_complete = create_event() + + # ASGI callables. + + async def receive() -> dict[str, typing.Any]: + nonlocal request_complete + + if request_complete: + await response_complete.wait() + return {"type": "http.disconnect"} + + try: + body = await request_body_chunks.__anext__() + except StopAsyncIteration: + request_complete = True + return {"type": "http.request", "body": b"", "more_body": False} + return {"type": "http.request", "body": body, "more_body": True} + + async def send(message: typing.MutableMapping[str, typing.Any]) -> None: + nonlocal status_code, response_headers, response_started + + if message["type"] == "http.response.start": + assert not response_started + + status_code = message["status"] + response_headers = message.get("headers", []) + response_started = True + + elif message["type"] == "http.response.body": + assert not response_complete.is_set() + body = message.get("body", b"") + more_body = message.get("more_body", False) + + if body and request.method != "HEAD": + body_parts.append(body) + + if not more_body: + response_complete.set() + + try: + await self.app(scope, receive, send) + except Exception: # noqa: PIE-786 + if self.raise_app_exceptions: + raise + + response_complete.set() + if status_code is None: + status_code = 500 + if response_headers is None: + response_headers = {} + + assert response_complete.is_set() + assert status_code is not None + assert response_headers is not None + + stream = ASGIResponseStream(body_parts) + + return Response(status_code, headers=response_headers, stream=stream) diff --git a/lib/python3.12/site-packages/httpx/_transports/base.py b/lib/python3.12/site-packages/httpx/_transports/base.py new file mode 100644 index 0000000000000000000000000000000000000000..66fd99d702480b555c06694fe14715ea6df3dfc3 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_transports/base.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import typing +from types import TracebackType + +from .._models import Request, Response + +T = typing.TypeVar("T", bound="BaseTransport") +A = typing.TypeVar("A", bound="AsyncBaseTransport") + +__all__ = ["AsyncBaseTransport", "BaseTransport"] + + +class BaseTransport: + def __enter__(self: T) -> T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self.close() + + def handle_request(self, request: Request) -> Response: + """ + Send a single HTTP request and return a response. + + Developers shouldn't typically ever need to call into this API directly, + since the Client class provides all the higher level user-facing API + niceties. + + In order to properly release any network resources, the response + stream should *either* be consumed immediately, with a call to + `response.stream.read()`, or else the `handle_request` call should + be followed with a try/finally block to ensuring the stream is + always closed. + + Example usage: + + with httpx.HTTPTransport() as transport: + req = httpx.Request( + method=b"GET", + url=(b"https", b"www.example.com", 443, b"/"), + headers=[(b"Host", b"www.example.com")], + ) + resp = transport.handle_request(req) + body = resp.stream.read() + print(resp.status_code, resp.headers, body) + + + Takes a `Request` instance as the only argument. + + Returns a `Response` instance. + """ + raise NotImplementedError( + "The 'handle_request' method must be implemented." + ) # pragma: no cover + + def close(self) -> None: + pass + + +class AsyncBaseTransport: + async def __aenter__(self: A) -> A: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + await self.aclose() + + async def handle_async_request( + self, + request: Request, + ) -> Response: + raise NotImplementedError( + "The 'handle_async_request' method must be implemented." + ) # pragma: no cover + + async def aclose(self) -> None: + pass diff --git a/lib/python3.12/site-packages/httpx/_transports/default.py b/lib/python3.12/site-packages/httpx/_transports/default.py new file mode 100644 index 0000000000000000000000000000000000000000..d5aa05ff234fd3fbf4fee88c4a7d3e3c151a538f --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_transports/default.py @@ -0,0 +1,406 @@ +""" +Custom transports, with nicely configured defaults. + +The following additional keyword arguments are currently supported by httpcore... + +* uds: str +* local_address: str +* retries: int + +Example usages... + +# Disable HTTP/2 on a single specific domain. +mounts = { + "all://": httpx.HTTPTransport(http2=True), + "all://*example.org": httpx.HTTPTransport() +} + +# Using advanced httpcore configuration, with connection retries. +transport = httpx.HTTPTransport(retries=1) +client = httpx.Client(transport=transport) + +# Using advanced httpcore configuration, with unix domain sockets. +transport = httpx.HTTPTransport(uds="socket.uds") +client = httpx.Client(transport=transport) +""" + +from __future__ import annotations + +import contextlib +import typing +from types import TracebackType + +if typing.TYPE_CHECKING: + import ssl # pragma: no cover + + import httpx # pragma: no cover + +from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context +from .._exceptions import ( + ConnectError, + ConnectTimeout, + LocalProtocolError, + NetworkError, + PoolTimeout, + ProtocolError, + ProxyError, + ReadError, + ReadTimeout, + RemoteProtocolError, + TimeoutException, + UnsupportedProtocol, + WriteError, + WriteTimeout, +) +from .._models import Request, Response +from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream +from .._urls import URL +from .base import AsyncBaseTransport, BaseTransport + +T = typing.TypeVar("T", bound="HTTPTransport") +A = typing.TypeVar("A", bound="AsyncHTTPTransport") + +SOCKET_OPTION = typing.Union[ + typing.Tuple[int, int, int], + typing.Tuple[int, int, typing.Union[bytes, bytearray]], + typing.Tuple[int, int, None, int], +] + +__all__ = ["AsyncHTTPTransport", "HTTPTransport"] + +HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {} + + +def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]: + import httpcore + + return { + httpcore.TimeoutException: TimeoutException, + httpcore.ConnectTimeout: ConnectTimeout, + httpcore.ReadTimeout: ReadTimeout, + httpcore.WriteTimeout: WriteTimeout, + httpcore.PoolTimeout: PoolTimeout, + httpcore.NetworkError: NetworkError, + httpcore.ConnectError: ConnectError, + httpcore.ReadError: ReadError, + httpcore.WriteError: WriteError, + httpcore.ProxyError: ProxyError, + httpcore.UnsupportedProtocol: UnsupportedProtocol, + httpcore.ProtocolError: ProtocolError, + httpcore.LocalProtocolError: LocalProtocolError, + httpcore.RemoteProtocolError: RemoteProtocolError, + } + + +@contextlib.contextmanager +def map_httpcore_exceptions() -> typing.Iterator[None]: + global HTTPCORE_EXC_MAP + if len(HTTPCORE_EXC_MAP) == 0: + HTTPCORE_EXC_MAP = _load_httpcore_exceptions() + try: + yield + except Exception as exc: + mapped_exc = None + + for from_exc, to_exc in HTTPCORE_EXC_MAP.items(): + if not isinstance(exc, from_exc): + continue + # We want to map to the most specific exception we can find. + # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to + # `httpx.ReadTimeout`, not just `httpx.TimeoutException`. + if mapped_exc is None or issubclass(to_exc, mapped_exc): + mapped_exc = to_exc + + if mapped_exc is None: # pragma: no cover + raise + + message = str(exc) + raise mapped_exc(message) from exc + + +class ResponseStream(SyncByteStream): + def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None: + self._httpcore_stream = httpcore_stream + + def __iter__(self) -> typing.Iterator[bytes]: + with map_httpcore_exceptions(): + for part in self._httpcore_stream: + yield part + + def close(self) -> None: + if hasattr(self._httpcore_stream, "close"): + self._httpcore_stream.close() + + +class HTTPTransport(BaseTransport): + def __init__( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + proxy: ProxyTypes | None = None, + uds: str | None = None, + local_address: str | None = None, + retries: int = 0, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + import httpcore + + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + + if proxy is None: + self._pool = httpcore.ConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("http", "https"): + self._pool = httpcore.HTTPProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + proxy_headers=proxy.headers.raw, + ssl_context=ssl_context, + proxy_ssl_context=proxy.ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("socks5", "socks5h"): + try: + import socksio # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) from None + + self._pool = httpcore.SOCKSProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + else: # pragma: no cover + raise ValueError( + "Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h'," + f" but got {proxy.url.scheme!r}." + ) + + def __enter__(self: T) -> T: # Use generics for subclass support. + self._pool.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + with map_httpcore_exceptions(): + self._pool.__exit__(exc_type, exc_value, traceback) + + def handle_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, SyncByteStream) + import httpcore + + req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + with map_httpcore_exceptions(): + resp = self._pool.handle_request(req) + + assert isinstance(resp.stream, typing.Iterable) + + return Response( + status_code=resp.status, + headers=resp.headers, + stream=ResponseStream(resp.stream), + extensions=resp.extensions, + ) + + def close(self) -> None: + self._pool.close() + + +class AsyncResponseStream(AsyncByteStream): + def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]) -> None: + self._httpcore_stream = httpcore_stream + + async def __aiter__(self) -> typing.AsyncIterator[bytes]: + with map_httpcore_exceptions(): + async for part in self._httpcore_stream: + yield part + + async def aclose(self) -> None: + if hasattr(self._httpcore_stream, "aclose"): + await self._httpcore_stream.aclose() + + +class AsyncHTTPTransport(AsyncBaseTransport): + def __init__( + self, + verify: ssl.SSLContext | str | bool = True, + cert: CertTypes | None = None, + trust_env: bool = True, + http1: bool = True, + http2: bool = False, + limits: Limits = DEFAULT_LIMITS, + proxy: ProxyTypes | None = None, + uds: str | None = None, + local_address: str | None = None, + retries: int = 0, + socket_options: typing.Iterable[SOCKET_OPTION] | None = None, + ) -> None: + import httpcore + + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + + if proxy is None: + self._pool = httpcore.AsyncConnectionPool( + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + uds=uds, + local_address=local_address, + retries=retries, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("http", "https"): + self._pool = httpcore.AsyncHTTPProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + proxy_headers=proxy.headers.raw, + proxy_ssl_context=proxy.ssl_context, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + socket_options=socket_options, + ) + elif proxy.url.scheme in ("socks5", "socks5h"): + try: + import socksio # noqa + except ImportError: # pragma: no cover + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) from None + + self._pool = httpcore.AsyncSOCKSProxy( + proxy_url=httpcore.URL( + scheme=proxy.url.raw_scheme, + host=proxy.url.raw_host, + port=proxy.url.port, + target=proxy.url.raw_path, + ), + proxy_auth=proxy.raw_auth, + ssl_context=ssl_context, + max_connections=limits.max_connections, + max_keepalive_connections=limits.max_keepalive_connections, + keepalive_expiry=limits.keepalive_expiry, + http1=http1, + http2=http2, + ) + else: # pragma: no cover + raise ValueError( + "Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h'," + " but got {proxy.url.scheme!r}." + ) + + async def __aenter__(self: A) -> A: # Use generics for subclass support. + await self._pool.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + with map_httpcore_exceptions(): + await self._pool.__aexit__(exc_type, exc_value, traceback) + + async def handle_async_request( + self, + request: Request, + ) -> Response: + assert isinstance(request.stream, AsyncByteStream) + import httpcore + + req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + with map_httpcore_exceptions(): + resp = await self._pool.handle_async_request(req) + + assert isinstance(resp.stream, typing.AsyncIterable) + + return Response( + status_code=resp.status, + headers=resp.headers, + stream=AsyncResponseStream(resp.stream), + extensions=resp.extensions, + ) + + async def aclose(self) -> None: + await self._pool.aclose() diff --git a/lib/python3.12/site-packages/httpx/_transports/mock.py b/lib/python3.12/site-packages/httpx/_transports/mock.py new file mode 100644 index 0000000000000000000000000000000000000000..8c418f59e06cae43abdbb626ec21cafc7e8c6277 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_transports/mock.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .._models import Request, Response +from .base import AsyncBaseTransport, BaseTransport + +SyncHandler = typing.Callable[[Request], Response] +AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]] + + +__all__ = ["MockTransport"] + + +class MockTransport(AsyncBaseTransport, BaseTransport): + def __init__(self, handler: SyncHandler | AsyncHandler) -> None: + self.handler = handler + + def handle_request( + self, + request: Request, + ) -> Response: + request.read() + response = self.handler(request) + if not isinstance(response, Response): # pragma: no cover + raise TypeError("Cannot use an async handler in a sync Client") + return response + + async def handle_async_request( + self, + request: Request, + ) -> Response: + await request.aread() + response = self.handler(request) + + # Allow handler to *optionally* be an `async` function. + # If it is, then the `response` variable need to be awaited to actually + # return the result. + + if not isinstance(response, Response): + response = await response + + return response diff --git a/lib/python3.12/site-packages/httpx/_transports/wsgi.py b/lib/python3.12/site-packages/httpx/_transports/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..8592ffe017a87367cc7578184540096a9682908d --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_transports/wsgi.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import io +import itertools +import sys +import typing + +from .._models import Request, Response +from .._types import SyncByteStream +from .base import BaseTransport + +if typing.TYPE_CHECKING: + from _typeshed import OptExcInfo # pragma: no cover + from _typeshed.wsgi import WSGIApplication # pragma: no cover + +_T = typing.TypeVar("_T") + + +__all__ = ["WSGITransport"] + + +def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]: + body = iter(body) + for chunk in body: + if chunk: + return itertools.chain([chunk], body) + return [] + + +class WSGIByteStream(SyncByteStream): + def __init__(self, result: typing.Iterable[bytes]) -> None: + self._close = getattr(result, "close", None) + self._result = _skip_leading_empty_chunks(result) + + def __iter__(self) -> typing.Iterator[bytes]: + for part in self._result: + yield part + + def close(self) -> None: + if self._close is not None: + self._close() + + +class WSGITransport(BaseTransport): + """ + A custom transport that handles sending requests directly to an WSGI app. + The simplest way to use this functionality is to use the `app` argument. + + ``` + client = httpx.Client(app=app) + ``` + + Alternatively, you can setup the transport instance explicitly. + This allows you to include any additional configuration arguments specific + to the WSGITransport class: + + ``` + transport = httpx.WSGITransport( + app=app, + script_name="/submount", + remote_addr="1.2.3.4" + ) + client = httpx.Client(transport=transport) + ``` + + Arguments: + + * `app` - The WSGI application. + * `raise_app_exceptions` - Boolean indicating if exceptions in the application + should be raised. Default to `True`. Can be set to `False` for use cases + such as testing the content of a client 500 response. + * `script_name` - The root path on which the WSGI application should be mounted. + * `remote_addr` - A string indicating the client IP of incoming requests. + ``` + """ + + def __init__( + self, + app: WSGIApplication, + raise_app_exceptions: bool = True, + script_name: str = "", + remote_addr: str = "127.0.0.1", + wsgi_errors: typing.TextIO | None = None, + ) -> None: + self.app = app + self.raise_app_exceptions = raise_app_exceptions + self.script_name = script_name + self.remote_addr = remote_addr + self.wsgi_errors = wsgi_errors + + def handle_request(self, request: Request) -> Response: + request.read() + wsgi_input = io.BytesIO(request.content) + + port = request.url.port or {"http": 80, "https": 443}[request.url.scheme] + environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": request.url.scheme, + "wsgi.input": wsgi_input, + "wsgi.errors": self.wsgi_errors or sys.stderr, + "wsgi.multithread": True, + "wsgi.multiprocess": False, + "wsgi.run_once": False, + "REQUEST_METHOD": request.method, + "SCRIPT_NAME": self.script_name, + "PATH_INFO": request.url.path, + "QUERY_STRING": request.url.query.decode("ascii"), + "SERVER_NAME": request.url.host, + "SERVER_PORT": str(port), + "SERVER_PROTOCOL": "HTTP/1.1", + "REMOTE_ADDR": self.remote_addr, + } + for header_key, header_value in request.headers.raw: + key = header_key.decode("ascii").upper().replace("-", "_") + if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"): + key = "HTTP_" + key + environ[key] = header_value.decode("ascii") + + seen_status = None + seen_response_headers = None + seen_exc_info = None + + def start_response( + status: str, + response_headers: list[tuple[str, str]], + exc_info: OptExcInfo | None = None, + ) -> typing.Callable[[bytes], typing.Any]: + nonlocal seen_status, seen_response_headers, seen_exc_info + seen_status = status + seen_response_headers = response_headers + seen_exc_info = exc_info + return lambda _: None + + result = self.app(environ, start_response) + + stream = WSGIByteStream(result) + + assert seen_status is not None + assert seen_response_headers is not None + if seen_exc_info and seen_exc_info[0] and self.raise_app_exceptions: + raise seen_exc_info[1] + + status_code = int(seen_status.split()[0]) + headers = [ + (key.encode("ascii"), value.encode("ascii")) + for key, value in seen_response_headers + ] + + return Response(status_code, headers=headers, stream=stream) diff --git a/lib/python3.12/site-packages/httpx/_types.py b/lib/python3.12/site-packages/httpx/_types.py new file mode 100644 index 0000000000000000000000000000000000000000..704dfdffc8ba61eb913fa918072381e410b23c00 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_types.py @@ -0,0 +1,114 @@ +""" +Type definitions for type checking purposes. +""" + +from http.cookiejar import CookieJar +from typing import ( + IO, + TYPE_CHECKING, + Any, + AsyncIterable, + AsyncIterator, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) + +if TYPE_CHECKING: # pragma: no cover + from ._auth import Auth # noqa: F401 + from ._config import Proxy, Timeout # noqa: F401 + from ._models import Cookies, Headers, Request # noqa: F401 + from ._urls import URL, QueryParams # noqa: F401 + + +PrimitiveData = Optional[Union[str, int, float, bool]] + +URLTypes = Union["URL", str] + +QueryParamTypes = Union[ + "QueryParams", + Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]], + List[Tuple[str, PrimitiveData]], + Tuple[Tuple[str, PrimitiveData], ...], + str, + bytes, +] + +HeaderTypes = Union[ + "Headers", + Mapping[str, str], + Mapping[bytes, bytes], + Sequence[Tuple[str, str]], + Sequence[Tuple[bytes, bytes]], +] + +CookieTypes = Union["Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]] + +TimeoutTypes = Union[ + Optional[float], + Tuple[Optional[float], Optional[float], Optional[float], Optional[float]], + "Timeout", +] +ProxyTypes = Union["URL", str, "Proxy"] +CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]] + +AuthTypes = Union[ + Tuple[Union[str, bytes], Union[str, bytes]], + Callable[["Request"], "Request"], + "Auth", +] + +RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +ResponseExtensions = Mapping[str, Any] + +RequestData = Mapping[str, Any] + +FileContent = Union[IO[bytes], bytes, str] +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +RequestExtensions = Mapping[str, Any] + +__all__ = ["AsyncByteStream", "SyncByteStream"] + + +class SyncByteStream: + def __iter__(self) -> Iterator[bytes]: + raise NotImplementedError( + "The '__iter__' method must be implemented." + ) # pragma: no cover + yield b"" # pragma: no cover + + def close(self) -> None: + """ + Subclasses can override this method to release any network resources + after a request/response cycle is complete. + """ + + +class AsyncByteStream: + async def __aiter__(self) -> AsyncIterator[bytes]: + raise NotImplementedError( + "The '__aiter__' method must be implemented." + ) # pragma: no cover + yield b"" # pragma: no cover + + async def aclose(self) -> None: + pass diff --git a/lib/python3.12/site-packages/httpx/_urlparse.py b/lib/python3.12/site-packages/httpx/_urlparse.py new file mode 100644 index 0000000000000000000000000000000000000000..bf190fd560ee4fc8a11af371a15fc5f1dc284d34 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_urlparse.py @@ -0,0 +1,527 @@ +""" +An implementation of `urlparse` that provides URL validation and normalization +as described by RFC3986. + +We rely on this implementation rather than the one in Python's stdlib, because: + +* It provides more complete URL validation. +* It properly differentiates between an empty querystring and an absent querystring, + to distinguish URLs with a trailing '?'. +* It handles scheme, hostname, port, and path normalization. +* It supports IDNA hostnames, normalizing them to their encoded form. +* The API supports passing individual components, as well as the complete URL string. + +Previously we relied on the excellent `rfc3986` package to handle URL parsing and +validation, but this module provides a simpler alternative, with less indirection +required. +""" + +from __future__ import annotations + +import ipaddress +import re +import typing + +import idna + +from ._exceptions import InvalidURL + +MAX_URL_LENGTH = 65536 + +# https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.3 +UNRESERVED_CHARACTERS = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" +) +SUB_DELIMS = "!$&'()*+,;=" + +PERCENT_ENCODED_REGEX = re.compile("%[A-Fa-f0-9]{2}") + +# https://url.spec.whatwg.org/#percent-encoded-bytes + +# The fragment percent-encode set is the C0 control percent-encode set +# and U+0020 SPACE, U+0022 ("), U+003C (<), U+003E (>), and U+0060 (`). +FRAG_SAFE = "".join( + [chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x3C, 0x3E, 0x60)] +) + +# The query percent-encode set is the C0 control percent-encode set +# and U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>). +QUERY_SAFE = "".join( + [chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E)] +) + +# The path percent-encode set is the query percent-encode set +# and U+003F (?), U+0060 (`), U+007B ({), and U+007D (}). +PATH_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + (0x3F, 0x60, 0x7B, 0x7D) + ] +) + +# The userinfo percent-encode set is the path percent-encode set +# and U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+0040 (@), +# U+005B ([) to U+005E (^), inclusive, and U+007C (|). +USERNAME_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i + not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + + (0x3F, 0x60, 0x7B, 0x7D) + + (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C) + ] +) +PASSWORD_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i + not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + + (0x3F, 0x60, 0x7B, 0x7D) + + (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C) + ] +) +# Note... The terminology 'userinfo' percent-encode set in the WHATWG document +# is used for the username and password quoting. For the joint userinfo component +# we remove U+003A (:) from the safe set. +USERINFO_SAFE = "".join( + [ + chr(i) + for i in range(0x20, 0x7F) + if i + not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + + (0x3F, 0x60, 0x7B, 0x7D) + + (0x2F, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C) + ] +) + + +# {scheme}: (optional) +# //{authority} (optional) +# {path} +# ?{query} (optional) +# #{fragment} (optional) +URL_REGEX = re.compile( + ( + r"(?:(?P{scheme}):)?" + r"(?://(?P{authority}))?" + r"(?P{path})" + r"(?:\?(?P{query}))?" + r"(?:#(?P{fragment}))?" + ).format( + scheme="([a-zA-Z][a-zA-Z0-9+.-]*)?", + authority="[^/?#]*", + path="[^?#]*", + query="[^#]*", + fragment=".*", + ) +) + +# {userinfo}@ (optional) +# {host} +# :{port} (optional) +AUTHORITY_REGEX = re.compile( + ( + r"(?:(?P{userinfo})@)?" r"(?P{host})" r":?(?P{port})?" + ).format( + userinfo=".*", # Any character sequence. + host="(\\[.*\\]|[^:@]*)", # Either any character sequence excluding ':' or '@', + # or an IPv6 address enclosed within square brackets. + port=".*", # Any character sequence. + ) +) + + +# If we call urlparse with an individual component, then we need to regex +# validate that component individually. +# Note that we're duplicating the same strings as above. Shock! Horror!! +COMPONENT_REGEX = { + "scheme": re.compile("([a-zA-Z][a-zA-Z0-9+.-]*)?"), + "authority": re.compile("[^/?#]*"), + "path": re.compile("[^?#]*"), + "query": re.compile("[^#]*"), + "fragment": re.compile(".*"), + "userinfo": re.compile("[^@]*"), + "host": re.compile("(\\[.*\\]|[^:]*)"), + "port": re.compile(".*"), +} + + +# We use these simple regexs as a first pass before handing off to +# the stdlib 'ipaddress' module for IP address validation. +IPv4_STYLE_HOSTNAME = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$") +IPv6_STYLE_HOSTNAME = re.compile(r"^\[.*\]$") + + +class ParseResult(typing.NamedTuple): + scheme: str + userinfo: str + host: str + port: int | None + path: str + query: str | None + fragment: str | None + + @property + def authority(self) -> str: + return "".join( + [ + f"{self.userinfo}@" if self.userinfo else "", + f"[{self.host}]" if ":" in self.host else self.host, + f":{self.port}" if self.port is not None else "", + ] + ) + + @property + def netloc(self) -> str: + return "".join( + [ + f"[{self.host}]" if ":" in self.host else self.host, + f":{self.port}" if self.port is not None else "", + ] + ) + + def copy_with(self, **kwargs: str | None) -> ParseResult: + if not kwargs: + return self + + defaults = { + "scheme": self.scheme, + "authority": self.authority, + "path": self.path, + "query": self.query, + "fragment": self.fragment, + } + defaults.update(kwargs) + return urlparse("", **defaults) + + def __str__(self) -> str: + authority = self.authority + return "".join( + [ + f"{self.scheme}:" if self.scheme else "", + f"//{authority}" if authority else "", + self.path, + f"?{self.query}" if self.query is not None else "", + f"#{self.fragment}" if self.fragment is not None else "", + ] + ) + + +def urlparse(url: str = "", **kwargs: str | None) -> ParseResult: + # Initial basic checks on allowable URLs. + # --------------------------------------- + + # Hard limit the maximum allowable URL length. + if len(url) > MAX_URL_LENGTH: + raise InvalidURL("URL too long") + + # If a URL includes any ASCII control characters including \t, \r, \n, + # then treat it as invalid. + if any(char.isascii() and not char.isprintable() for char in url): + char = next(char for char in url if char.isascii() and not char.isprintable()) + idx = url.find(char) + error = ( + f"Invalid non-printable ASCII character in URL, {char!r} at position {idx}." + ) + raise InvalidURL(error) + + # Some keyword arguments require special handling. + # ------------------------------------------------ + + # Coerce "port" to a string, if it is provided as an integer. + if "port" in kwargs: + port = kwargs["port"] + kwargs["port"] = str(port) if isinstance(port, int) else port + + # Replace "netloc" with "host and "port". + if "netloc" in kwargs: + netloc = kwargs.pop("netloc") or "" + kwargs["host"], _, kwargs["port"] = netloc.partition(":") + + # Replace "username" and/or "password" with "userinfo". + if "username" in kwargs or "password" in kwargs: + username = quote(kwargs.pop("username", "") or "", safe=USERNAME_SAFE) + password = quote(kwargs.pop("password", "") or "", safe=PASSWORD_SAFE) + kwargs["userinfo"] = f"{username}:{password}" if password else username + + # Replace "raw_path" with "path" and "query". + if "raw_path" in kwargs: + raw_path = kwargs.pop("raw_path") or "" + kwargs["path"], seperator, kwargs["query"] = raw_path.partition("?") + if not seperator: + kwargs["query"] = None + + # Ensure that IPv6 "host" addresses are always escaped with "[...]". + if "host" in kwargs: + host = kwargs.get("host") or "" + if ":" in host and not (host.startswith("[") and host.endswith("]")): + kwargs["host"] = f"[{host}]" + + # If any keyword arguments are provided, ensure they are valid. + # ------------------------------------------------------------- + + for key, value in kwargs.items(): + if value is not None: + if len(value) > MAX_URL_LENGTH: + raise InvalidURL(f"URL component '{key}' too long") + + # If a component includes any ASCII control characters including \t, \r, \n, + # then treat it as invalid. + if any(char.isascii() and not char.isprintable() for char in value): + char = next( + char for char in value if char.isascii() and not char.isprintable() + ) + idx = value.find(char) + error = ( + f"Invalid non-printable ASCII character in URL {key} component, " + f"{char!r} at position {idx}." + ) + raise InvalidURL(error) + + # Ensure that keyword arguments match as a valid regex. + if not COMPONENT_REGEX[key].fullmatch(value): + raise InvalidURL(f"Invalid URL component '{key}'") + + # The URL_REGEX will always match, but may have empty components. + url_match = URL_REGEX.match(url) + assert url_match is not None + url_dict = url_match.groupdict() + + # * 'scheme', 'authority', and 'path' may be empty strings. + # * 'query' may be 'None', indicating no trailing "?" portion. + # Any string including the empty string, indicates a trailing "?". + # * 'fragment' may be 'None', indicating no trailing "#" portion. + # Any string including the empty string, indicates a trailing "#". + scheme = kwargs.get("scheme", url_dict["scheme"]) or "" + authority = kwargs.get("authority", url_dict["authority"]) or "" + path = kwargs.get("path", url_dict["path"]) or "" + query = kwargs.get("query", url_dict["query"]) + frag = kwargs.get("fragment", url_dict["fragment"]) + + # The AUTHORITY_REGEX will always match, but may have empty components. + authority_match = AUTHORITY_REGEX.match(authority) + assert authority_match is not None + authority_dict = authority_match.groupdict() + + # * 'userinfo' and 'host' may be empty strings. + # * 'port' may be 'None'. + userinfo = kwargs.get("userinfo", authority_dict["userinfo"]) or "" + host = kwargs.get("host", authority_dict["host"]) or "" + port = kwargs.get("port", authority_dict["port"]) + + # Normalize and validate each component. + # We end up with a parsed representation of the URL, + # with components that are plain ASCII bytestrings. + parsed_scheme: str = scheme.lower() + parsed_userinfo: str = quote(userinfo, safe=USERINFO_SAFE) + parsed_host: str = encode_host(host) + parsed_port: int | None = normalize_port(port, scheme) + + has_scheme = parsed_scheme != "" + has_authority = ( + parsed_userinfo != "" or parsed_host != "" or parsed_port is not None + ) + validate_path(path, has_scheme=has_scheme, has_authority=has_authority) + if has_scheme or has_authority: + path = normalize_path(path) + + parsed_path: str = quote(path, safe=PATH_SAFE) + parsed_query: str | None = None if query is None else quote(query, safe=QUERY_SAFE) + parsed_frag: str | None = None if frag is None else quote(frag, safe=FRAG_SAFE) + + # The parsed ASCII bytestrings are our canonical form. + # All properties of the URL are derived from these. + return ParseResult( + parsed_scheme, + parsed_userinfo, + parsed_host, + parsed_port, + parsed_path, + parsed_query, + parsed_frag, + ) + + +def encode_host(host: str) -> str: + if not host: + return "" + + elif IPv4_STYLE_HOSTNAME.match(host): + # Validate IPv4 hostnames like #.#.#.# + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + try: + ipaddress.IPv4Address(host) + except ipaddress.AddressValueError: + raise InvalidURL(f"Invalid IPv4 address: {host!r}") + return host + + elif IPv6_STYLE_HOSTNAME.match(host): + # Validate IPv6 hostnames like [...] + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # "A host identified by an Internet Protocol literal address, version 6 + # [RFC3513] or later, is distinguished by enclosing the IP literal + # within square brackets ("[" and "]"). This is the only place where + # square bracket characters are allowed in the URI syntax." + try: + ipaddress.IPv6Address(host[1:-1]) + except ipaddress.AddressValueError: + raise InvalidURL(f"Invalid IPv6 address: {host!r}") + return host[1:-1] + + elif host.isascii(): + # Regular ASCII hostnames + # + # From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2 + # + # reg-name = *( unreserved / pct-encoded / sub-delims ) + WHATWG_SAFE = '"`{}%|\\' + return quote(host.lower(), safe=SUB_DELIMS + WHATWG_SAFE) + + # IDNA hostnames + try: + return idna.encode(host.lower()).decode("ascii") + except idna.IDNAError: + raise InvalidURL(f"Invalid IDNA hostname: {host!r}") + + +def normalize_port(port: str | int | None, scheme: str) -> int | None: + # From https://tools.ietf.org/html/rfc3986#section-3.2.3 + # + # "A scheme may define a default port. For example, the "http" scheme + # defines a default port of "80", corresponding to its reserved TCP + # port number. The type of port designated by the port number (e.g., + # TCP, UDP, SCTP) is defined by the URI scheme. URI producers and + # normalizers should omit the port component and its ":" delimiter if + # port is empty or if its value would be the same as that of the + # scheme's default." + if port is None or port == "": + return None + + try: + port_as_int = int(port) + except ValueError: + raise InvalidURL(f"Invalid port: {port!r}") + + # See https://url.spec.whatwg.org/#url-miscellaneous + default_port = {"ftp": 21, "http": 80, "https": 443, "ws": 80, "wss": 443}.get( + scheme + ) + if port_as_int == default_port: + return None + return port_as_int + + +def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None: + """ + Path validation rules that depend on if the URL contains + a scheme or authority component. + + See https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3 + """ + if has_authority: + # If a URI contains an authority component, then the path component + # must either be empty or begin with a slash ("/") character." + if path and not path.startswith("/"): + raise InvalidURL("For absolute URLs, path must be empty or begin with '/'") + + if not has_scheme and not has_authority: + # If a URI does not contain an authority component, then the path cannot begin + # with two slash characters ("//"). + if path.startswith("//"): + raise InvalidURL("Relative URLs cannot have a path starting with '//'") + + # In addition, a URI reference (Section 4.1) may be a relative-path reference, + # in which case the first path segment cannot contain a colon (":") character. + if path.startswith(":"): + raise InvalidURL("Relative URLs cannot have a path starting with ':'") + + +def normalize_path(path: str) -> str: + """ + Drop "." and ".." segments from a URL path. + + For example: + + normalize_path("/path/./to/somewhere/..") == "/path/to" + """ + # Fast return when no '.' characters in the path. + if "." not in path: + return path + + components = path.split("/") + + # Fast return when no '.' or '..' components in the path. + if "." not in components and ".." not in components: + return path + + # https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 + output: list[str] = [] + for component in components: + if component == ".": + pass + elif component == "..": + if output and output != [""]: + output.pop() + else: + output.append(component) + return "/".join(output) + + +def PERCENT(string: str) -> str: + return "".join([f"%{byte:02X}" for byte in string.encode("utf-8")]) + + +def percent_encoded(string: str, safe: str) -> str: + """ + Use percent-encoding to quote a string. + """ + NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + + # Fast path for strings that don't need escaping. + if not string.rstrip(NON_ESCAPED_CHARS): + return string + + return "".join( + [char if char in NON_ESCAPED_CHARS else PERCENT(char) for char in string] + ) + + +def quote(string: str, safe: str) -> str: + """ + Use percent-encoding to quote a string, omitting existing '%xx' escape sequences. + + See: https://www.rfc-editor.org/rfc/rfc3986#section-2.1 + + * `string`: The string to be percent-escaped. + * `safe`: A string containing characters that may be treated as safe, and do not + need to be escaped. Unreserved characters are always treated as safe. + See: https://www.rfc-editor.org/rfc/rfc3986#section-2.3 + """ + parts = [] + current_position = 0 + for match in re.finditer(PERCENT_ENCODED_REGEX, string): + start_position, end_position = match.start(), match.end() + matched_text = match.group(0) + # Add any text up to the '%xx' escape sequence. + if start_position != current_position: + leading_text = string[current_position:start_position] + parts.append(percent_encoded(leading_text, safe=safe)) + + # Add the '%xx' escape sequence. + parts.append(matched_text) + current_position = end_position + + # Add any text after the final '%xx' escape sequence. + if current_position != len(string): + trailing_text = string[current_position:] + parts.append(percent_encoded(trailing_text, safe=safe)) + + return "".join(parts) diff --git a/lib/python3.12/site-packages/httpx/_urls.py b/lib/python3.12/site-packages/httpx/_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..147a8fa333acaf31618d37ba2896e3a5bf5e4d02 --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_urls.py @@ -0,0 +1,641 @@ +from __future__ import annotations + +import typing +from urllib.parse import parse_qs, unquote, urlencode + +import idna + +from ._types import QueryParamTypes +from ._urlparse import urlparse +from ._utils import primitive_value_to_str + +__all__ = ["URL", "QueryParams"] + + +class URL: + """ + url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink") + + assert url.scheme == "https" + assert url.username == "jo@email.com" + assert url.password == "a secret" + assert url.userinfo == b"jo%40email.com:a%20secret" + assert url.host == "müller.de" + assert url.raw_host == b"xn--mller-kva.de" + assert url.port == 1234 + assert url.netloc == b"xn--mller-kva.de:1234" + assert url.path == "/pa th" + assert url.query == b"?search=ab" + assert url.raw_path == b"/pa%20th?search=ab" + assert url.fragment == "anchorlink" + + The components of a URL are broken down like this: + + https://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink + [scheme] [ username ] [password] [ host ][port][ path ] [ query ] [fragment] + [ userinfo ] [ netloc ][ raw_path ] + + Note that: + + * `url.scheme` is normalized to always be lowercased. + + * `url.host` is normalized to always be lowercased. Internationalized domain + names are represented in unicode, without IDNA encoding applied. For instance: + + url = httpx.URL("http://中国.icom.museum") + assert url.host == "中国.icom.museum" + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.host == "中国.icom.museum" + + * `url.raw_host` is normalized to always be lowercased, and is IDNA encoded. + + url = httpx.URL("http://中国.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + * `url.port` is either None or an integer. URLs that include the default port for + "http", "https", "ws", "wss", and "ftp" schemes have their port + normalized to `None`. + + assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80") + assert httpx.URL("http://example.com").port is None + assert httpx.URL("http://example.com:80").port is None + + * `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work + with `url.username` and `url.password` instead, which handle the URL escaping. + + * `url.raw_path` is raw bytes of both the path and query, without URL escaping. + This portion is used as the target when constructing HTTP requests. Usually you'll + want to work with `url.path` instead. + + * `url.query` is raw bytes, without URL escaping. A URL query string portion can + only be properly URL escaped when decoding the parameter names and values + themselves. + """ + + def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None: + if kwargs: + allowed = { + "scheme": str, + "username": str, + "password": str, + "userinfo": bytes, + "host": str, + "port": int, + "netloc": bytes, + "path": str, + "query": bytes, + "raw_path": bytes, + "fragment": str, + "params": object, + } + + # Perform type checking for all supported keyword arguments. + for key, value in kwargs.items(): + if key not in allowed: + message = f"{key!r} is an invalid keyword argument for URL()" + raise TypeError(message) + if value is not None and not isinstance(value, allowed[key]): + expected = allowed[key].__name__ + seen = type(value).__name__ + message = f"Argument {key!r} must be {expected} but got {seen}" + raise TypeError(message) + if isinstance(value, bytes): + kwargs[key] = value.decode("ascii") + + if "params" in kwargs: + # Replace any "params" keyword with the raw "query" instead. + # + # Ensure that empty params use `kwargs["query"] = None` rather + # than `kwargs["query"] = ""`, so that generated URLs do not + # include an empty trailing "?". + params = kwargs.pop("params") + kwargs["query"] = None if not params else str(QueryParams(params)) + + if isinstance(url, str): + self._uri_reference = urlparse(url, **kwargs) + elif isinstance(url, URL): + self._uri_reference = url._uri_reference.copy_with(**kwargs) + else: + raise TypeError( + "Invalid type for url. Expected str or httpx.URL," + f" got {type(url)}: {url!r}" + ) + + @property + def scheme(self) -> str: + """ + The URL scheme, such as "http", "https". + Always normalised to lowercase. + """ + return self._uri_reference.scheme + + @property + def raw_scheme(self) -> bytes: + """ + The raw bytes representation of the URL scheme, such as b"http", b"https". + Always normalised to lowercase. + """ + return self._uri_reference.scheme.encode("ascii") + + @property + def userinfo(self) -> bytes: + """ + The URL userinfo as a raw bytestring. + For example: b"jo%40email.com:a%20secret". + """ + return self._uri_reference.userinfo.encode("ascii") + + @property + def username(self) -> str: + """ + The URL username as a string, with URL decoding applied. + For example: "jo@email.com" + """ + userinfo = self._uri_reference.userinfo + return unquote(userinfo.partition(":")[0]) + + @property + def password(self) -> str: + """ + The URL password as a string, with URL decoding applied. + For example: "a secret" + """ + userinfo = self._uri_reference.userinfo + return unquote(userinfo.partition(":")[2]) + + @property + def host(self) -> str: + """ + The URL host as a string. + Always normalized to lowercase, with IDNA hosts decoded into unicode. + + Examples: + + url = httpx.URL("http://www.EXAMPLE.org") + assert url.host == "www.example.org" + + url = httpx.URL("http://中国.icom.museum") + assert url.host == "中国.icom.museum" + + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.host == "中国.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.host == "::ffff:192.168.0.1" + """ + host: str = self._uri_reference.host + + if host.startswith("xn--"): + host = idna.decode(host) + + return host + + @property + def raw_host(self) -> bytes: + """ + The raw bytes representation of the URL host. + Always normalized to lowercase, and IDNA encoded. + + Examples: + + url = httpx.URL("http://www.EXAMPLE.org") + assert url.raw_host == b"www.example.org" + + url = httpx.URL("http://中国.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + url = httpx.URL("http://xn--fiqs8s.icom.museum") + assert url.raw_host == b"xn--fiqs8s.icom.museum" + + url = httpx.URL("https://[::ffff:192.168.0.1]") + assert url.raw_host == b"::ffff:192.168.0.1" + """ + return self._uri_reference.host.encode("ascii") + + @property + def port(self) -> int | None: + """ + The URL port as an integer. + + Note that the URL class performs port normalization as per the WHATWG spec. + Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always + treated as `None`. + + For example: + + assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80") + assert httpx.URL("http://www.example.com:80").port is None + """ + return self._uri_reference.port + + @property + def netloc(self) -> bytes: + """ + Either `` or `:` as bytes. + Always normalized to lowercase, and IDNA encoded. + + This property may be used for generating the value of a request + "Host" header. + """ + return self._uri_reference.netloc.encode("ascii") + + @property + def path(self) -> str: + """ + The URL path as a string. Excluding the query string, and URL decoded. + + For example: + + url = httpx.URL("https://example.com/pa%20th") + assert url.path == "/pa th" + """ + path = self._uri_reference.path or "/" + return unquote(path) + + @property + def query(self) -> bytes: + """ + The URL query string, as raw bytes, excluding the leading b"?". + + This is necessarily a bytewise interface, because we cannot + perform URL decoding of this representation until we've parsed + the keys and values into a QueryParams instance. + + For example: + + url = httpx.URL("https://example.com/?filter=some%20search%20terms") + assert url.query == b"filter=some%20search%20terms" + """ + query = self._uri_reference.query or "" + return query.encode("ascii") + + @property + def params(self) -> QueryParams: + """ + The URL query parameters, neatly parsed and packaged into an immutable + multidict representation. + """ + return QueryParams(self._uri_reference.query) + + @property + def raw_path(self) -> bytes: + """ + The complete URL path and query string as raw bytes. + Used as the target when constructing HTTP requests. + + For example: + + GET /users?search=some%20text HTTP/1.1 + Host: www.example.org + Connection: close + """ + path = self._uri_reference.path or "/" + if self._uri_reference.query is not None: + path += "?" + self._uri_reference.query + return path.encode("ascii") + + @property + def fragment(self) -> str: + """ + The URL fragments, as used in HTML anchors. + As a string, without the leading '#'. + """ + return unquote(self._uri_reference.fragment or "") + + @property + def is_absolute_url(self) -> bool: + """ + Return `True` for absolute URLs such as 'http://example.com/path', + and `False` for relative URLs such as '/path'. + """ + # We don't use `.is_absolute` from `rfc3986` because it treats + # URLs with a fragment portion as not absolute. + # What we actually care about is if the URL provides + # a scheme and hostname to which connections should be made. + return bool(self._uri_reference.scheme and self._uri_reference.host) + + @property + def is_relative_url(self) -> bool: + """ + Return `False` for absolute URLs such as 'http://example.com/path', + and `True` for relative URLs such as '/path'. + """ + return not self.is_absolute_url + + def copy_with(self, **kwargs: typing.Any) -> URL: + """ + Copy this URL, returning a new URL with some components altered. + Accepts the same set of parameters as the components that are made + available via properties on the `URL` class. + + For example: + + url = httpx.URL("https://www.example.com").copy_with( + username="jo@gmail.com", password="a secret" + ) + assert url == "https://jo%40email.com:a%20secret@www.example.com" + """ + return URL(self, **kwargs) + + def copy_set_param(self, key: str, value: typing.Any = None) -> URL: + return self.copy_with(params=self.params.set(key, value)) + + def copy_add_param(self, key: str, value: typing.Any = None) -> URL: + return self.copy_with(params=self.params.add(key, value)) + + def copy_remove_param(self, key: str) -> URL: + return self.copy_with(params=self.params.remove(key)) + + def copy_merge_params(self, params: QueryParamTypes) -> URL: + return self.copy_with(params=self.params.merge(params)) + + def join(self, url: URL | str) -> URL: + """ + Return an absolute URL, using this URL as the base. + + Eg. + + url = httpx.URL("https://www.example.com/test") + url = url.join("/new/path") + assert url == "https://www.example.com/new/path" + """ + from urllib.parse import urljoin + + return URL(urljoin(str(self), str(URL(url)))) + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, (URL, str)) and str(self) == str(URL(other)) + + def __str__(self) -> str: + return str(self._uri_reference) + + def __repr__(self) -> str: + scheme, userinfo, host, port, path, query, fragment = self._uri_reference + + if ":" in userinfo: + # Mask any password component. + userinfo = f'{userinfo.split(":")[0]}:[secure]' + + authority = "".join( + [ + f"{userinfo}@" if userinfo else "", + f"[{host}]" if ":" in host else host, + f":{port}" if port is not None else "", + ] + ) + url = "".join( + [ + f"{self.scheme}:" if scheme else "", + f"//{authority}" if authority else "", + path, + f"?{query}" if query is not None else "", + f"#{fragment}" if fragment is not None else "", + ] + ) + + return f"{self.__class__.__name__}({url!r})" + + @property + def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma: nocover + import collections + import warnings + + warnings.warn("URL.raw is deprecated.") + RawURL = collections.namedtuple( + "RawURL", ["raw_scheme", "raw_host", "port", "raw_path"] + ) + return RawURL( + raw_scheme=self.raw_scheme, + raw_host=self.raw_host, + port=self.port, + raw_path=self.raw_path, + ) + + +class QueryParams(typing.Mapping[str, str]): + """ + URL query parameters, as a multi-dict. + """ + + def __init__(self, *args: QueryParamTypes | None, **kwargs: typing.Any) -> None: + assert len(args) < 2, "Too many arguments." + assert not (args and kwargs), "Cannot mix named and unnamed arguments." + + value = args[0] if args else kwargs + + if value is None or isinstance(value, (str, bytes)): + value = value.decode("ascii") if isinstance(value, bytes) else value + self._dict = parse_qs(value, keep_blank_values=True) + elif isinstance(value, QueryParams): + self._dict = {k: list(v) for k, v in value._dict.items()} + else: + dict_value: dict[typing.Any, list[typing.Any]] = {} + if isinstance(value, (list, tuple)): + # Convert list inputs like: + # [("a", "123"), ("a", "456"), ("b", "789")] + # To a dict representation, like: + # {"a": ["123", "456"], "b": ["789"]} + for item in value: + dict_value.setdefault(item[0], []).append(item[1]) + else: + # Convert dict inputs like: + # {"a": "123", "b": ["456", "789"]} + # To dict inputs where values are always lists, like: + # {"a": ["123"], "b": ["456", "789"]} + dict_value = { + k: list(v) if isinstance(v, (list, tuple)) else [v] + for k, v in value.items() + } + + # Ensure that keys and values are neatly coerced to strings. + # We coerce values `True` and `False` to JSON-like "true" and "false" + # representations, and coerce `None` values to the empty string. + self._dict = { + str(k): [primitive_value_to_str(item) for item in v] + for k, v in dict_value.items() + } + + def keys(self) -> typing.KeysView[str]: + """ + Return all the keys in the query params. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.keys()) == ["a", "b"] + """ + return self._dict.keys() + + def values(self) -> typing.ValuesView[str]: + """ + Return all the values in the query params. If a key occurs more than once + only the first item for that key is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.values()) == ["123", "789"] + """ + return {k: v[0] for k, v in self._dict.items()}.values() + + def items(self) -> typing.ItemsView[str, str]: + """ + Return all items in the query params. If a key occurs more than once + only the first item for that key is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.items()) == [("a", "123"), ("b", "789")] + """ + return {k: v[0] for k, v in self._dict.items()}.items() + + def multi_items(self) -> list[tuple[str, str]]: + """ + Return all items in the query params. Allow duplicate keys to occur. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")] + """ + multi_items: list[tuple[str, str]] = [] + for k, v in self._dict.items(): + multi_items.extend([(k, i) for i in v]) + return multi_items + + def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any: + """ + Get a value from the query param for a given key. If the key occurs + more than once, then only the first value is returned. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert q.get("a") == "123" + """ + if key in self._dict: + return self._dict[str(key)][0] + return default + + def get_list(self, key: str) -> list[str]: + """ + Get all values from the query param for a given key. + + Usage: + + q = httpx.QueryParams("a=123&a=456&b=789") + assert q.get_list("a") == ["123", "456"] + """ + return list(self._dict.get(str(key), [])) + + def set(self, key: str, value: typing.Any = None) -> QueryParams: + """ + Return a new QueryParams instance, setting the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.set("a", "456") + assert q == httpx.QueryParams("a=456") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict[str(key)] = [primitive_value_to_str(value)] + return q + + def add(self, key: str, value: typing.Any = None) -> QueryParams: + """ + Return a new QueryParams instance, setting or appending the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.add("a", "456") + assert q == httpx.QueryParams("a=123&a=456") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)] + return q + + def remove(self, key: str) -> QueryParams: + """ + Return a new QueryParams instance, removing the value of a key. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.remove("a") + assert q == httpx.QueryParams("") + """ + q = QueryParams() + q._dict = dict(self._dict) + q._dict.pop(str(key), None) + return q + + def merge(self, params: QueryParamTypes | None = None) -> QueryParams: + """ + Return a new QueryParams instance, updated with. + + Usage: + + q = httpx.QueryParams("a=123") + q = q.merge({"b": "456"}) + assert q == httpx.QueryParams("a=123&b=456") + + q = httpx.QueryParams("a=123") + q = q.merge({"a": "456", "b": "789"}) + assert q == httpx.QueryParams("a=456&b=789") + """ + q = QueryParams(params) + q._dict = {**self._dict, **q._dict} + return q + + def __getitem__(self, key: typing.Any) -> str: + return self._dict[key][0] + + def __contains__(self, key: typing.Any) -> bool: + return key in self._dict + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self._dict) + + def __bool__(self) -> bool: + return bool(self._dict) + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: typing.Any) -> bool: + if not isinstance(other, self.__class__): + return False + return sorted(self.multi_items()) == sorted(other.multi_items()) + + def __str__(self) -> str: + return urlencode(self.multi_items()) + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + query_string = str(self) + return f"{class_name}({query_string!r})" + + def update(self, params: QueryParamTypes | None = None) -> None: + raise RuntimeError( + "QueryParams are immutable since 0.18.0. " + "Use `q = q.merge(...)` to create an updated copy." + ) + + def __setitem__(self, key: str, value: str) -> None: + raise RuntimeError( + "QueryParams are immutable since 0.18.0. " + "Use `q = q.set(key, value)` to create an updated copy." + ) diff --git a/lib/python3.12/site-packages/httpx/_utils.py b/lib/python3.12/site-packages/httpx/_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7fe827da4d071b32ea6da44328629699d6fc88ce --- /dev/null +++ b/lib/python3.12/site-packages/httpx/_utils.py @@ -0,0 +1,242 @@ +from __future__ import annotations + +import ipaddress +import os +import re +import typing +from urllib.request import getproxies + +from ._types import PrimitiveData + +if typing.TYPE_CHECKING: # pragma: no cover + from ._urls import URL + + +def primitive_value_to_str(value: PrimitiveData) -> str: + """ + Coerce a primitive data type into a string value. + + Note that we prefer JSON-style 'true'/'false' for boolean values here. + """ + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +def get_environment_proxies() -> dict[str, str | None]: + """Gets proxy information from the environment""" + + # urllib.request.getproxies() falls back on System + # Registry and Config for proxies on Windows and macOS. + # We don't want to propagate non-HTTP proxies into + # our configuration such as 'TRAVIS_APT_PROXY'. + proxy_info = getproxies() + mounts: dict[str, str | None] = {} + + for scheme in ("http", "https", "all"): + if proxy_info.get(scheme): + hostname = proxy_info[scheme] + mounts[f"{scheme}://"] = ( + hostname if "://" in hostname else f"http://{hostname}" + ) + + no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")] + for hostname in no_proxy_hosts: + # See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details + # on how names in `NO_PROXY` are handled. + if hostname == "*": + # If NO_PROXY=* is used or if "*" occurs as any one of the comma + # separated hostnames, then we should just bypass any information + # from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore + # proxies. + return {} + elif hostname: + # NO_PROXY=.google.com is marked as "all://*.google.com, + # which disables "www.google.com" but not "google.com" + # NO_PROXY=google.com is marked as "all://*google.com, + # which disables "www.google.com" and "google.com". + # (But not "wwwgoogle.com") + # NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost" + # NO_PROXY=example.com,::1,localhost,192.168.0.0/16 + if "://" in hostname: + mounts[hostname] = None + elif is_ipv4_hostname(hostname): + mounts[f"all://{hostname}"] = None + elif is_ipv6_hostname(hostname): + mounts[f"all://[{hostname}]"] = None + elif hostname.lower() == "localhost": + mounts[f"all://{hostname}"] = None + else: + mounts[f"all://*{hostname}"] = None + + return mounts + + +def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes: + return value.encode(encoding) if isinstance(value, str) else value + + +def to_str(value: str | bytes, encoding: str = "utf-8") -> str: + return value if isinstance(value, str) else value.decode(encoding) + + +def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr: + return value if isinstance(match_type_of, str) else value.encode() + + +def unquote(value: str) -> str: + return value[1:-1] if value[0] == value[-1] == '"' else value + + +def peek_filelike_length(stream: typing.Any) -> int | None: + """ + Given a file-like stream object, return its length in number of bytes + without reading it into memory. + """ + try: + # Is it an actual file? + fd = stream.fileno() + # Yup, seems to be an actual file. + length = os.fstat(fd).st_size + except (AttributeError, OSError): + # No... Maybe it's something that supports random access, like `io.BytesIO`? + try: + # Assuming so, go to end of stream to figure out its length, + # then put it back in place. + offset = stream.tell() + length = stream.seek(0, os.SEEK_END) + stream.seek(offset) + except (AttributeError, OSError): + # Not even that? Sorry, we're doomed... + return None + + return length + + +class URLPattern: + """ + A utility class currently used for making lookups against proxy keys... + + # Wildcard matching... + >>> pattern = URLPattern("all://") + >>> pattern.matches(httpx.URL("http://example.com")) + True + + # Witch scheme matching... + >>> pattern = URLPattern("https://") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + False + + # With domain matching... + >>> pattern = URLPattern("https://example.com") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + False + >>> pattern.matches(httpx.URL("https://other.com")) + False + + # Wildcard scheme, with domain matching... + >>> pattern = URLPattern("all://example.com") + >>> pattern.matches(httpx.URL("https://example.com")) + True + >>> pattern.matches(httpx.URL("http://example.com")) + True + >>> pattern.matches(httpx.URL("https://other.com")) + False + + # With port matching... + >>> pattern = URLPattern("https://example.com:1234") + >>> pattern.matches(httpx.URL("https://example.com:1234")) + True + >>> pattern.matches(httpx.URL("https://example.com")) + False + """ + + def __init__(self, pattern: str) -> None: + from ._urls import URL + + if pattern and ":" not in pattern: + raise ValueError( + f"Proxy keys should use proper URL forms rather " + f"than plain scheme strings. " + f'Instead of "{pattern}", use "{pattern}://"' + ) + + url = URL(pattern) + self.pattern = pattern + self.scheme = "" if url.scheme == "all" else url.scheme + self.host = "" if url.host == "*" else url.host + self.port = url.port + if not url.host or url.host == "*": + self.host_regex: typing.Pattern[str] | None = None + elif url.host.startswith("*."): + # *.example.com should match "www.example.com", but not "example.com" + domain = re.escape(url.host[2:]) + self.host_regex = re.compile(f"^.+\\.{domain}$") + elif url.host.startswith("*"): + # *example.com should match "www.example.com" and "example.com" + domain = re.escape(url.host[1:]) + self.host_regex = re.compile(f"^(.+\\.)?{domain}$") + else: + # example.com should match "example.com" but not "www.example.com" + domain = re.escape(url.host) + self.host_regex = re.compile(f"^{domain}$") + + def matches(self, other: URL) -> bool: + if self.scheme and self.scheme != other.scheme: + return False + if ( + self.host + and self.host_regex is not None + and not self.host_regex.match(other.host) + ): + return False + if self.port is not None and self.port != other.port: + return False + return True + + @property + def priority(self) -> tuple[int, int, int]: + """ + The priority allows URLPattern instances to be sortable, so that + we can match from most specific to least specific. + """ + # URLs with a port should take priority over URLs without a port. + port_priority = 0 if self.port is not None else 1 + # Longer hostnames should match first. + host_priority = -len(self.host) + # Longer schemes should match first. + scheme_priority = -len(self.scheme) + return (port_priority, host_priority, scheme_priority) + + def __hash__(self) -> int: + return hash(self.pattern) + + def __lt__(self, other: URLPattern) -> bool: + return self.priority < other.priority + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, URLPattern) and self.pattern == other.pattern + + +def is_ipv4_hostname(hostname: str) -> bool: + try: + ipaddress.IPv4Address(hostname.split("/")[0]) + except Exception: + return False + return True + + +def is_ipv6_hostname(hostname: str) -> bool: + try: + ipaddress.IPv6Address(hostname.split("/")[0]) + except Exception: + return False + return True diff --git a/lib/python3.12/site-packages/httpx/py.typed b/lib/python3.12/site-packages/httpx/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/jedi/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d8209cf46da1ce0a4029459907f398a93696246 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/__main__.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65c884f1c997f4598048ddb0914dae7d81aca89b Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/__main__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/_compatibility.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/_compatibility.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d10e8b15d647df2e757fd8f0b015f15c827605b Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/_compatibility.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/cache.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/cache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29162d24d390a34ace578dc28504ae72932e6c1c Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/cache.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/common.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b8b27553354b4af4269bae7c77b1b608f98b998 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/common.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/debug.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/debug.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71281ed65af28c8cec400e2292eee1c3ed0a2f5a Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/debug.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/file_io.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/file_io.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00cbf79dd22059ecd30b5d29c39623e1249e9872 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/file_io.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/parser_utils.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/parser_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc9f5d849f13c708c1775408dd9b12de5e3e0f25 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/parser_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/settings.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6606b501c0453a3884a8eb3d8f6790ad21c2644a Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/settings.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/jedi/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f6e5c018ab11be1d06641839d6356c7614133ee Binary files /dev/null and b/lib/python3.12/site-packages/jedi/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c88196fc38f02372ed00181c883e91dab7d31b8a Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/classes.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/classes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3951a7afc0550724b60eba784db752536d3cca52 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/classes.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/completion.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/completion.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e65d433d90cc2fe03e26a731539cabf91b610707 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/completion.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/completion_cache.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/completion_cache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72f79d904299311b5c55e82ef7b0e5eecf137f18 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/completion_cache.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/environment.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/environment.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ed49f901cd8af3b89ce5efc2d92d1094c8862b2 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/environment.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/errors.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb162f799e2a0d6aeeae7d20ccb67097b8dc4905 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/errors.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/exceptions.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e874cf402bf263b836e1c7031fd529469fe3b3e Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/exceptions.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/file_name.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/file_name.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd6221b4ef55bfcad6e0619854642e9bc839ba5c Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/file_name.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/helpers.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b01b5950128756315d04c0fb63a0a726fccfee25 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/helpers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/interpreter.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/interpreter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab72eb2bbf2ba86de5fe790a4b525422938c2a64 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/interpreter.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/keywords.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/keywords.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bd9c5581feb4f3bef14889f78018fdc5e69f84e Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/keywords.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/project.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/project.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c0131e7e66b2bcdb1109261e34d4dae7355d7f5 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/project.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/replstartup.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/replstartup.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24d884745194e1a98e25025105dc317736bec512 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/replstartup.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/__pycache__/strings.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/__pycache__/strings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d255a77fb8e6f3a71362c6e86b8b2a1be854184 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/__pycache__/strings.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/classes.py b/lib/python3.12/site-packages/jedi/api/classes.py new file mode 100644 index 0000000000000000000000000000000000000000..7054788e3fe19f3892513d1430a2fb79808d9eb8 --- /dev/null +++ b/lib/python3.12/site-packages/jedi/api/classes.py @@ -0,0 +1,895 @@ +""" +There are a couple of classes documented in here: + +- :class:`.BaseName` as an abstact base class for almost everything. +- :class:`.Name` used in a lot of places +- :class:`.Completion` for completions +- :class:`.BaseSignature` as a base class for signatures +- :class:`.Signature` for :meth:`.Script.get_signatures` only +- :class:`.ParamName` used for parameters of signatures +- :class:`.Refactoring` for refactorings +- :class:`.SyntaxError` for :meth:`.Script.get_syntax_errors` only + +These classes are the much biggest part of the API, because they contain +the interesting information about all operations. +""" +import re +from pathlib import Path +from typing import Optional + +from parso.tree import search_ancestor + +from jedi import settings +from jedi import debug +from jedi.inference.utils import unite +from jedi.cache import memoize_method +from jedi.inference.compiled.mixed import MixedName +from jedi.inference.names import ImportName, SubModuleName +from jedi.inference.gradual.stub_value import StubModuleValue +from jedi.inference.gradual.conversion import convert_names, convert_values +from jedi.inference.base_value import ValueSet, HasNoContext +from jedi.api.keywords import KeywordName +from jedi.api import completion_cache +from jedi.api.helpers import filter_follow_imports + + +def _sort_names_by_start_pos(names): + return sorted(names, key=lambda s: s.start_pos or (0, 0)) + + +def defined_names(inference_state, value): + """ + List sub-definitions (e.g., methods in class). + + :type scope: Scope + :rtype: list of Name + """ + try: + context = value.as_context() + except HasNoContext: + return [] + filter = next(context.get_filters()) + names = [name for name in filter.values()] + return [Name(inference_state, n) for n in _sort_names_by_start_pos(names)] + + +def _values_to_definitions(values): + return [Name(c.inference_state, c.name) for c in values] + + +class BaseName: + """ + The base class for all definitions, completions and signatures. + """ + _mapping = { + 'posixpath': 'os.path', + 'riscospath': 'os.path', + 'ntpath': 'os.path', + 'os2emxpath': 'os.path', + 'macpath': 'os.path', + 'genericpath': 'os.path', + 'posix': 'os', + '_io': 'io', + '_functools': 'functools', + '_collections': 'collections', + '_socket': 'socket', + '_sqlite3': 'sqlite3', + } + + _tuple_mapping = dict((tuple(k.split('.')), v) for (k, v) in { + 'argparse._ActionsContainer': 'argparse.ArgumentParser', + }.items()) + + def __init__(self, inference_state, name): + self._inference_state = inference_state + self._name = name + """ + An instance of :class:`parso.python.tree.Name` subclass. + """ + self.is_keyword = isinstance(self._name, KeywordName) + + @memoize_method + def _get_module_context(self): + # This can take a while to complete, because in the worst case of + # imports (consider `import a` completions), we need to load all + # modules starting with a first. + return self._name.get_root_context() + + @property + def module_path(self) -> Optional[Path]: + """ + Shows the file path of a module. e.g. ``/usr/lib/python3.9/os.py`` + """ + module = self._get_module_context() + if module.is_stub() or not module.is_compiled(): + # Compiled modules should not return a module path even if they + # have one. + path: Optional[Path] = self._get_module_context().py__file__() + return path + + return None + + @property + def name(self): + """ + Name of variable/function/class/module. + + For example, for ``x = None`` it returns ``'x'``. + + :rtype: str or None + """ + return self._name.get_public_name() + + @property + def type(self): + """ + The type of the definition. + + Here is an example of the value of this attribute. Let's consider + the following source. As what is in ``variable`` is unambiguous + to Jedi, :meth:`jedi.Script.infer` should return a list of + definition for ``sys``, ``f``, ``C`` and ``x``. + + >>> from jedi import Script + >>> source = ''' + ... import keyword + ... + ... class C: + ... pass + ... + ... class D: + ... pass + ... + ... x = D() + ... + ... def f(): + ... pass + ... + ... for variable in [keyword, f, C, x]: + ... variable''' + + >>> script = Script(source) + >>> defs = script.infer() + + Before showing what is in ``defs``, let's sort it by :attr:`line` + so that it is easy to relate the result to the source code. + + >>> defs = sorted(defs, key=lambda d: d.line) + >>> print(defs) # doctest: +NORMALIZE_WHITESPACE + [, + , + , + ] + + Finally, here is what you can get from :attr:`type`: + + >>> defs = [d.type for d in defs] + >>> defs[0] + 'module' + >>> defs[1] + 'class' + >>> defs[2] + 'instance' + >>> defs[3] + 'function' + + Valid values for type are ``module``, ``class``, ``instance``, ``function``, + ``param``, ``path``, ``keyword``, ``property`` and ``statement``. + + """ + tree_name = self._name.tree_name + resolve = False + if tree_name is not None: + # TODO move this to their respective names. + definition = tree_name.get_definition() + if definition is not None and definition.type == 'import_from' and \ + tree_name.is_definition(): + resolve = True + + if isinstance(self._name, SubModuleName) or resolve: + for value in self._name.infer(): + return value.api_type + return self._name.api_type + + @property + def module_name(self): + """ + The module name, a bit similar to what ``__name__`` is in a random + Python module. + + >>> from jedi import Script + >>> source = 'import json' + >>> script = Script(source, path='example.py') + >>> d = script.infer()[0] + >>> print(d.module_name) # doctest: +ELLIPSIS + json + """ + return self._get_module_context().py__name__() + + def in_builtin_module(self): + """ + Returns True, if this is a builtin module. + """ + value = self._get_module_context().get_value() + if isinstance(value, StubModuleValue): + return any(v.is_compiled() for v in value.non_stub_value_set) + return value.is_compiled() + + @property + def line(self): + """The line where the definition occurs (starting with 1).""" + start_pos = self._name.start_pos + if start_pos is None: + return None + return start_pos[0] + + @property + def column(self): + """The column where the definition occurs (starting with 0).""" + start_pos = self._name.start_pos + if start_pos is None: + return None + return start_pos[1] + + def get_definition_start_position(self): + """ + The (row, column) of the start of the definition range. Rows start with + 1, columns start with 0. + + :rtype: Optional[Tuple[int, int]] + """ + if self._name.tree_name is None: + return None + definition = self._name.tree_name.get_definition() + if definition is None: + return self._name.start_pos + return definition.start_pos + + def get_definition_end_position(self): + """ + The (row, column) of the end of the definition range. Rows start with + 1, columns start with 0. + + :rtype: Optional[Tuple[int, int]] + """ + if self._name.tree_name is None: + return None + definition = self._name.tree_name.get_definition() + if definition is None: + return self._name.tree_name.end_pos + if self.type in ("function", "class"): + last_leaf = definition.get_last_leaf() + if last_leaf.type == "newline": + return last_leaf.get_previous_leaf().end_pos + return last_leaf.end_pos + return definition.end_pos + + def docstring(self, raw=False, fast=True): + r""" + Return a document string for this completion object. + + Example: + + >>> from jedi import Script + >>> source = '''\ + ... def f(a, b=1): + ... "Document for function f." + ... ''' + >>> script = Script(source, path='example.py') + >>> doc = script.infer(1, len('def f'))[0].docstring() + >>> print(doc) + f(a, b=1) + + Document for function f. + + Notice that useful extra information is added to the actual + docstring, e.g. function signatures are prepended to their docstrings. + If you need the actual docstring, use ``raw=True`` instead. + + >>> print(script.infer(1, len('def f'))[0].docstring(raw=True)) + Document for function f. + + :param fast: Don't follow imports that are only one level deep like + ``import foo``, but follow ``from foo import bar``. This makes + sense for speed reasons. Completing `import a` is slow if you use + the ``foo.docstring(fast=False)`` on every object, because it + parses all libraries starting with ``a``. + """ + if isinstance(self._name, ImportName) and fast: + return '' + doc = self._get_docstring() + if raw: + return doc + + signature_text = self._get_docstring_signature() + if signature_text and doc: + return signature_text + '\n\n' + doc + else: + return signature_text + doc + + def _get_docstring(self): + return self._name.py__doc__() + + def _get_docstring_signature(self): + return '\n'.join( + signature.to_string() + for signature in self._get_signatures(for_docstring=True) + ) + + @property + def description(self): + """ + A description of the :class:`.Name` object, which is heavily used + in testing. e.g. for ``isinstance`` it returns ``def isinstance``. + + Example: + + >>> from jedi import Script + >>> source = ''' + ... def f(): + ... pass + ... + ... class C: + ... pass + ... + ... variable = f if random.choice([0,1]) else C''' + >>> script = Script(source) # line is maximum by default + >>> defs = script.infer(column=3) + >>> defs = sorted(defs, key=lambda d: d.line) + >>> print(defs) # doctest: +NORMALIZE_WHITESPACE + [, + ] + >>> str(defs[0].description) + 'def f' + >>> str(defs[1].description) + 'class C' + + """ + typ = self.type + tree_name = self._name.tree_name + if typ == 'param': + return typ + ' ' + self._name.to_string() + if typ in ('function', 'class', 'module', 'instance') or tree_name is None: + if typ == 'function': + # For the description we want a short and a pythonic way. + typ = 'def' + return typ + ' ' + self._name.get_public_name() + + definition = tree_name.get_definition(include_setitem=True) or tree_name + # Remove the prefix, because that's not what we want for get_code + # here. + txt = definition.get_code(include_prefix=False) + # Delete comments: + txt = re.sub(r'#[^\n]+\n', ' ', txt) + # Delete multi spaces/newlines + txt = re.sub(r'\s+', ' ', txt).strip() + return txt + + @property + def full_name(self): + """ + Dot-separated path of this object. + + It is in the form of ``[.[...]][.]``. + It is useful when you want to look up Python manual of the + object at hand. + + Example: + + >>> from jedi import Script + >>> source = ''' + ... import os + ... os.path.join''' + >>> script = Script(source, path='example.py') + >>> print(script.infer(3, len('os.path.join'))[0].full_name) + os.path.join + + Notice that it returns ``'os.path.join'`` instead of (for example) + ``'posixpath.join'``. This is not correct, since the modules name would + be `````. However most users find the latter + more practical. + """ + if not self._name.is_value_name: + return None + + names = self._name.get_qualified_names(include_module_names=True) + if names is None: + return None + + names = list(names) + try: + names[0] = self._mapping[names[0]] + except KeyError: + pass + + return '.'.join(names) + + def is_stub(self): + """ + Returns True if the current name is defined in a stub file. + """ + if not self._name.is_value_name: + return False + + return self._name.get_root_context().is_stub() + + def is_side_effect(self): + """ + Checks if a name is defined as ``self.foo = 3``. In case of self, this + function would return False, for foo it would return True. + """ + tree_name = self._name.tree_name + if tree_name is None: + return False + return tree_name.is_definition() and tree_name.parent.type == 'trailer' + + @debug.increase_indent_cm('goto on name') + def goto(self, *, follow_imports=False, follow_builtin_imports=False, + only_stubs=False, prefer_stubs=False): + + """ + Like :meth:`.Script.goto` (also supports the same params), but does it + for the current name. This is typically useful if you are using + something like :meth:`.Script.get_names()`. + + :param follow_imports: The goto call will follow imports. + :param follow_builtin_imports: If follow_imports is True will try to + look up names in builtins (i.e. compiled or extension modules). + :param only_stubs: Only return stubs for this goto call. + :param prefer_stubs: Prefer stubs to Python objects for this goto call. + :rtype: list of :class:`Name` + """ + if not self._name.is_value_name: + return [] + + names = self._name.goto() + if follow_imports: + names = filter_follow_imports(names, follow_builtin_imports) + names = convert_names( + names, + only_stubs=only_stubs, + prefer_stubs=prefer_stubs, + ) + return [self if n == self._name else Name(self._inference_state, n) + for n in names] + + @debug.increase_indent_cm('infer on name') + def infer(self, *, only_stubs=False, prefer_stubs=False): + """ + Like :meth:`.Script.infer`, it can be useful to understand which type + the current name has. + + Return the actual definitions. I strongly recommend not using it for + your completions, because it might slow down |jedi|. If you want to + read only a few objects (<=20), it might be useful, especially to get + the original docstrings. The basic problem of this function is that it + follows all results. This means with 1000 completions (e.g. numpy), + it's just very, very slow. + + :param only_stubs: Only return stubs for this goto call. + :param prefer_stubs: Prefer stubs to Python objects for this type + inference call. + :rtype: list of :class:`Name` + """ + assert not (only_stubs and prefer_stubs) + + if not self._name.is_value_name: + return [] + + # First we need to make sure that we have stub names (if possible) that + # we can follow. If we don't do that, we can end up with the inferred + # results of Python objects instead of stubs. + names = convert_names([self._name], prefer_stubs=True) + values = convert_values( + ValueSet.from_sets(n.infer() for n in names), + only_stubs=only_stubs, + prefer_stubs=prefer_stubs, + ) + resulting_names = [c.name for c in values] + return [self if n == self._name else Name(self._inference_state, n) + for n in resulting_names] + + def parent(self): + """ + Returns the parent scope of this identifier. + + :rtype: Name + """ + if not self._name.is_value_name: + return None + + if self.type in ('function', 'class', 'param') and self._name.tree_name is not None: + # Since the parent_context doesn't really match what the user + # thinks of that the parent is here, we do these cases separately. + # The reason for this is the following: + # - class: Nested classes parent_context is always the + # parent_context of the most outer one. + # - function: Functions in classes have the module as + # parent_context. + # - param: The parent_context of a param is not its function but + # e.g. the outer class or module. + cls_or_func_node = self._name.tree_name.get_definition() + parent = search_ancestor(cls_or_func_node, 'funcdef', 'classdef', 'file_input') + context = self._get_module_context().create_value(parent).as_context() + else: + context = self._name.parent_context + + if context is None: + return None + while context.name is None: + # Happens for comprehension contexts + context = context.parent_context + + return Name(self._inference_state, context.name) + + def __repr__(self): + return "<%s %sname=%r, description=%r>" % ( + self.__class__.__name__, + 'full_' if self.full_name else '', + self.full_name or self.name, + self.description, + ) + + def get_line_code(self, before=0, after=0): + """ + Returns the line of code where this object was defined. + + :param before: Add n lines before the current line to the output. + :param after: Add n lines after the current line to the output. + + :return str: Returns the line(s) of code or an empty string if it's a + builtin. + """ + if not self._name.is_value_name: + return '' + + lines = self._name.get_root_context().code_lines + if lines is None: + # Probably a builtin module, just ignore in that case. + return '' + + index = self._name.start_pos[0] - 1 + start_index = max(index - before, 0) + return ''.join(lines[start_index:index + after + 1]) + + def _get_signatures(self, for_docstring=False): + if self._name.api_type == 'property': + return [] + if for_docstring and self._name.api_type == 'statement' and not self.is_stub(): + # For docstrings we don't resolve signatures if they are simple + # statements and not stubs. This is a speed optimization. + return [] + + if isinstance(self._name, MixedName): + # While this would eventually happen anyway, it's basically just a + # shortcut to not infer anything tree related, because it's really + # not necessary. + return self._name.infer_compiled_value().get_signatures() + + names = convert_names([self._name], prefer_stubs=True) + return [sig for name in names for sig in name.infer().get_signatures()] + + def get_signatures(self): + """ + Returns all potential signatures for a function or a class. Multiple + signatures are typical if you use Python stubs with ``@overload``. + + :rtype: list of :class:`BaseSignature` + """ + return [ + BaseSignature(self._inference_state, s) + for s in self._get_signatures() + ] + + def execute(self): + """ + Uses type inference to "execute" this identifier and returns the + executed objects. + + :rtype: list of :class:`Name` + """ + return _values_to_definitions(self._name.infer().execute_with_values()) + + def get_type_hint(self): + """ + Returns type hints like ``Iterable[int]`` or ``Union[int, str]``. + + This method might be quite slow, especially for functions. The problem + is finding executions for those functions to return something like + ``Callable[[int, str], str]``. + + :rtype: str + """ + return self._name.infer().get_type_hint() + + +class Completion(BaseName): + """ + ``Completion`` objects are returned from :meth:`.Script.complete`. They + provide additional information about a completion. + """ + def __init__(self, inference_state, name, stack, like_name_length, + is_fuzzy, cached_name=None): + super().__init__(inference_state, name) + + self._like_name_length = like_name_length + self._stack = stack + self._is_fuzzy = is_fuzzy + self._cached_name = cached_name + + # Completion objects with the same Completion name (which means + # duplicate items in the completion) + self._same_name_completions = [] + + def _complete(self, like_name): + append = '' + if settings.add_bracket_after_function \ + and self.type == 'function': + append = '(' + + name = self._name.get_public_name() + if like_name: + name = name[self._like_name_length:] + return name + append + + @property + def complete(self): + """ + Only works with non-fuzzy completions. Returns None if fuzzy + completions are used. + + Return the rest of the word, e.g. completing ``isinstance``:: + + isinstan# <-- Cursor is here + + would return the string 'ce'. It also adds additional stuff, depending + on your ``settings.py``. + + Assuming the following function definition:: + + def foo(param=0): + pass + + completing ``foo(par`` would give a ``Completion`` which ``complete`` + would be ``am=``. + """ + if self._is_fuzzy: + return None + return self._complete(True) + + @property + def name_with_symbols(self): + """ + Similar to :attr:`.name`, but like :attr:`.name` returns also the + symbols, for example assuming the following function definition:: + + def foo(param=0): + pass + + completing ``foo(`` would give a ``Completion`` which + ``name_with_symbols`` would be "param=". + + """ + return self._complete(False) + + def docstring(self, raw=False, fast=True): + """ + Documented under :meth:`BaseName.docstring`. + """ + if self._like_name_length >= 3: + # In this case we can just resolve the like name, because we + # wouldn't load like > 100 Python modules anymore. + fast = False + + return super().docstring(raw=raw, fast=fast) + + def _get_docstring(self): + if self._cached_name is not None: + return completion_cache.get_docstring( + self._cached_name, + self._name.get_public_name(), + lambda: self._get_cache() + ) + return super()._get_docstring() + + def _get_docstring_signature(self): + if self._cached_name is not None: + return completion_cache.get_docstring_signature( + self._cached_name, + self._name.get_public_name(), + lambda: self._get_cache() + ) + return super()._get_docstring_signature() + + def _get_cache(self): + return ( + super().type, + super()._get_docstring_signature(), + super()._get_docstring(), + ) + + @property + def type(self): + """ + Documented under :meth:`BaseName.type`. + """ + # Purely a speed optimization. + if self._cached_name is not None: + return completion_cache.get_type( + self._cached_name, + self._name.get_public_name(), + lambda: self._get_cache() + ) + + return super().type + + def get_completion_prefix_length(self): + """ + Returns the length of the prefix being completed. + For example, completing ``isinstance``:: + + isinstan# <-- Cursor is here + + would return 8, because len('isinstan') == 8. + + Assuming the following function definition:: + + def foo(param=0): + pass + + completing ``foo(par`` would return 3. + """ + return self._like_name_length + + def __repr__(self): + return '<%s: %s>' % (type(self).__name__, self._name.get_public_name()) + + +class Name(BaseName): + """ + *Name* objects are returned from many different APIs including + :meth:`.Script.goto` or :meth:`.Script.infer`. + """ + def __init__(self, inference_state, definition): + super().__init__(inference_state, definition) + + @memoize_method + def defined_names(self): + """ + List sub-definitions (e.g., methods in class). + + :rtype: list of :class:`Name` + """ + defs = self._name.infer() + return sorted( + unite(defined_names(self._inference_state, d) for d in defs), + key=lambda s: s._name.start_pos or (0, 0) + ) + + def is_definition(self): + """ + Returns True, if defined as a name in a statement, function or class. + Returns False, if it's a reference to such a definition. + """ + if self._name.tree_name is None: + return True + else: + return self._name.tree_name.is_definition() + + def __eq__(self, other): + return self._name.start_pos == other._name.start_pos \ + and self.module_path == other.module_path \ + and self.name == other.name \ + and self._inference_state == other._inference_state + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self._name.start_pos, self.module_path, self.name, self._inference_state)) + + +class BaseSignature(Name): + """ + These signatures are returned by :meth:`BaseName.get_signatures` + calls. + """ + def __init__(self, inference_state, signature): + super().__init__(inference_state, signature.name) + self._signature = signature + + @property + def params(self): + """ + Returns definitions for all parameters that a signature defines. + This includes stuff like ``*args`` and ``**kwargs``. + + :rtype: list of :class:`.ParamName` + """ + return [ParamName(self._inference_state, n) + for n in self._signature.get_param_names(resolve_stars=True)] + + def to_string(self): + """ + Returns a text representation of the signature. This could for example + look like ``foo(bar, baz: int, **kwargs)``. + + :rtype: str + """ + return self._signature.to_string() + + +class Signature(BaseSignature): + """ + A full signature object is the return value of + :meth:`.Script.get_signatures`. + """ + def __init__(self, inference_state, signature, call_details): + super().__init__(inference_state, signature) + self._call_details = call_details + self._signature = signature + + @property + def index(self): + """ + Returns the param index of the current cursor position. + Returns None if the index cannot be found in the curent call. + + :rtype: int + """ + return self._call_details.calculate_index( + self._signature.get_param_names(resolve_stars=True) + ) + + @property + def bracket_start(self): + """ + Returns a line/column tuple of the bracket that is responsible for the + last function call. The first line is 1 and the first column 0. + + :rtype: int, int + """ + return self._call_details.bracket_leaf.start_pos + + def __repr__(self): + return '<%s: index=%r %s>' % ( + type(self).__name__, + self.index, + self._signature.to_string(), + ) + + +class ParamName(Name): + def infer_default(self): + """ + Returns default values like the ``1`` of ``def foo(x=1):``. + + :rtype: list of :class:`.Name` + """ + return _values_to_definitions(self._name.infer_default()) + + def infer_annotation(self, **kwargs): + """ + :param execute_annotation: Default True; If False, values are not + executed and classes are returned instead of instances. + :rtype: list of :class:`.Name` + """ + return _values_to_definitions(self._name.infer_annotation(ignore_stars=True, **kwargs)) + + def to_string(self): + """ + Returns a simple representation of a param, like + ``f: Callable[..., Any]``. + + :rtype: str + """ + return self._name.to_string() + + @property + def kind(self): + """ + Returns an enum instance of :mod:`inspect`'s ``Parameter`` enum. + + :rtype: :py:attr:`inspect.Parameter.kind` + """ + return self._name.get_kind() diff --git a/lib/python3.12/site-packages/jedi/api/completion_cache.py b/lib/python3.12/site-packages/jedi/api/completion_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..46e9bead210d817399113bed5db11e4e5e69e6cd --- /dev/null +++ b/lib/python3.12/site-packages/jedi/api/completion_cache.py @@ -0,0 +1,31 @@ +from typing import Dict, Tuple, Callable + +CacheValues = Tuple[str, str, str] +CacheValuesCallback = Callable[[], CacheValues] + + +_cache: Dict[str, Dict[str, CacheValues]] = {} + + +def save_entry(module_name: str, name: str, cache: CacheValues) -> None: + try: + module_cache = _cache[module_name] + except KeyError: + module_cache = _cache[module_name] = {} + module_cache[name] = cache + + +def _create_get_from_cache(number: int) -> Callable[[str, str, CacheValuesCallback], str]: + def _get_from_cache(module_name: str, name: str, get_cache_values: CacheValuesCallback) -> str: + try: + return _cache[module_name][name][number] + except KeyError: + v = get_cache_values() + save_entry(module_name, name, v) + return v[number] + return _get_from_cache + + +get_type = _create_get_from_cache(0) +get_docstring_signature = _create_get_from_cache(1) +get_docstring = _create_get_from_cache(2) diff --git a/lib/python3.12/site-packages/jedi/api/refactoring/__init__.py b/lib/python3.12/site-packages/jedi/api/refactoring/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d453064793c245f252d36bb9cf57644192b43f84 --- /dev/null +++ b/lib/python3.12/site-packages/jedi/api/refactoring/__init__.py @@ -0,0 +1,264 @@ +import difflib +from pathlib import Path +from typing import Dict, Iterable, Tuple + +from parso import split_lines + +from jedi.api.exceptions import RefactoringError +from jedi.inference.value.namespace import ImplicitNSName + +EXPRESSION_PARTS = ( + 'or_test and_test not_test comparison ' + 'expr xor_expr and_expr shift_expr arith_expr term factor power atom_expr' +).split() + + +class ChangedFile: + def __init__(self, inference_state, from_path, to_path, + module_node, node_to_str_map): + self._inference_state = inference_state + self._from_path = from_path + self._to_path = to_path + self._module_node = module_node + self._node_to_str_map = node_to_str_map + + def get_diff(self): + old_lines = split_lines(self._module_node.get_code(), keepends=True) + new_lines = split_lines(self.get_new_code(), keepends=True) + + # Add a newline at the end if it's missing. Otherwise the diff will be + # very weird. A `diff -u file1 file2` would show the string: + # + # \ No newline at end of file + # + # This is not necessary IMO, because Jedi does not really play with + # newlines and the ending newline does not really matter in Python + # files. ~dave + if old_lines[-1] != '': + old_lines[-1] += '\n' + if new_lines[-1] != '': + new_lines[-1] += '\n' + + project_path = self._inference_state.project.path + if self._from_path is None: + from_p = '' + else: + try: + from_p = self._from_path.relative_to(project_path) + except ValueError: # Happens it the path is not on th project_path + from_p = self._from_path + if self._to_path is None: + to_p = '' + else: + try: + to_p = self._to_path.relative_to(project_path) + except ValueError: + to_p = self._to_path + diff = difflib.unified_diff( + old_lines, new_lines, + fromfile=str(from_p), + tofile=str(to_p), + ) + # Apparently there's a space at the end of the diff - for whatever + # reason. + return ''.join(diff).rstrip(' ') + + def get_new_code(self): + return self._inference_state.grammar.refactor(self._module_node, self._node_to_str_map) + + def apply(self): + if self._from_path is None: + raise RefactoringError( + 'Cannot apply a refactoring on a Script with path=None' + ) + + with open(self._from_path, 'w', newline='') as f: + f.write(self.get_new_code()) + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self._from_path) + + +class Refactoring: + def __init__(self, inference_state, file_to_node_changes, renames=()): + self._inference_state = inference_state + self._renames = renames + self._file_to_node_changes = file_to_node_changes + + def get_changed_files(self) -> Dict[Path, ChangedFile]: + def calculate_to_path(p): + if p is None: + return p + p = str(p) + for from_, to in renames: + if p.startswith(str(from_)): + p = str(to) + p[len(str(from_)):] + return Path(p) + + renames = self.get_renames() + return { + path: ChangedFile( + self._inference_state, + from_path=path, + to_path=calculate_to_path(path), + module_node=next(iter(map_)).get_root_node(), + node_to_str_map=map_ + ) + # We need to use `or`, because the path can be None + for path, map_ in sorted( + self._file_to_node_changes.items(), + key=lambda x: x[0] or Path("") + ) + } + + def get_renames(self) -> Iterable[Tuple[Path, Path]]: + """ + Files can be renamed in a refactoring. + """ + return sorted(self._renames) + + def get_diff(self): + text = '' + project_path = self._inference_state.project.path + for from_, to in self.get_renames(): + text += 'rename from %s\nrename to %s\n' \ + % (_try_relative_to(from_, project_path), _try_relative_to(to, project_path)) + + return text + ''.join(f.get_diff() for f in self.get_changed_files().values()) + + def apply(self): + """ + Applies the whole refactoring to the files, which includes renames. + """ + for f in self.get_changed_files().values(): + f.apply() + + for old, new in self.get_renames(): + old.rename(new) + + +def _calculate_rename(path, new_name): + dir_ = path.parent + if path.name in ('__init__.py', '__init__.pyi'): + return dir_, dir_.parent.joinpath(new_name) + return path, dir_.joinpath(new_name + path.suffix) + + +def rename(inference_state, definitions, new_name): + file_renames = set() + file_tree_name_map = {} + + if not definitions: + raise RefactoringError("There is no name under the cursor") + + for d in definitions: + # This private access is ok in a way. It's not public to + # protect Jedi users from seeing it. + tree_name = d._name.tree_name + if d.type == 'module' and tree_name is None and d.module_path is not None: + p = Path(d.module_path) + file_renames.add(_calculate_rename(p, new_name)) + elif isinstance(d._name, ImplicitNSName): + for p in d._name._value.py__path__(): + file_renames.add(_calculate_rename(Path(p), new_name)) + else: + if tree_name is not None: + fmap = file_tree_name_map.setdefault(d.module_path, {}) + fmap[tree_name] = tree_name.prefix + new_name + return Refactoring(inference_state, file_tree_name_map, file_renames) + + +def inline(inference_state, names): + if not names: + raise RefactoringError("There is no name under the cursor") + if any(n.api_type in ('module', 'namespace') for n in names): + raise RefactoringError("Cannot inline imports, modules or namespaces") + if any(n.tree_name is None for n in names): + raise RefactoringError("Cannot inline builtins/extensions") + + definitions = [n for n in names if n.tree_name.is_definition()] + if len(definitions) == 0: + raise RefactoringError("No definition found to inline") + if len(definitions) > 1: + raise RefactoringError("Cannot inline a name with multiple definitions") + if len(names) == 1: + raise RefactoringError("There are no references to this name") + + tree_name = definitions[0].tree_name + + expr_stmt = tree_name.get_definition() + if expr_stmt.type != 'expr_stmt': + type_ = dict( + funcdef='function', + classdef='class', + ).get(expr_stmt.type, expr_stmt.type) + raise RefactoringError("Cannot inline a %s" % type_) + + if len(expr_stmt.get_defined_names(include_setitem=True)) > 1: + raise RefactoringError("Cannot inline a statement with multiple definitions") + first_child = expr_stmt.children[1] + if first_child.type == 'annassign' and len(first_child.children) == 4: + first_child = first_child.children[2] + if first_child != '=': + if first_child.type == 'annassign': + raise RefactoringError( + 'Cannot inline a statement that is defined by an annotation' + ) + else: + raise RefactoringError( + 'Cannot inline a statement with "%s"' + % first_child.get_code(include_prefix=False) + ) + + rhs = expr_stmt.get_rhs() + replace_code = rhs.get_code(include_prefix=False) + + references = [n for n in names if not n.tree_name.is_definition()] + file_to_node_changes = {} + for name in references: + tree_name = name.tree_name + path = name.get_root_context().py__file__() + s = replace_code + if rhs.type == 'testlist_star_expr' \ + or tree_name.parent.type in EXPRESSION_PARTS \ + or tree_name.parent.type == 'trailer' \ + and tree_name.parent.get_next_sibling() is not None: + s = '(' + replace_code + ')' + + of_path = file_to_node_changes.setdefault(path, {}) + + n = tree_name + prefix = n.prefix + par = n.parent + if par.type == 'trailer' and par.children[0] == '.': + prefix = par.parent.children[0].prefix + n = par + for some_node in par.parent.children[:par.parent.children.index(par)]: + of_path[some_node] = '' + of_path[n] = prefix + s + + path = definitions[0].get_root_context().py__file__() + changes = file_to_node_changes.setdefault(path, {}) + changes[expr_stmt] = _remove_indent_of_prefix(expr_stmt.get_first_leaf().prefix) + next_leaf = expr_stmt.get_next_leaf() + + # Most of the time we have to remove the newline at the end of the + # statement, but if there's a comment we might not need to. + if next_leaf.prefix.strip(' \t') == '' \ + and (next_leaf.type == 'newline' or next_leaf == ';'): + changes[next_leaf] = '' + return Refactoring(inference_state, file_to_node_changes) + + +def _remove_indent_of_prefix(prefix): + r""" + Removes the last indentation of a prefix, e.g. " \n \n " becomes " \n \n". + """ + return ''.join(split_lines(prefix, keepends=True)[:-1]) + + +def _try_relative_to(path: Path, base: Path) -> Path: + try: + return path.relative_to(base) + except ValueError: + return path diff --git a/lib/python3.12/site-packages/jedi/api/refactoring/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/refactoring/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..423d9a36161443b3a33ddcced2cf9f244cc25a43 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/refactoring/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/refactoring/__pycache__/extract.cpython-312.pyc b/lib/python3.12/site-packages/jedi/api/refactoring/__pycache__/extract.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab6a6d4889aab6a46ecf67ff4b1af7eca6644651 Binary files /dev/null and b/lib/python3.12/site-packages/jedi/api/refactoring/__pycache__/extract.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/jedi/api/refactoring/extract.py b/lib/python3.12/site-packages/jedi/api/refactoring/extract.py new file mode 100644 index 0000000000000000000000000000000000000000..6e7df7e13b6a0b0f75fe7b31890aae65b6bc43e0 --- /dev/null +++ b/lib/python3.12/site-packages/jedi/api/refactoring/extract.py @@ -0,0 +1,386 @@ +from textwrap import dedent + +from parso import split_lines + +from jedi import debug +from jedi.api.exceptions import RefactoringError +from jedi.api.refactoring import Refactoring, EXPRESSION_PARTS +from jedi.common import indent_block +from jedi.parser_utils import function_is_classmethod, function_is_staticmethod + + +_DEFINITION_SCOPES = ('suite', 'file_input') +_VARIABLE_EXCTRACTABLE = EXPRESSION_PARTS + \ + ('atom testlist_star_expr testlist test lambdef lambdef_nocond ' + 'keyword name number string fstring').split() + + +def extract_variable(inference_state, path, module_node, name, pos, until_pos): + nodes = _find_nodes(module_node, pos, until_pos) + debug.dbg('Extracting nodes: %s', nodes) + + is_expression, message = _is_expression_with_error(nodes) + if not is_expression: + raise RefactoringError(message) + + generated_code = name + ' = ' + _expression_nodes_to_string(nodes) + file_to_node_changes = {path: _replace(nodes, name, generated_code, pos)} + return Refactoring(inference_state, file_to_node_changes) + + +def _is_expression_with_error(nodes): + """ + Returns a tuple (is_expression, error_string). + """ + if any(node.type == 'name' and node.is_definition() for node in nodes): + return False, 'Cannot extract a name that defines something' + + if nodes[0].type not in _VARIABLE_EXCTRACTABLE: + return False, 'Cannot extract a "%s"' % nodes[0].type + return True, '' + + +def _find_nodes(module_node, pos, until_pos): + """ + Looks up a module and tries to find the appropriate amount of nodes that + are in there. + """ + start_node = module_node.get_leaf_for_position(pos, include_prefixes=True) + + if until_pos is None: + if start_node.type == 'operator': + next_leaf = start_node.get_next_leaf() + if next_leaf is not None and next_leaf.start_pos == pos: + start_node = next_leaf + + if _is_not_extractable_syntax(start_node): + start_node = start_node.parent + + if start_node.parent.type == 'trailer': + start_node = start_node.parent.parent + while start_node.parent.type in EXPRESSION_PARTS: + start_node = start_node.parent + + nodes = [start_node] + else: + # Get the next leaf if we are at the end of a leaf + if start_node.end_pos == pos: + next_leaf = start_node.get_next_leaf() + if next_leaf is not None: + start_node = next_leaf + + # Some syntax is not exactable, just use its parent + if _is_not_extractable_syntax(start_node): + start_node = start_node.parent + + # Find the end + end_leaf = module_node.get_leaf_for_position(until_pos, include_prefixes=True) + if end_leaf.start_pos > until_pos: + end_leaf = end_leaf.get_previous_leaf() + if end_leaf is None: + raise RefactoringError('Cannot extract anything from that') + + parent_node = start_node + while parent_node.end_pos < end_leaf.end_pos: + parent_node = parent_node.parent + + nodes = _remove_unwanted_expression_nodes(parent_node, pos, until_pos) + + # If the user marks just a return statement, we return the expression + # instead of the whole statement, because the user obviously wants to + # extract that part. + if len(nodes) == 1 and start_node.type in ('return_stmt', 'yield_expr'): + return [nodes[0].children[1]] + return nodes + + +def _replace(nodes, expression_replacement, extracted, pos, + insert_before_leaf=None, remaining_prefix=None): + # Now try to replace the nodes found with a variable and move the code + # before the current statement. + definition = _get_parent_definition(nodes[0]) + if insert_before_leaf is None: + insert_before_leaf = definition.get_first_leaf() + first_node_leaf = nodes[0].get_first_leaf() + + lines = split_lines(insert_before_leaf.prefix, keepends=True) + if first_node_leaf is insert_before_leaf: + if remaining_prefix is not None: + # The remaining prefix has already been calculated. + lines[:-1] = remaining_prefix + lines[-1:-1] = [indent_block(extracted, lines[-1]) + '\n'] + extracted_prefix = ''.join(lines) + + replacement_dct = {} + if first_node_leaf is insert_before_leaf: + replacement_dct[nodes[0]] = extracted_prefix + expression_replacement + else: + if remaining_prefix is None: + p = first_node_leaf.prefix + else: + p = remaining_prefix + _get_indentation(nodes[0]) + replacement_dct[nodes[0]] = p + expression_replacement + replacement_dct[insert_before_leaf] = extracted_prefix + insert_before_leaf.value + + for node in nodes[1:]: + replacement_dct[node] = '' + return replacement_dct + + +def _expression_nodes_to_string(nodes): + return ''.join(n.get_code(include_prefix=i != 0) for i, n in enumerate(nodes)) + + +def _suite_nodes_to_string(nodes, pos): + n = nodes[0] + prefix, part_of_code = _split_prefix_at(n.get_first_leaf(), pos[0] - 1) + code = part_of_code + n.get_code(include_prefix=False) \ + + ''.join(n.get_code() for n in nodes[1:]) + return prefix, code + + +def _split_prefix_at(leaf, until_line): + """ + Returns a tuple of the leaf's prefix, split at the until_line + position. + """ + # second means the second returned part + second_line_count = leaf.start_pos[0] - until_line + lines = split_lines(leaf.prefix, keepends=True) + return ''.join(lines[:-second_line_count]), ''.join(lines[-second_line_count:]) + + +def _get_indentation(node): + return split_lines(node.get_first_leaf().prefix)[-1] + + +def _get_parent_definition(node): + """ + Returns the statement where a node is defined. + """ + while node is not None: + if node.parent.type in _DEFINITION_SCOPES: + return node + node = node.parent + raise NotImplementedError('We should never even get here') + + +def _remove_unwanted_expression_nodes(parent_node, pos, until_pos): + """ + This function makes it so for `1 * 2 + 3` you can extract `2 + 3`, even + though it is not part of the expression. + """ + typ = parent_node.type + is_suite_part = typ in ('suite', 'file_input') + if typ in EXPRESSION_PARTS or is_suite_part: + nodes = parent_node.children + for i, n in enumerate(nodes): + if n.end_pos > pos: + start_index = i + if n.type == 'operator': + start_index -= 1 + break + for i, n in reversed(list(enumerate(nodes))): + if n.start_pos < until_pos: + end_index = i + if n.type == 'operator': + end_index += 1 + + # Something like `not foo or bar` should not be cut after not + for n2 in nodes[i:]: + if _is_not_extractable_syntax(n2): + end_index += 1 + else: + break + break + nodes = nodes[start_index:end_index + 1] + if not is_suite_part: + nodes[0:1] = _remove_unwanted_expression_nodes(nodes[0], pos, until_pos) + nodes[-1:] = _remove_unwanted_expression_nodes(nodes[-1], pos, until_pos) + return nodes + return [parent_node] + + +def _is_not_extractable_syntax(node): + return node.type == 'operator' \ + or node.type == 'keyword' and node.value not in ('None', 'True', 'False') + + +def extract_function(inference_state, path, module_context, name, pos, until_pos): + nodes = _find_nodes(module_context.tree_node, pos, until_pos) + assert len(nodes) + + is_expression, _ = _is_expression_with_error(nodes) + context = module_context.create_context(nodes[0]) + is_bound_method = context.is_bound_method() + params, return_variables = list(_find_inputs_and_outputs(module_context, context, nodes)) + + # Find variables + # Is a class method / method + if context.is_module(): + insert_before_leaf = None # Leaf will be determined later + else: + node = _get_code_insertion_node(context.tree_node, is_bound_method) + insert_before_leaf = node.get_first_leaf() + if is_expression: + code_block = 'return ' + _expression_nodes_to_string(nodes) + '\n' + remaining_prefix = None + has_ending_return_stmt = False + else: + has_ending_return_stmt = _is_node_ending_return_stmt(nodes[-1]) + if not has_ending_return_stmt: + # Find the actually used variables (of the defined ones). If none are + # used (e.g. if the range covers the whole function), return the last + # defined variable. + return_variables = list(_find_needed_output_variables( + context, + nodes[0].parent, + nodes[-1].end_pos, + return_variables + )) or [return_variables[-1]] if return_variables else [] + + remaining_prefix, code_block = _suite_nodes_to_string(nodes, pos) + after_leaf = nodes[-1].get_next_leaf() + first, second = _split_prefix_at(after_leaf, until_pos[0]) + code_block += first + + code_block = dedent(code_block) + if not has_ending_return_stmt: + output_var_str = ', '.join(return_variables) + code_block += 'return ' + output_var_str + '\n' + + # Check if we have to raise RefactoringError + _check_for_non_extractables(nodes[:-1] if has_ending_return_stmt else nodes) + + decorator = '' + self_param = None + if is_bound_method: + if not function_is_staticmethod(context.tree_node): + function_param_names = context.get_value().get_param_names() + if len(function_param_names): + self_param = function_param_names[0].string_name + params = [p for p in params if p != self_param] + + if function_is_classmethod(context.tree_node): + decorator = '@classmethod\n' + else: + code_block += '\n' + + function_code = '%sdef %s(%s):\n%s' % ( + decorator, + name, + ', '.join(params if self_param is None else [self_param] + params), + indent_block(code_block) + ) + + function_call = '%s(%s)' % ( + ('' if self_param is None else self_param + '.') + name, + ', '.join(params) + ) + if is_expression: + replacement = function_call + else: + if has_ending_return_stmt: + replacement = 'return ' + function_call + '\n' + else: + replacement = output_var_str + ' = ' + function_call + '\n' + + replacement_dct = _replace(nodes, replacement, function_code, pos, + insert_before_leaf, remaining_prefix) + if not is_expression: + replacement_dct[after_leaf] = second + after_leaf.value + file_to_node_changes = {path: replacement_dct} + return Refactoring(inference_state, file_to_node_changes) + + +def _check_for_non_extractables(nodes): + for n in nodes: + try: + children = n.children + except AttributeError: + if n.value == 'return': + raise RefactoringError( + 'Can only extract return statements if they are at the end.') + if n.value == 'yield': + raise RefactoringError('Cannot extract yield statements.') + else: + _check_for_non_extractables(children) + + +def _is_name_input(module_context, names, first, last): + for name in names: + if name.api_type == 'param' or not name.parent_context.is_module(): + if name.get_root_context() is not module_context: + return True + if name.start_pos is None or not (first <= name.start_pos < last): + return True + return False + + +def _find_inputs_and_outputs(module_context, context, nodes): + first = nodes[0].start_pos + last = nodes[-1].end_pos + + inputs = [] + outputs = [] + for name in _find_non_global_names(nodes): + if name.is_definition(): + if name not in outputs: + outputs.append(name.value) + else: + if name.value not in inputs: + name_definitions = context.goto(name, name.start_pos) + if not name_definitions \ + or _is_name_input(module_context, name_definitions, first, last): + inputs.append(name.value) + + # Check if outputs are really needed: + return inputs, outputs + + +def _find_non_global_names(nodes): + for node in nodes: + try: + children = node.children + except AttributeError: + if node.type == 'name': + yield node + else: + # We only want to check foo in foo.bar + if node.type == 'trailer' and node.children[0] == '.': + continue + + yield from _find_non_global_names(children) + + +def _get_code_insertion_node(node, is_bound_method): + if not is_bound_method or function_is_staticmethod(node): + while node.parent.type != 'file_input': + node = node.parent + + while node.parent.type in ('async_funcdef', 'decorated', 'async_stmt'): + node = node.parent + return node + + +def _find_needed_output_variables(context, search_node, at_least_pos, return_variables): + """ + Searches everything after at_least_pos in a node and checks if any of the + return_variables are used in there and returns those. + """ + for node in search_node.children: + if node.start_pos < at_least_pos: + continue + + return_variables = set(return_variables) + for name in _find_non_global_names([node]): + if not name.is_definition() and name.value in return_variables: + return_variables.remove(name.value) + yield name.value + + +def _is_node_ending_return_stmt(node): + t = node.type + if t == 'simple_stmt': + return _is_node_ending_return_stmt(node.children[0]) + return t == 'return_stmt' diff --git a/lib/python3.12/site-packages/jedi/api/replstartup.py b/lib/python3.12/site-packages/jedi/api/replstartup.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f23d19b71082fad3b92cc760b069dce92faa21 --- /dev/null +++ b/lib/python3.12/site-packages/jedi/api/replstartup.py @@ -0,0 +1,29 @@ +""" +To use Jedi completion in Python interpreter, add the following in your shell +setup (e.g., ``.bashrc``). This works only on Linux/Mac, because readline is +not available on Windows. If you still want Jedi autocompletion in your REPL, +just use IPython instead:: + + export PYTHONSTARTUP="$(python -m jedi repl)" + +Then you will be able to use Jedi completer in your Python interpreter:: + + $ python + Python 3.9.2+ (default, Jul 20 2020, 22:15:08) + [GCC 4.6.1] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> import os + >>> os.path.join('a', 'b').split().in # doctest: +SKIP + ..dex ..sert + +""" +import jedi.utils +from jedi import __version__ as __jedi_version__ + +print('REPL completion using Jedi %s' % __jedi_version__) +jedi.utils.setup_readline(fuzzy=False) + +del jedi + +# Note: try not to do many things here, as it will contaminate global +# namespace of the interpreter. diff --git a/lib/python3.12/site-packages/jedi/api/strings.py b/lib/python3.12/site-packages/jedi/api/strings.py new file mode 100644 index 0000000000000000000000000000000000000000..7850142c5e1470a0c6c2a3313f205defbd35fb85 --- /dev/null +++ b/lib/python3.12/site-packages/jedi/api/strings.py @@ -0,0 +1,111 @@ +""" +This module is here for string completions. This means mostly stuff where +strings are returned, like `foo = dict(bar=3); foo["ba` would complete to +`"bar"]`. + +It however does the same for numbers. The difference between string completions +and other completions is mostly that this module doesn't return defined +names in a module, but pretty much an arbitrary string. +""" +import re + +from jedi.inference.names import AbstractArbitraryName +from jedi.inference.helpers import infer_call_of_leaf +from jedi.api.classes import Completion +from jedi.parser_utils import cut_value_at_position + +_sentinel = object() + + +class StringName(AbstractArbitraryName): + api_type = 'string' + is_value_name = False + + +def complete_dict(module_context, code_lines, leaf, position, string, fuzzy): + bracket_leaf = leaf + if bracket_leaf != '[': + bracket_leaf = leaf.get_previous_leaf() + + cut_end_quote = '' + if string: + cut_end_quote = get_quote_ending(string, code_lines, position, invert_result=True) + + if bracket_leaf == '[': + if string is None and leaf is not bracket_leaf: + string = cut_value_at_position(leaf, position) + + context = module_context.create_context(bracket_leaf) + + before_node = before_bracket_leaf = bracket_leaf.get_previous_leaf() + if before_node in (')', ']', '}'): + before_node = before_node.parent + if before_node.type in ('atom', 'trailer', 'name'): + values = infer_call_of_leaf(context, before_bracket_leaf) + return list(_completions_for_dicts( + module_context.inference_state, + values, + '' if string is None else string, + cut_end_quote, + fuzzy=fuzzy, + )) + return [] + + +def _completions_for_dicts(inference_state, dicts, literal_string, cut_end_quote, fuzzy): + for dict_key in sorted(_get_python_keys(dicts), key=lambda x: repr(x)): + dict_key_str = _create_repr_string(literal_string, dict_key) + if dict_key_str.startswith(literal_string): + name = StringName(inference_state, dict_key_str[:-len(cut_end_quote) or None]) + yield Completion( + inference_state, + name, + stack=None, + like_name_length=len(literal_string), + is_fuzzy=fuzzy + ) + + +def _create_repr_string(literal_string, dict_key): + if not isinstance(dict_key, (str, bytes)) or not literal_string: + return repr(dict_key) + + r = repr(dict_key) + prefix, quote = _get_string_prefix_and_quote(literal_string) + if quote is None: + return r + if quote == r[0]: + return prefix + r + return prefix + quote + r[1:-1] + quote + + +def _get_python_keys(dicts): + for dct in dicts: + if dct.array_type == 'dict': + for key in dct.get_key_values(): + dict_key = key.get_safe_value(default=_sentinel) + if dict_key is not _sentinel: + yield dict_key + + +def _get_string_prefix_and_quote(string): + match = re.match(r'(\w*)("""|\'{3}|"|\')', string) + if match is None: + return None, None + return match.group(1), match.group(2) + + +def _matches_quote_at_position(code_lines, quote, position): + string = code_lines[position[0] - 1][position[1]:position[1] + len(quote)] + return string == quote + + +def get_quote_ending(string, code_lines, position, invert_result=False): + _, quote = _get_string_prefix_and_quote(string) + if quote is None: + return '' + + # Add a quote only if it's not already there. + if _matches_quote_at_position(code_lines, quote, position) != invert_result: + return '' + return quote diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/COPYING b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..17f34bc3d8ae0889ae327ae0c16bf78870c41527 --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/COPYING @@ -0,0 +1,28 @@ +Copyright (c) 2006-2008, R Oudkerk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of author nor the names of any contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/INSTALLER b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/LICENSE b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..0f46bc0edd00d1950d98fec0f78e4366691391d5 --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/LICENSE @@ -0,0 +1,38 @@ +Copyright (c) 2008-2016 California Institute of Technology. +Copyright (c) 2016-2024 The Uncertainty Quantification Foundation. +All rights reserved. + +This software forks the python package "multiprocessing". Licence and +copyright information for multiprocessing can be found in "COPYING". + +This software is available subject to the conditions and terms laid +out below. By downloading and using this software you are agreeing +to the following conditions. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + - Neither the names of the copyright holders nor the names of any of + the contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/METADATA b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..1e8d30dd497e6ac867b1f7f11f79ac2704525be0 --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/METADATA @@ -0,0 +1,203 @@ +Metadata-Version: 2.1 +Name: multiprocess +Version: 0.70.16 +Summary: better multiprocessing and multithreading in Python +Home-page: https://github.com/uqfoundation/multiprocess +Download-URL: https://pypi.org/project/multiprocess/#files +Author: Mike McKerns +Author-email: mmckerns@uqfoundation.org +Maintainer: Mike McKerns +Maintainer-email: mmckerns@uqfoundation.org +License: BSD-3-Clause +Project-URL: Documentation, http://multiprocess.rtfd.io +Project-URL: Source Code, https://github.com/uqfoundation/multiprocess +Project-URL: Bug Tracker, https://github.com/uqfoundation/multiprocess/issues +Platform: Linux +Platform: Windows +Platform: Mac +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Software Development +Requires-Python: >=3.8 +License-File: LICENSE +License-File: COPYING +Requires-Dist: dill (>=0.3.8) + +----------------------------------------------------------------- +multiprocess: better multiprocessing and multithreading in Python +----------------------------------------------------------------- + +About Multiprocess +================== + +``multiprocess`` is a fork of ``multiprocessing``. ``multiprocess`` extends ``multiprocessing`` to provide enhanced serialization, using `dill`. ``multiprocess`` leverages ``multiprocessing`` to support the spawning of processes using the API of the Python standard library's ``threading`` module. ``multiprocessing`` has been distributed as part of the standard library since Python 2.6. + +``multiprocess`` is part of ``pathos``, a Python framework for heterogeneous computing. +``multiprocess`` is in active development, so any user feedback, bug reports, comments, +or suggestions are highly appreciated. A list of issues is located at https://github.com/uqfoundation/multiprocess/issues, with a legacy list maintained at https://uqfoundation.github.io/project/pathos/query. + + +Major Features +============== + +``multiprocess`` enables: + + - objects to be transferred between processes using pipes or multi-producer/multi-consumer queues + - objects to be shared between processes using a server process or (for simple data) shared memory + +``multiprocess`` provides: + + - equivalents of all the synchronization primitives in ``threading`` + - a ``Pool`` class to facilitate submitting tasks to worker processes + - enhanced serialization, using ``dill`` + + +Current Release +=============== + +The latest released version of ``multiprocess`` is available from: + + https://pypi.org/project/multiprocess + +``multiprocess`` is distributed under a 3-clause BSD license, and is a fork of ``multiprocessing``. + + +Development Version +=================== + +You can get the latest development version with all the shiny new features at: + + https://github.com/uqfoundation + +If you have a new contribution, please submit a pull request. + + +Installation +============ + +``multiprocess`` can be installed with ``pip``:: + + $ pip install multiprocess + +For Python 2, a C compiler is required to build the included extension module from source. Python 3 and binary installs do not require a C compiler. + + +Requirements +============ + +``multiprocess`` requires: + + - ``python`` (or ``pypy``), **>=3.8** + - ``setuptools``, **>=42** + - ``dill``, **>=0.3.8** + + +Basic Usage +=========== + +The ``multiprocess.Process`` class follows the API of ``threading.Thread``. +For example :: + + from multiprocess import Process, Queue + + def f(q): + q.put('hello world') + + if __name__ == '__main__': + q = Queue() + p = Process(target=f, args=[q]) + p.start() + print (q.get()) + p.join() + +Synchronization primitives like locks, semaphores and conditions are +available, for example :: + + >>> from multiprocess import Condition + >>> c = Condition() + >>> print (c) + ), 0> + >>> c.acquire() + True + >>> print (c) + ), 0> + +One can also use a manager to create shared objects either in shared +memory or in a server process, for example :: + + >>> from multiprocess import Manager + >>> manager = Manager() + >>> l = manager.list(range(10)) + >>> l.reverse() + >>> print (l) + [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + >>> print (repr(l)) + + +Tasks can be offloaded to a pool of worker processes in various ways, +for example :: + + >>> from multiprocess import Pool + >>> def f(x): return x*x + ... + >>> p = Pool(4) + >>> result = p.map_async(f, range(10)) + >>> print (result.get(timeout=1)) + [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +When ``dill`` is installed, serialization is extended to most objects, +for example :: + + >>> from multiprocess import Pool + >>> p = Pool(4) + >>> print (p.map(lambda x: (lambda y:y**2)(x) + x, xrange(10))) + [0, 2, 6, 12, 20, 30, 42, 56, 72, 90] + + +More Information +================ + +Probably the best way to get started is to look at the documentation at +http://multiprocess.rtfd.io. Also see ``multiprocess.tests`` for scripts that +demonstrate how ``multiprocess`` can be used to leverge multiple processes +to execute Python in parallel. You can run the test suite with +``python -m multiprocess.tests``. As ``multiprocess`` conforms to the +``multiprocessing`` interface, the examples and documentation found at +http://docs.python.org/library/multiprocessing.html also apply to +``multiprocess`` if one will ``import multiprocessing as multiprocess``. +See https://github.com/uqfoundation/multiprocess/tree/master/py3.12/examples +for a set of examples that demonstrate some basic use cases and benchmarking +for running Python code in parallel. Please feel free to submit a ticket on +github, or ask a question on stackoverflow (**@Mike McKerns**). If you would +like to share how you use ``multiprocess`` in your work, please send an email +(to **mmckerns at uqfoundation dot org**). + + +Citation +======== + +If you use ``multiprocess`` to do research that leads to publication, we ask that you +acknowledge use of ``multiprocess`` by citing the following in your publication:: + + M.M. McKerns, L. Strand, T. Sullivan, A. Fang, M.A.G. Aivazis, + "Building a framework for predictive science", Proceedings of + the 10th Python in Science Conference, 2011; + http://arxiv.org/pdf/1202.1056 + + Michael McKerns and Michael Aivazis, + "pathos: a framework for heterogeneous computing", 2010- ; + https://uqfoundation.github.io/project/pathos + +Please see https://uqfoundation.github.io/project/pathos or +http://arxiv.org/pdf/1202.1056 for further information. diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/RECORD b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..0d4dae2a891fa98303a545317807684025a58081 --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/RECORD @@ -0,0 +1,97 @@ +_multiprocess/__init__.py,sha256=zX5_h36TGSL0brHRtBvCL5E59ccW7yjL79i-Y399ODM,321 +_multiprocess/__pycache__/__init__.cpython-312.pyc,, +multiprocess-0.70.16.dist-info/COPYING,sha256=n3_yfLkw0sMgLuB-PS1hRvTeZ20GmjPaMWbJjNuoOpU,1493 +multiprocess-0.70.16.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +multiprocess-0.70.16.dist-info/LICENSE,sha256=6XUJedJKg2dhI98BD3PMtVtZvRFT-oGczkOr5B4tEEA,1930 +multiprocess-0.70.16.dist-info/METADATA,sha256=Sv2eH2CjjyjVYaryTKqHkbJTlxlVA-SbmziCgkBJeQ0,7151 +multiprocess-0.70.16.dist-info/RECORD,, +multiprocess-0.70.16.dist-info/WHEEL,sha256=5fgWN-TjI_Xv7EKUndC5Gdk-CmDdjWHfedmCHAp9Yhg,94 +multiprocess-0.70.16.dist-info/top_level.txt,sha256=qtJc8GNdvi6suNpISX0Myln9AXJBYrNuas1MCqRPPqg,27 +multiprocess/__info__.py,sha256=84TUBn1oJMNpbVvXKs0lKyfLYaZvRr-ZVh1zHM9VeCY,7997 +multiprocess/__init__.py,sha256=XWUBDGorUkDW04h64xe51pUV9N5gzvSDj3tNT2ekifw,1856 +multiprocess/__pycache__/__info__.cpython-312.pyc,, +multiprocess/__pycache__/__init__.cpython-312.pyc,, +multiprocess/__pycache__/connection.cpython-312.pyc,, +multiprocess/__pycache__/context.cpython-312.pyc,, +multiprocess/__pycache__/forkserver.cpython-312.pyc,, +multiprocess/__pycache__/heap.cpython-312.pyc,, +multiprocess/__pycache__/managers.cpython-312.pyc,, +multiprocess/__pycache__/pool.cpython-312.pyc,, +multiprocess/__pycache__/popen_fork.cpython-312.pyc,, +multiprocess/__pycache__/popen_forkserver.cpython-312.pyc,, +multiprocess/__pycache__/popen_spawn_posix.cpython-312.pyc,, +multiprocess/__pycache__/popen_spawn_win32.cpython-312.pyc,, +multiprocess/__pycache__/process.cpython-312.pyc,, +multiprocess/__pycache__/queues.cpython-312.pyc,, +multiprocess/__pycache__/reduction.cpython-312.pyc,, +multiprocess/__pycache__/resource_sharer.cpython-312.pyc,, +multiprocess/__pycache__/resource_tracker.cpython-312.pyc,, +multiprocess/__pycache__/shared_memory.cpython-312.pyc,, +multiprocess/__pycache__/sharedctypes.cpython-312.pyc,, +multiprocess/__pycache__/spawn.cpython-312.pyc,, +multiprocess/__pycache__/synchronize.cpython-312.pyc,, +multiprocess/__pycache__/util.cpython-312.pyc,, +multiprocess/connection.py,sha256=-MnrMNG2rA4O-2U5s0Ct_nXi1Q8x1QFTAShwSELFiJ0,41577 +multiprocess/context.py,sha256=xqunfqOp2vRQqrw-eFq3q4jfwa3GdTrL4Mh1M4AvtgI,11686 +multiprocess/dummy/__init__.py,sha256=kSekDqD_NCy0FDg7XnxZSgW-Ldg1_iRr07sNwDajKpA,3061 +multiprocess/dummy/__pycache__/__init__.cpython-312.pyc,, +multiprocess/dummy/__pycache__/connection.cpython-312.pyc,, +multiprocess/dummy/connection.py,sha256=1j3Rl5_enBM-_kMO6HDmum3kPAoFE4Zs485HV5H-V6s,1598 +multiprocess/forkserver.py,sha256=_WT6-elIjrr7t8RXoZ6u0RGHgLaqoWweAiqwaWDlUCw,12130 +multiprocess/heap.py,sha256=9rt5u5m5rkhJNfDWiCLpYDoWIt0LbElmx52yMqk7phQ,11626 +multiprocess/managers.py,sha256=wzD4jg66U41kwWsqguoDZN0z-JKFbrn0loK4b_fU3GA,47675 +multiprocess/pool.py,sha256=QYzUHqXDQeZsaXmjgHD29VLLvfBTkHMCK20Vxn5T9IA,32760 +multiprocess/popen_fork.py,sha256=Nvq5vVId24UfkOQxXhxZbcXuo8d6YMc409yRXAamTd0,2374 +multiprocess/popen_forkserver.py,sha256=SrEbV8Wv0Uu_UegkaW-cayXRdjTGXr560Yyy90pj-yE,2227 +multiprocess/popen_spawn_posix.py,sha256=l7XSWqR5UWiUSJh35qeSElLuNfUeEYwvH5HzKRnnyqg,2029 +multiprocess/popen_spawn_win32.py,sha256=XA9nNjov3-JjmO5ztPE3Ba9zJbR-W4UunYPjm0Y8C98,4353 +multiprocess/process.py,sha256=WD3nlOajRpvtXuw18J4r7fUKGumP-h7oV21bqhQihEk,12133 +multiprocess/queues.py,sha256=IyJsaDl3X6RJD8yOuYX6iGU9OXwi4Um6BA3tNhQt1OI,12615 +multiprocess/reduction.py,sha256=NQQ6KbDhmuAyaDeWaIarTZQokGPhcFda1poNnPm5uNc,9637 +multiprocess/resource_sharer.py,sha256=nEApLhMQqd8KunfaNKl3n8vdeiCGPxKrSL1Ja0nNAEk,5132 +multiprocess/resource_tracker.py,sha256=YX78ClEGXDk1ieCaKRlJ5K1pNZnsvTyz6bBuWmGqZFw,10449 +multiprocess/shared_memory.py,sha256=UTAecHECIOHElP9Tg6yURCo4pKZiLy65TkASjEXeGus,18458 +multiprocess/sharedctypes.py,sha256=d-9SKRJHRlJJC331IxEoWOUXIeY9zxCbhWejXOmzGw0,6306 +multiprocess/spawn.py,sha256=GPe8Ht5UI4wqLSGcPJvo8rcJmDcEK2A_bVNUEyG6ozI,9641 +multiprocess/synchronize.py,sha256=xMTFX2wwOgbg_bXYgaL49zZAocuraXQ0XG3Uh8Uu7F0,12506 +multiprocess/tests/__init__.py,sha256=AX00HJqMOEPdte08EFBG_5cpDjscm5XZQpN3bbClAsA,209750 +multiprocess/tests/__main__.py,sha256=kePVxic_T6xt2jmqlQFlR4ef0ZAGaD4S-od3pXL-pnQ,888 +multiprocess/tests/__pycache__/__init__.cpython-312.pyc,, +multiprocess/tests/__pycache__/__main__.cpython-312.pyc,, +multiprocess/tests/__pycache__/mp_fork_bomb.cpython-312.pyc,, +multiprocess/tests/__pycache__/mp_preload.cpython-312.pyc,, +multiprocess/tests/__pycache__/test_multiprocessing_main_handling.cpython-312.pyc,, +multiprocess/tests/mp_fork_bomb.py,sha256=6ADOEzh1aXHZ21aOGoBPhKcgB5sj15G9tQVgSc6GrlY,448 +multiprocess/tests/mp_preload.py,sha256=1-WvLFMaPoH-vZbpUaJvvZHFxTpA9tgmct2vblQy99M,365 +multiprocess/tests/test_multiprocessing_fork/__init__.py,sha256=h4YpM8po-3m2kFeoCuV9ZsKDP8UgY-e1Sx3mat9oI9o,829 +multiprocess/tests/test_multiprocessing_fork/__pycache__/__init__.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_fork/__pycache__/test_manager.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_fork/__pycache__/test_misc.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_fork/__pycache__/test_processes.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_fork/__pycache__/test_threads.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_fork/test_manager.py,sha256=A5K0x43Vrfe2jAVqFkBZpyzfuFoYm_Ickby4z31-xPg,194 +multiprocess/tests/test_multiprocessing_fork/test_misc.py,sha256=KhsBZTAEng6Dn-KAww-_eFMnnf14lWCXayQ4cMjx9Mk,193 +multiprocess/tests/test_multiprocessing_fork/test_processes.py,sha256=3bPddzlEnRMkan2cSLfDx77hEydV51Mx-l51ERtwt4Q,196 +multiprocess/tests/test_multiprocessing_fork/test_threads.py,sha256=_qq2fWmU5FuGtsOSoesXRV3DyCn0ptEezKrsnsE6MqI,194 +multiprocess/tests/test_multiprocessing_forkserver/__init__.py,sha256=GnMnSAaJ_y557T6vZsqu7C1Z_H1gOjGbXqklkfNz6Wc,738 +multiprocess/tests/test_multiprocessing_forkserver/__pycache__/__init__.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_forkserver/__pycache__/test_manager.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_forkserver/__pycache__/test_misc.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_forkserver/__pycache__/test_processes.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_forkserver/__pycache__/test_threads.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_forkserver/test_manager.py,sha256=PQTSGlK3U5B2RVaHbW8MJ2pMWeYJj4arKVLm49Fc9TY,200 +multiprocess/tests/test_multiprocessing_forkserver/test_misc.py,sha256=EbDnh3_2MTNI3fFuC2GjM4pUUJ9cxRWXEwZZBV9FyoQ,199 +multiprocess/tests/test_multiprocessing_forkserver/test_processes.py,sha256=ks2hMY1_y0ZiyEGqgAK340b2M6oupXksCo3QFCsgTVk,202 +multiprocess/tests/test_multiprocessing_forkserver/test_threads.py,sha256=H1OyjLOUPkFsnw6QEEp7aSFPLPVndsZX92MAaU_gBFs,200 +multiprocess/tests/test_multiprocessing_main_handling.py,sha256=gFVw9bMOg8pFZp0QGPIPlXDtrY8K5poeb54PrTGT0Ow,11847 +multiprocess/tests/test_multiprocessing_spawn/__init__.py,sha256=pkhcDlpFcKmrN7-ekKF8Zz69x_X24JWqdIdiDypwJiY,639 +multiprocess/tests/test_multiprocessing_spawn/__pycache__/__init__.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_spawn/__pycache__/test_manager.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_spawn/__pycache__/test_misc.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_spawn/__pycache__/test_processes.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_spawn/__pycache__/test_threads.cpython-312.pyc,, +multiprocess/tests/test_multiprocessing_spawn/test_manager.py,sha256=3zMuPfUzqSaRZbNbVpQoU9OU52KaJotd7ps0H-_hdFU,195 +multiprocess/tests/test_multiprocessing_spawn/test_misc.py,sha256=Lb8nqsQL893F8TsvjBEh5s7nV37qAmetcLGk5tuZ3Kc,194 +multiprocess/tests/test_multiprocessing_spawn/test_processes.py,sha256=HoZZlby6gRZONFoV5-wEC50lQFEawvwT_Jh6yZ4SCag,197 +multiprocess/tests/test_multiprocessing_spawn/test_threads.py,sha256=pr9-K0KR8THDzYfGw4PGJ45RcGqREcTNX0OAE14exEw,195 +multiprocess/util.py,sha256=zUGS2s1PyQSS90vLctLRC7QzegW56br_JDkLR7pDAFQ,14060 diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/WHEEL b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..db9549056ba91c67c431591a77afb31c7ce0fa2f --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.38.4) +Root-Is-Purelib: true +Tag: py312-none-any + diff --git a/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/top_level.txt b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..d547cb06bfcaf32e902f6d3c00ec331470ab2f71 --- /dev/null +++ b/lib/python3.12/site-packages/multiprocess-0.70.16.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_multiprocess +multiprocess diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/INSTALLER b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/License.txt b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/License.txt new file mode 100644 index 0000000000000000000000000000000000000000..b491c70e0aef319022ded661e111ddbd45b8a17f --- /dev/null +++ b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/License.txt @@ -0,0 +1,1568 @@ +End User License Agreement +-------------------------- + + +Preface +------- + +The Software License Agreement in Chapter 1 and the Supplement +in Chapter 2 contain license terms and conditions that govern +the use of NVIDIA software. By accepting this agreement, you +agree to comply with all the terms and conditions applicable +to the product(s) included herein. + + +NVIDIA Driver + + +Description + +This package contains the operating system driver and +fundamental system software components for NVIDIA GPUs. + + +NVIDIA CUDA Toolkit + + +Description + +The NVIDIA CUDA Toolkit provides command-line and graphical +tools for building, debugging and optimizing the performance +of applications accelerated by NVIDIA GPUs, runtime and math +libraries, and documentation including programming guides, +user manuals, and API references. + + +Default Install Location of CUDA Toolkit + +Windows platform: + +%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v#.# + +Linux platform: + +/usr/local/cuda-#.# + +Mac platform: + +/Developer/NVIDIA/CUDA-#.# + + +NVIDIA CUDA Samples + + +Description + +This package includes over 100+ CUDA examples that demonstrate +various CUDA programming principles, and efficient CUDA +implementation of algorithms in specific application domains. + + +Default Install Location of CUDA Samples + +Windows platform: + +%ProgramData%\NVIDIA Corporation\CUDA Samples\v#.# + +Linux platform: + +/usr/local/cuda-#.#/samples + +and + +$HOME/NVIDIA_CUDA-#.#_Samples + +Mac platform: + +/Developer/NVIDIA/CUDA-#.#/samples + + +NVIDIA Nsight Visual Studio Edition (Windows only) + + +Description + +NVIDIA Nsight Development Platform, Visual Studio Edition is a +development environment integrated into Microsoft Visual +Studio that provides tools for debugging, profiling, analyzing +and optimizing your GPU computing and graphics applications. + + +Default Install Location of Nsight Visual Studio Edition + +Windows platform: + +%ProgramFiles(x86)%\NVIDIA Corporation\Nsight Visual Studio Edition #.# + + +1. License Agreement for NVIDIA Software Development Kits +--------------------------------------------------------- + + +Release Date: July 26, 2018 +--------------------------- + + +Important NoticeRead before downloading, installing, +copying or using the licensed software: +------------------------------------------------------- + +This license agreement, including exhibits attached +("Agreement”) is a legal agreement between you and NVIDIA +Corporation ("NVIDIA") and governs your use of a NVIDIA +software development kit (“SDK”). + +Each SDK has its own set of software and materials, but here +is a description of the types of items that may be included in +a SDK: source code, header files, APIs, data sets and assets +(examples include images, textures, models, scenes, videos, +native API input/output files), binary software, sample code, +libraries, utility programs, programming code and +documentation. + +This Agreement can be accepted only by an adult of legal age +of majority in the country in which the SDK is used. + +If you are entering into this Agreement on behalf of a company +or other legal entity, you represent that you have the legal +authority to bind the entity to this Agreement, in which case +“you” will mean the entity you represent. + +If you don’t have the required age or authority to accept +this Agreement, or if you don’t accept all the terms and +conditions of this Agreement, do not download, install or use +the SDK. + +You agree to use the SDK only for purposes that are permitted +by (a) this Agreement, and (b) any applicable law, regulation +or generally accepted practices or guidelines in the relevant +jurisdictions. + + +1.1. License + + +1.1.1. License Grant + +Subject to the terms of this Agreement, NVIDIA hereby grants +you a non-exclusive, non-transferable license, without the +right to sublicense (except as expressly provided in this +Agreement) to: + + 1. Install and use the SDK, + + 2. Modify and create derivative works of sample source code + delivered in the SDK, and + + 3. Distribute those portions of the SDK that are identified + in this Agreement as distributable, as incorporated in + object code format into a software application that meets + the distribution requirements indicated in this Agreement. + + +1.1.2. Distribution Requirements + +These are the distribution requirements for you to exercise +the distribution grant: + + 1. Your application must have material additional + functionality, beyond the included portions of the SDK. + + 2. The distributable portions of the SDK shall only be + accessed by your application. + + 3. The following notice shall be included in modifications + and derivative works of sample source code distributed: + “This software contains source code provided by NVIDIA + Corporation.” + + 4. Unless a developer tool is identified in this Agreement + as distributable, it is delivered for your internal use + only. + + 5. The terms under which you distribute your application + must be consistent with the terms of this Agreement, + including (without limitation) terms relating to the + license grant and license restrictions and protection of + NVIDIA’s intellectual property rights. Additionally, you + agree that you will protect the privacy, security and + legal rights of your application users. + + 6. You agree to notify NVIDIA in writing of any known or + suspected distribution or use of the SDK not in compliance + with the requirements of this Agreement, and to enforce + the terms of your agreements with respect to distributed + SDK. + + +1.1.3. Authorized Users + +You may allow employees and contractors of your entity or of +your subsidiary(ies) to access and use the SDK from your +secure network to perform work on your behalf. + +If you are an academic institution you may allow users +enrolled or employed by the academic institution to access and +use the SDK from your secure network. + +You are responsible for the compliance with the terms of this +Agreement by your authorized users. If you become aware that +your authorized users didn’t follow the terms of this +Agreement, you agree to take reasonable steps to resolve the +non-compliance and prevent new occurrences. + + +1.1.4. Pre-Release SDK + +The SDK versions identified as alpha, beta, preview or +otherwise as pre-release, may not be fully functional, may +contain errors or design flaws, and may have reduced or +different security, privacy, accessibility, availability, and +reliability standards relative to commercial versions of +NVIDIA software and materials. Use of a pre-release SDK may +result in unexpected results, loss of data, project delays or +other unpredictable damage or loss. + +You may use a pre-release SDK at your own risk, understanding +that pre-release SDKs are not intended for use in production +or business-critical systems. + +NVIDIA may choose not to make available a commercial version +of any pre-release SDK. NVIDIA may also choose to abandon +development and terminate the availability of a pre-release +SDK at any time without liability. + + +1.1.5. Updates + +NVIDIA may, at its option, make available patches, workarounds +or other updates to this SDK. Unless the updates are provided +with their separate governing terms, they are deemed part of +the SDK licensed to you as provided in this Agreement. You +agree that the form and content of the SDK that NVIDIA +provides may change without prior notice to you. While NVIDIA +generally maintains compatibility between versions, NVIDIA may +in some cases make changes that introduce incompatibilities in +future versions of the SDK. + + +1.1.6. Third Party Licenses + +The SDK may come bundled with, or otherwise include or be +distributed with, third party software licensed by a NVIDIA +supplier and/or open source software provided under an open +source license. Use of third party software is subject to the +third-party license terms, or in the absence of third party +terms, the terms of this Agreement. Copyright to third party +software is held by the copyright holders indicated in the +third-party software or license. + + +1.1.7. Reservation of Rights + +NVIDIA reserves all rights, title, and interest in and to the +SDK, not expressly granted to you under this Agreement. + + +1.2. Limitations + +The following license limitations apply to your use of the +SDK: + + 1. You may not reverse engineer, decompile or disassemble, + or remove copyright or other proprietary notices from any + portion of the SDK or copies of the SDK. + + 2. Except as expressly provided in this Agreement, you may + not copy, sell, rent, sublicense, transfer, distribute, + modify, or create derivative works of any portion of the + SDK. For clarity, you may not distribute or sublicense the + SDK as a stand-alone product. + + 3. Unless you have an agreement with NVIDIA for this + purpose, you may not indicate that an application created + with the SDK is sponsored or endorsed by NVIDIA. + + 4. You may not bypass, disable, or circumvent any + encryption, security, digital rights management or + authentication mechanism in the SDK. + + 5. You may not use the SDK in any manner that would cause it + to become subject to an open source software license. As + examples, licenses that require as a condition of use, + modification, and/or distribution that the SDK be: + + a. Disclosed or distributed in source code form; + + b. Licensed for the purpose of making derivative works; + or + + c. Redistributable at no charge. + + 6. Unless you have an agreement with NVIDIA for this + purpose, you may not use the SDK with any system or + application where the use or failure of the system or + application can reasonably be expected to threaten or + result in personal injury, death, or catastrophic loss. + Examples include use in avionics, navigation, military, + medical, life support or other life critical applications. + NVIDIA does not design, test or manufacture the SDK for + these critical uses and NVIDIA shall not be liable to you + or any third party, in whole or in part, for any claims or + damages arising from such uses. + + 7. You agree to defend, indemnify and hold harmless NVIDIA + and its affiliates, and their respective employees, + contractors, agents, officers and directors, from and + against any and all claims, damages, obligations, losses, + liabilities, costs or debt, fines, restitutions and + expenses (including but not limited to attorney’s fees + and costs incident to establishing the right of + indemnification) arising out of or related to your use of + the SDK outside of the scope of this Agreement, or not in + compliance with its terms. + + +1.3. Ownership + + 1. NVIDIA or its licensors hold all rights, title and + interest in and to the SDK and its modifications and + derivative works, including their respective intellectual + property rights, subject to your rights described in this + section. This SDK may include software and materials from + NVIDIA’s licensors, and these licensors are intended + third party beneficiaries that may enforce this Agreement + with respect to their intellectual property rights. + + 2. You hold all rights, title and interest in and to your + applications and your derivative works of the sample + source code delivered in the SDK, including their + respective intellectual property rights, subject to + NVIDIA’s rights described in this section. + + 3. You may, but don’t have to, provide to NVIDIA + suggestions, feature requests or other feedback regarding + the SDK, including possible enhancements or modifications + to the SDK. For any feedback that you voluntarily provide, + you hereby grant NVIDIA and its affiliates a perpetual, + non-exclusive, worldwide, irrevocable license to use, + reproduce, modify, license, sublicense (through multiple + tiers of sublicensees), and distribute (through multiple + tiers of distributors) it without the payment of any + royalties or fees to you. NVIDIA will use feedback at its + choice. NVIDIA is constantly looking for ways to improve + its products, so you may send feedback to NVIDIA through + the developer portal at https://developer.nvidia.com. + + +1.4. No Warranties + +THE SDK IS PROVIDED BY NVIDIA “AS IS” AND “WITH ALL +FAULTS.” TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND +ITS AFFILIATES EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND +OR NATURE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, +BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THE +ABSENCE OF ANY DEFECTS THEREIN, WHETHER LATENT OR PATENT. NO +WARRANTY IS MADE ON THE BASIS OF TRADE USAGE, COURSE OF +DEALING OR COURSE OF TRADE. + + +1.5. Limitation of Liability + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS +AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, +PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, LOSS +OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF +PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION +WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SDK, +WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH +OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), +PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF +LIABILITY. IN NO EVENT WILL NVIDIA’S AND ITS AFFILIATES +TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS +AGREEMENT EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE +NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS +LIMIT. + +These exclusions and limitations of liability shall apply +regardless if NVIDIA or its affiliates have been advised of +the possibility of such damages, and regardless of whether a +remedy fails its essential purpose. These exclusions and +limitations of liability form an essential basis of the +bargain between the parties, and, absent any of these +exclusions or limitations of liability, the provisions of this +Agreement, including, without limitation, the economic terms, +would be substantially different. + + +1.6. Termination + + 1. This Agreement will continue to apply until terminated by + either you or NVIDIA as described below. + + 2. If you want to terminate this Agreement, you may do so by + stopping to use the SDK. + + 3. NVIDIA may, at any time, terminate this Agreement if: + + a. (i) you fail to comply with any term of this + Agreement and the non-compliance is not fixed within + thirty (30) days following notice from NVIDIA (or + immediately if you violate NVIDIA’s intellectual + property rights); + + b. (ii) you commence or participate in any legal + proceeding against NVIDIA with respect to the SDK; or + + c. (iii) NVIDIA decides to no longer provide the SDK in + a country or, in NVIDIA’s sole discretion, the + continued use of it is no longer commercially viable. + + 4. Upon any termination of this Agreement, you agree to + promptly discontinue use of the SDK and destroy all copies + in your possession or control. Your prior distributions in + accordance with this Agreement are not affected by the + termination of this Agreement. Upon written request, you + will certify in writing that you have complied with your + commitments under this section. Upon any termination of + this Agreement all provisions survive except for the + license grant provisions. + + +1.7. General + +If you wish to assign this Agreement or your rights and +obligations, including by merger, consolidation, dissolution +or operation of law, contact NVIDIA to ask for permission. Any +attempted assignment not approved by NVIDIA in writing shall +be void and of no effect. NVIDIA may assign, delegate or +transfer this Agreement and its rights and obligations, and if +to a non-affiliate you will be notified. + +You agree to cooperate with NVIDIA and provide reasonably +requested information to verify your compliance with this +Agreement. + +This Agreement will be governed in all respects by the laws of +the United States and of the State of Delaware as those laws +are applied to contracts entered into and performed entirely +within Delaware by Delaware residents, without regard to the +conflicts of laws principles. The United Nations Convention on +Contracts for the International Sale of Goods is specifically +disclaimed. You agree to all terms of this Agreement in the +English language. + +The state or federal courts residing in Santa Clara County, +California shall have exclusive jurisdiction over any dispute +or claim arising out of this Agreement. Notwithstanding this, +you agree that NVIDIA shall still be allowed to apply for +injunctive remedies or an equivalent type of urgent legal +relief in any jurisdiction. + +If any court of competent jurisdiction determines that any +provision of this Agreement is illegal, invalid or +unenforceable, such provision will be construed as limited to +the extent necessary to be consistent with and fully +enforceable under the law and the remaining provisions will +remain in full force and effect. Unless otherwise specified, +remedies are cumulative. + +Each party acknowledges and agrees that the other is an +independent contractor in the performance of this Agreement. + +The SDK has been developed entirely at private expense and is +“commercial items” consisting of “commercial computer +software” and “commercial computer software +documentation” provided with RESTRICTED RIGHTS. Use, +duplication or disclosure by the U.S. Government or a U.S. +Government subcontractor is subject to the restrictions in +this Agreement pursuant to DFARS 227.7202-3(a) or as set forth +in subparagraphs (c)(1) and (2) of the Commercial Computer +Software - Restricted Rights clause at FAR 52.227-19, as +applicable. Contractor/manufacturer is NVIDIA, 2788 San Tomas +Expressway, Santa Clara, CA 95051. + +The SDK is subject to United States export laws and +regulations. You agree that you will not ship, transfer or +export the SDK into any country, or use the SDK in any manner, +prohibited by the United States Bureau of Industry and +Security or economic sanctions regulations administered by the +U.S. Department of Treasury’s Office of Foreign Assets +Control (OFAC), or any applicable export laws, restrictions or +regulations. These laws include restrictions on destinations, +end users and end use. By accepting this Agreement, you +confirm that you are not a resident or citizen of any country +currently embargoed by the U.S. and that you are not otherwise +prohibited from receiving the SDK. + +Any notice delivered by NVIDIA to you under this Agreement +will be delivered via mail, email or fax. You agree that any +notices that NVIDIA sends you electronically will satisfy any +legal communication requirements. Please direct your legal +notices or other correspondence to NVIDIA Corporation, 2788 +San Tomas Expressway, Santa Clara, California 95051, United +States of America, Attention: Legal Department. + +This Agreement and any exhibits incorporated into this +Agreement constitute the entire agreement of the parties with +respect to the subject matter of this Agreement and supersede +all prior negotiations or documentation exchanged between the +parties relating to this SDK license. Any additional and/or +conflicting terms on documents issued by you are null, void, +and invalid. Any amendment or waiver under this Agreement +shall be in writing and signed by representatives of both +parties. + + +2. CUDA Toolkit Supplement to Software License Agreement for +NVIDIA Software Development Kits +------------------------------------------------------------ + + +Release date: August 16, 2018 +----------------------------- + +The terms in this supplement govern your use of the NVIDIA +CUDA Toolkit SDK under the terms of your license agreement +(“Agreement”) as modified by this supplement. Capitalized +terms used but not defined below have the meaning assigned to +them in the Agreement. + +This supplement is an exhibit to the Agreement and is +incorporated as an integral part of the Agreement. In the +event of conflict between the terms in this supplement and the +terms in the Agreement, the terms in this supplement govern. + + +2.1. License Scope + +The SDK is licensed for you to develop applications only for +use in systems with NVIDIA GPUs. + + +2.2. Distribution + +The portions of the SDK that are distributable under the +Agreement are listed in Attachment A. + + +2.3. Operating Systems + +Those portions of the SDK designed exclusively for use on the +Linux or FreeBSD operating systems, or other operating systems +derived from the source code to these operating systems, may +be copied and redistributed for use in accordance with this +Agreement, provided that the object code files are not +modified in any way (except for unzipping of compressed +files). + + +2.4. Audio and Video Encoders and Decoders + +You acknowledge and agree that it is your sole responsibility +to obtain any additional third-party licenses required to +make, have made, use, have used, sell, import, and offer for +sale your products or services that include or incorporate any +third-party software and content relating to audio and/or +video encoders and decoders from, including but not limited +to, Microsoft, Thomson, Fraunhofer IIS, Sisvel S.p.A., +MPEG-LA, and Coding Technologies. NVIDIA does not grant to you +under this Agreement any necessary patent or other rights with +respect to any audio and/or video encoders and decoders. + + +2.5. Licensing + +If the distribution terms in this Agreement are not suitable +for your organization, or for any questions regarding this +Agreement, please contact NVIDIA at +nvidia-compute-license-questions@nvidia.com. + + +2.6. Attachment A + +The following portions of the SDK are distributable under the +Agreement: + +Component + +CUDA Runtime + +Windows + +cudart.dll, cudart_static.lib, cudadevrt.lib + +Mac OSX + +libcudart.dylib, libcudart_static.a, libcudadevrt.a + +Linux + +libcudart.so, libcudart_static.a, libcudadevrt.a + +Android + +libcudart.so, libcudart_static.a, libcudadevrt.a + +Component + +CUDA FFT Library + +Windows + +cufft.dll, cufftw.dll, cufft.lib, cufftw.lib + +Mac OSX + +libcufft.dylib, libcufft_static.a, libcufftw.dylib, +libcufftw_static.a + +Linux + +libcufft.so, libcufft_static.a, libcufftw.so, +libcufftw_static.a + +Android + +libcufft.so, libcufft_static.a, libcufftw.so, +libcufftw_static.a + +Component + +CUDA BLAS Library + +Windows + +cublas.dll, cublasLt.dll + +Mac OSX + +libcublas.dylib, libcublasLt.dylib, libcublas_static.a, +libcublasLt_static.a + +Linux + +libcublas.so, libcublasLt.so, libcublas_static.a, +libcublasLt_static.a + +Android + +libcublas.so, libcublasLt.so, libcublas_static.a, +libcublasLt_static.a + +Component + +NVIDIA "Drop-in" BLAS Library + +Windows + +nvblas.dll + +Mac OSX + +libnvblas.dylib + +Linux + +libnvblas.so + +Component + +CUDA Sparse Matrix Library + +Windows + +cusparse.dll, cusparse.lib + +Mac OSX + +libcusparse.dylib, libcusparse_static.a + +Linux + +libcusparse.so, libcusparse_static.a + +Android + +libcusparse.so, libcusparse_static.a + +Component + +CUDA Linear Solver Library + +Windows + +cusolver.dll, cusolver.lib + +Mac OSX + +libcusolver.dylib, libcusolver_static.a + +Linux + +libcusolver.so, libcusolver_static.a + +Android + +libcusolver.so, libcusolver_static.a + +Component + +CUDA Random Number Generation Library + +Windows + +curand.dll, curand.lib + +Mac OSX + +libcurand.dylib, libcurand_static.a + +Linux + +libcurand.so, libcurand_static.a + +Android + +libcurand.so, libcurand_static.a + +Component + +CUDA Accelerated Graph Library + +Component + +NVIDIA Performance Primitives Library + +Windows + +nppc.dll, nppc.lib, nppial.dll, nppial.lib, nppicc.dll, +nppicc.lib, nppicom.dll, nppicom.lib, nppidei.dll, +nppidei.lib, nppif.dll, nppif.lib, nppig.dll, nppig.lib, +nppim.dll, nppim.lib, nppist.dll, nppist.lib, nppisu.dll, +nppisu.lib, nppitc.dll, nppitc.lib, npps.dll, npps.lib + +Mac OSX + +libnppc.dylib, libnppc_static.a, libnppial.dylib, +libnppial_static.a, libnppicc.dylib, libnppicc_static.a, +libnppicom.dylib, libnppicom_static.a, libnppidei.dylib, +libnppidei_static.a, libnppif.dylib, libnppif_static.a, +libnppig.dylib, libnppig_static.a, libnppim.dylib, +libnppisu_static.a, libnppitc.dylib, libnppitc_static.a, +libnpps.dylib, libnpps_static.a + +Linux + +libnppc.so, libnppc_static.a, libnppial.so, +libnppial_static.a, libnppicc.so, libnppicc_static.a, +libnppicom.so, libnppicom_static.a, libnppidei.so, +libnppidei_static.a, libnppif.so, libnppif_static.a +libnppig.so, libnppig_static.a, libnppim.so, +libnppim_static.a, libnppist.so, libnppist_static.a, +libnppisu.so, libnppisu_static.a, libnppitc.so +libnppitc_static.a, libnpps.so, libnpps_static.a + +Android + +libnppc.so, libnppc_static.a, libnppial.so, +libnppial_static.a, libnppicc.so, libnppicc_static.a, +libnppicom.so, libnppicom_static.a, libnppidei.so, +libnppidei_static.a, libnppif.so, libnppif_static.a +libnppig.so, libnppig_static.a, libnppim.so, +libnppim_static.a, libnppist.so, libnppist_static.a, +libnppisu.so, libnppisu_static.a, libnppitc.so +libnppitc_static.a, libnpps.so, libnpps_static.a + +Component + +NVIDIA JPEG Library + +Linux + +libnvjpeg.so, libnvjpeg_static.a + +Component + +Internal common library required for statically linking to +cuBLAS, cuSPARSE, cuFFT, cuRAND, nvJPEG and NPP + +Mac OSX + +libculibos.a + +Linux + +libculibos.a + +Component + +NVIDIA Runtime Compilation Library and Header + +All + +nvrtc.h + +Windows + +nvrtc.dll, nvrtc-builtins.dll + +Mac OSX + +libnvrtc.dylib, libnvrtc-builtins.dylib + +Linux + +libnvrtc.so, libnvrtc-builtins.so + +Component + +NVIDIA Optimizing Compiler Library + +Windows + +nvvm.dll + +Mac OSX + +libnvvm.dylib + +Linux + +libnvvm.so + +Component + +NVIDIA Common Device Math Functions Library + +Windows + +libdevice.10.bc + +Mac OSX + +libdevice.10.bc + +Linux + +libdevice.10.bc + +Component + +CUDA Occupancy Calculation Header Library + +All + +cuda_occupancy.h + +Component + +CUDA Half Precision Headers + +All + +cuda_fp16.h, cuda_fp16.hpp + +Component + +CUDA Profiling Tools Interface (CUPTI) Library + +Windows + +cupti.dll + +Mac OSX + +libcupti.dylib + +Linux + +libcupti.so + +Component + +NVIDIA Tools Extension Library + +Windows + +nvToolsExt.dll, nvToolsExt.lib + +Mac OSX + +libnvToolsExt.dylib + +Linux + +libnvToolsExt.so + +Component + +NVIDIA CUDA Driver Libraries + +Linux + +libcuda.so, libnvidia-fatbinaryloader.so, +libnvidia-ptxjitcompiler.so + +The NVIDIA CUDA Driver Libraries are only distributable in +applications that meet this criteria: + + 1. The application was developed starting from a NVIDIA CUDA + container obtained from Docker Hub or the NVIDIA GPU + Cloud, and + + 2. The resulting application is packaged as a Docker + container and distributed to users on Docker Hub or the + NVIDIA GPU Cloud only. + + +2.7. Attachment B + + +Additional Licensing Obligations + +The following third party components included in the SOFTWARE +are licensed to Licensee pursuant to the following terms and +conditions: + + 1. Licensee's use of the GDB third party component is + subject to the terms and conditions of GNU GPL v3: + + This product includes copyrighted third-party software licensed + under the terms of the GNU General Public License v3 ("GPL v3"). + All third-party software packages are copyright by their respective + authors. GPL v3 terms and conditions are hereby incorporated into + the Agreement by this reference: http://www.gnu.org/licenses/gpl.txt + + Consistent with these licensing requirements, the software + listed below is provided under the terms of the specified + open source software licenses. To obtain source code for + software provided under licenses that require + redistribution of source code, including the GNU General + Public License (GPL) and GNU Lesser General Public License + (LGPL), contact oss-requests@nvidia.com. This offer is + valid for a period of three (3) years from the date of the + distribution of this product by NVIDIA CORPORATION. + + Component License + CUDA-GDB GPL v3 + + 2. Licensee represents and warrants that any and all third + party licensing and/or royalty payment obligations in + connection with Licensee's use of the H.264 video codecs + are solely the responsibility of Licensee. + + 3. Licensee's use of the Thrust library is subject to the + terms and conditions of the Apache License Version 2.0. + All third-party software packages are copyright by their + respective authors. Apache License Version 2.0 terms and + conditions are hereby incorporated into the Agreement by + this reference. + http://www.apache.org/licenses/LICENSE-2.0.html + + In addition, Licensee acknowledges the following notice: + Thrust includes source code from the Boost Iterator, + Tuple, System, and Random Number libraries. + + Boost Software License - Version 1.0 - August 17th, 2003 + . . . . + + Permission is hereby granted, free of charge, to any person or + organization obtaining a copy of the software and accompanying + documentation covered by this license (the "Software") to use, + reproduce, display, distribute, execute, and transmit the Software, + and to prepare derivative works of the Software, and to permit + third-parties to whom the Software is furnished to do so, all + subject to the following: + + The copyright notices in the Software and this entire statement, + including the above license grant, this restriction and the following + disclaimer, must be included in all copies of the Software, in whole + or in part, and all derivative works of the Software, unless such + copies or derivative works are solely in the form of machine-executable + object code generated by a source language processor. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND + NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR + OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + 4. Licensee's use of the LLVM third party component is + subject to the following terms and conditions: + + ====================================================== + LLVM Release License + ====================================================== + University of Illinois/NCSA + Open Source License + + Copyright (c) 2003-2010 University of Illinois at Urbana-Champaign. + All rights reserved. + + Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal with the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at Urbana- + Champaign, nor the names of its contributors may be used to endorse or + promote products derived from this Software without specific prior + written permission. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS WITH THE SOFTWARE. + + 5. Licensee's use (e.g. nvprof) of the PCRE third party + component is subject to the following terms and + conditions: + + ------------ + PCRE LICENCE + ------------ + PCRE is a library of functions to support regular expressions whose syntax + and semantics are as close as possible to those of the Perl 5 language. + Release 8 of PCRE is distributed under the terms of the "BSD" licence, as + specified below. The documentation for PCRE, supplied in the "doc" + directory, is distributed under the same terms as the software itself. The + basic library functions are written in C and are freestanding. Also + included in the distribution is a set of C++ wrapper functions, and a just- + in-time compiler that can be used to optimize pattern matching. These are + both optional features that can be omitted when the library is built. + + THE BASIC LIBRARY FUNCTIONS + --------------------------- + Written by: Philip Hazel + Email local part: ph10 + Email domain: cam.ac.uk + University of Cambridge Computing Service, + Cambridge, England. + Copyright (c) 1997-2012 University of Cambridge + All rights reserved. + + PCRE JUST-IN-TIME COMPILATION SUPPORT + ------------------------------------- + Written by: Zoltan Herczeg + Email local part: hzmester + Emain domain: freemail.hu + Copyright(c) 2010-2012 Zoltan Herczeg + All rights reserved. + + STACK-LESS JUST-IN-TIME COMPILER + -------------------------------- + Written by: Zoltan Herczeg + Email local part: hzmester + Emain domain: freemail.hu + Copyright(c) 2009-2012 Zoltan Herczeg + All rights reserved. + + THE C++ WRAPPER FUNCTIONS + ------------------------- + Contributed by: Google Inc. + Copyright (c) 2007-2012, Google Inc. + All rights reserved. + + THE "BSD" LICENCE + ----------------- + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the University of Cambridge nor the name of Google + Inc. nor the names of their contributors may be used to endorse or + promote products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + 6. Some of the cuBLAS library routines were written by or + derived from code written by Vasily Volkov and are subject + to the Modified Berkeley Software Distribution License as + follows: + + Copyright (c) 2007-2009, Regents of the University of California + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the University of California, Berkeley nor + the names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + 7. Some of the cuBLAS library routines were written by or + derived from code written by Davide Barbieri and are + subject to the Modified Berkeley Software Distribution + License as follows: + + Copyright (c) 2008-2009 Davide Barbieri @ University of Rome Tor Vergata. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * The name of the author may not be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + 8. Some of the cuBLAS library routines were derived from + code developed by the University of Tennessee and are + subject to the Modified Berkeley Software Distribution + License as follows: + + Copyright (c) 2010 The University of Tennessee. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer listed in this license in the documentation and/or + other materials provided with the distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + 9. Some of the cuBLAS library routines were written by or + derived from code written by Jonathan Hogg and are subject + to the Modified Berkeley Software Distribution License as + follows: + + Copyright (c) 2012, The Science and Technology Facilities Council (STFC). + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the STFC nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE STFC BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + 10. Some of the cuBLAS library routines were written by or + derived from code written by Ahmad M. Abdelfattah, David + Keyes, and Hatem Ltaief, and are subject to the Apache + License, Version 2.0, as follows: + + -- (C) Copyright 2013 King Abdullah University of Science and Technology + Authors: + Ahmad Abdelfattah (ahmad.ahmad@kaust.edu.sa) + David Keyes (david.keyes@kaust.edu.sa) + Hatem Ltaief (hatem.ltaief@kaust.edu.sa) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the King Abdullah University of Science and + Technology nor the names of its contributors may be used to endorse + or promote products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE + + 11. Some of the cuSPARSE library routines were written by or + derived from code written by Li-Wen Chang and are subject + to the NCSA Open Source License as follows: + + Copyright (c) 2012, University of Illinois. + + All rights reserved. + + Developed by: IMPACT Group, University of Illinois, http://impact.crhc.illinois.edu + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal with the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimers in the documentation and/or other materials provided + with the distribution. + * Neither the names of IMPACT Group, University of Illinois, nor + the names of its contributors may be used to endorse or promote + products derived from this Software without specific prior + written permission. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. + + 12. Some of the cuRAND library routines were written by or + derived from code written by Mutsuo Saito and Makoto + Matsumoto and are subject to the following license: + + Copyright (c) 2009, 2010 Mutsuo Saito, Makoto Matsumoto and Hiroshima + University. All rights reserved. + + Copyright (c) 2011 Mutsuo Saito, Makoto Matsumoto, Hiroshima + University and University of Tokyo. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the Hiroshima University nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + 13. Some of the cuRAND library routines were derived from + code developed by D. E. Shaw Research and are subject to + the following license: + + Copyright 2010-2011, D. E. Shaw Research. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions, and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions, and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of D. E. Shaw Research nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + 14. Some of the Math library routines were written by or + derived from code developed by Norbert Juffa and are + subject to the following license: + + Copyright (c) 2015-2017, Norbert Juffa + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + 15. Licensee's use of the lz4 third party component is + subject to the following terms and conditions: + + Copyright (C) 2011-2013, Yann Collet. + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + 16. The NPP library uses code from the Boost Math Toolkit, + and is subject to the following license: + + Boost Software License - Version 1.0 - August 17th, 2003 + . . . . + + Permission is hereby granted, free of charge, to any person or + organization obtaining a copy of the software and accompanying + documentation covered by this license (the "Software") to use, + reproduce, display, distribute, execute, and transmit the Software, + and to prepare derivative works of the Software, and to permit + third-parties to whom the Software is furnished to do so, all + subject to the following: + + The copyright notices in the Software and this entire statement, + including the above license grant, this restriction and the following + disclaimer, must be included in all copies of the Software, in whole + or in part, and all derivative works of the Software, unless such + copies or derivative works are solely in the form of machine-executable + object code generated by a source language processor. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND + NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR + OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + 17. Portions of the Nsight Eclipse Edition is subject to the + following license: + + The Eclipse Foundation makes available all content in this plug-in + ("Content"). Unless otherwise indicated below, the Content is provided + to you under the terms and conditions of the Eclipse Public License + Version 1.0 ("EPL"). A copy of the EPL is available at http:// + www.eclipse.org/legal/epl-v10.html. For purposes of the EPL, "Program" + will mean the Content. + + If you did not receive this Content directly from the Eclipse + Foundation, the Content is being redistributed by another party + ("Redistributor") and different terms and conditions may apply to your + use of any object code in the Content. Check the Redistributor's + license that was provided with the Content. If no such license exists, + contact the Redistributor. Unless otherwise indicated below, the terms + and conditions of the EPL still apply to any source code in the + Content and such source code may be obtained at http://www.eclipse.org. + + 18. Some of the cuBLAS library routines uses code from + OpenAI, which is subject to the following license: + + License URL + https://github.com/openai/openai-gemm/blob/master/LICENSE + + License Text + The MIT License + + Copyright (c) 2016 OpenAI (http://openai.com), 2016 Google Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + 19. Licensee's use of the Visual Studio Setup Configuration + Samples is subject to the following license: + + The MIT License (MIT) + Copyright (C) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + 20. Licensee's use of linmath.h header for CPU functions for + GL vector/matrix operations from lunarG is subject to the + Apache License Version 2.0. + + 21. The DX12-CUDA sample uses the d3dx12.h header, which is + subject to the MIT license . + +----------------- diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/METADATA b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..c36b3c91708c206398b57e017ff425b087739ed7 --- /dev/null +++ b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/METADATA @@ -0,0 +1,35 @@ +Metadata-Version: 2.1 +Name: nvidia-nvjitlink-cu12 +Version: 12.4.127 +Summary: Nvidia JIT LTO Library +Home-page: https://developer.nvidia.com/cuda-zone +Author: Nvidia CUDA Installer Team +Author-email: cuda_installer@nvidia.com +License: NVIDIA Proprietary Software +Keywords: cuda,nvidia,runtime,machine learning,deep learning +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Education +Classifier: Intended Audience :: Science/Research +Classifier: License :: Other/Proprietary License +Classifier: Natural Language :: English +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence +Classifier: Topic :: Software Development +Classifier: Topic :: Software Development :: Libraries +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Requires-Python: >=3 +License-File: License.txt + +NVIDIA compiler library for JIT LTO functionality. diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/RECORD b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..4a92496fde1c9064e380e40516cad661851f1e4b --- /dev/null +++ b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/RECORD @@ -0,0 +1,17 @@ +nvidia/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +nvidia/__pycache__/__init__.cpython-312.pyc,, +nvidia/nvjitlink/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +nvidia/nvjitlink/__pycache__/__init__.cpython-312.pyc,, +nvidia/nvjitlink/include/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +nvidia/nvjitlink/include/__pycache__/__init__.cpython-312.pyc,, +nvidia/nvjitlink/include/nvJitLink.h,sha256=6P2KtTBXDDOsbxIi0Nr9Ot8_fh9XvsSAaDhaElJwLQU,16496 +nvidia/nvjitlink/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +nvidia/nvjitlink/lib/__pycache__/__init__.cpython-312.pyc,, +nvidia/nvjitlink/lib/libnvJitLink.so.12,sha256=y9FIi774K41kwt1Rqbau8_awvDp6gKmCG-SNHAf3AOc,53594512 +nvidia_nvjitlink_cu12-12.4.127.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +nvidia_nvjitlink_cu12-12.4.127.dist-info/License.txt,sha256=rW9YU_ugyg0VnQ9Y1JrkmDDC-Mk_epJki5zpCttMbM0,59262 +nvidia_nvjitlink_cu12-12.4.127.dist-info/METADATA,sha256=Tkm9zx69iT4wHTNBYxiPi30271awlBpcUiAg1itXbn0,1518 +nvidia_nvjitlink_cu12-12.4.127.dist-info/RECORD,, +nvidia_nvjitlink_cu12-12.4.127.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +nvidia_nvjitlink_cu12-12.4.127.dist-info/WHEEL,sha256=XDTs3wIbcE-BcRO08VJlZpA6z9OaC1mOKPCGGGwuM2g,109 +nvidia_nvjitlink_cu12-12.4.127.dist-info/top_level.txt,sha256=fTkAtiFuL16nUrB9ytDDtpytz2t0B4NvYTnRzwAhO14,7 diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/REQUESTED b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/WHEEL b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..e6c30e957cfb045017a9fef3430bb8ee87c4a074 --- /dev/null +++ b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-manylinux2014_x86_64 + diff --git a/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/top_level.txt b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..862f7abf232cdfbb928609856247292e81c9decb --- /dev/null +++ b/lib/python3.12/site-packages/nvidia_nvjitlink_cu12-12.4.127.dist-info/top_level.txt @@ -0,0 +1 @@ +nvidia diff --git a/lib/python3.12/site-packages/openai-1.75.0.dist-info/METADATA b/lib/python3.12/site-packages/openai-1.75.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..d708953ec041d361b57e997c2877fbe9d090b0bc --- /dev/null +++ b/lib/python3.12/site-packages/openai-1.75.0.dist-info/METADATA @@ -0,0 +1,782 @@ +Metadata-Version: 2.3 +Name: openai +Version: 1.75.0 +Summary: The official Python library for the openai API +Project-URL: Homepage, https://github.com/openai/openai-python +Project-URL: Repository, https://github.com/openai/openai-python +Author-email: OpenAI +License: Apache-2.0 +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: MacOS +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: OS Independent +Classifier: Operating System :: POSIX +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.8 +Requires-Dist: anyio<5,>=3.5.0 +Requires-Dist: distro<2,>=1.7.0 +Requires-Dist: httpx<1,>=0.23.0 +Requires-Dist: jiter<1,>=0.4.0 +Requires-Dist: pydantic<3,>=1.9.0 +Requires-Dist: sniffio +Requires-Dist: tqdm>4 +Requires-Dist: typing-extensions<5,>=4.11 +Provides-Extra: datalib +Requires-Dist: numpy>=1; extra == 'datalib' +Requires-Dist: pandas-stubs>=1.1.0.11; extra == 'datalib' +Requires-Dist: pandas>=1.2.3; extra == 'datalib' +Provides-Extra: realtime +Requires-Dist: websockets<16,>=13; extra == 'realtime' +Provides-Extra: voice-helpers +Requires-Dist: numpy>=2.0.2; extra == 'voice-helpers' +Requires-Dist: sounddevice>=0.5.1; extra == 'voice-helpers' +Description-Content-Type: text/markdown + +# OpenAI Python API library + +[![PyPI version](https://img.shields.io/pypi/v/openai.svg)](https://pypi.org/project/openai/) + +The OpenAI Python library provides convenient access to the OpenAI REST API from any Python 3.8+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated from our [OpenAPI specification](https://github.com/openai/openai-openapi) with [Stainless](https://stainlessapi.com/). + +## Documentation + +The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs/api-reference). The full API of this library can be found in [api.md](https://github.com/openai/openai-python/tree/main/api.md). + +## Installation + +```sh +# install from PyPI +pip install openai +``` + +## Usage + +The full API of this library can be found in [api.md](https://github.com/openai/openai-python/tree/main/api.md). + +The primary API for interacting with OpenAI models is the [Responses API](https://platform.openai.com/docs/api-reference/responses). You can generate text from the model with the code below. + +```python +import os +from openai import OpenAI + +client = OpenAI( + # This is the default and can be omitted + api_key=os.environ.get("OPENAI_API_KEY"), +) + +response = client.responses.create( + model="gpt-4o", + instructions="You are a coding assistant that talks like a pirate.", + input="How do I check if a Python object is an instance of a class?", +) + +print(response.output_text) +``` + +The previous standard (supported indefinitely) for generating text is the [Chat Completions API](https://platform.openai.com/docs/api-reference/chat). You can use that API to generate text from the model with the code below. + +```python +from openai import OpenAI + +client = OpenAI() + +completion = client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "developer", "content": "Talk like a pirate."}, + { + "role": "user", + "content": "How do I check if a Python object is an instance of a class?", + }, + ], +) + +print(completion.choices[0].message.content) +``` + +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `OPENAI_API_KEY="My API Key"` to your `.env` file +so that your API key is not stored in source control. +[Get an API key here](https://platform.openai.com/settings/organization/api-keys). + +### Vision + +With an image URL: + +```python +prompt = "What is in this image?" +img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/2023_06_08_Raccoon1.jpg/1599px-2023_06_08_Raccoon1.jpg" + +response = client.responses.create( + model="gpt-4o-mini", + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": prompt}, + {"type": "input_image", "image_url": f"{img_url}"}, + ], + } + ], +) +``` + +With the image as a base64 encoded string: + +```python +import base64 +from openai import OpenAI + +client = OpenAI() + +prompt = "What is in this image?" +with open("path/to/image.png", "rb") as image_file: + b64_image = base64.b64encode(image_file.read()).decode("utf-8") + +response = client.responses.create( + model="gpt-4o-mini", + input=[ + { + "role": "user", + "content": [ + {"type": "input_text", "text": prompt}, + {"type": "input_image", "image_url": f"data:image/png;base64,{b64_image}"}, + ], + } + ], +) +``` + +## Async usage + +Simply import `AsyncOpenAI` instead of `OpenAI` and use `await` with each API call: + +```python +import os +import asyncio +from openai import AsyncOpenAI + +client = AsyncOpenAI( + # This is the default and can be omitted + api_key=os.environ.get("OPENAI_API_KEY"), +) + + +async def main() -> None: + response = await client.responses.create( + model="gpt-4o", input="Explain disestablishmentarianism to a smart five year old." + ) + print(response.output_text) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +## Streaming responses + +We provide support for streaming responses using Server Side Events (SSE). + +```python +from openai import OpenAI + +client = OpenAI() + +stream = client.responses.create( + model="gpt-4o", + input="Write a one-sentence bedtime story about a unicorn.", + stream=True, +) + +for event in stream: + print(event) +``` + +The async client uses the exact same interface. + +```python +import asyncio +from openai import AsyncOpenAI + +client = AsyncOpenAI() + + +async def main(): + stream = client.responses.create( + model="gpt-4o", + input="Write a one-sentence bedtime story about a unicorn.", + stream=True, + ) + + for event in stream: + print(event) + + +asyncio.run(main()) +``` + +## Realtime API beta + +The Realtime API enables you to build low-latency, multi-modal conversational experiences. It currently supports text and audio as both input and output, as well as [function calling](https://platform.openai.com/docs/guides/function-calling) through a WebSocket connection. + +Under the hood the SDK uses the [`websockets`](https://websockets.readthedocs.io/en/stable/) library to manage connections. + +The Realtime API works through a combination of client-sent events and server-sent events. Clients can send events to do things like update session configuration or send text and audio inputs. Server events confirm when audio responses have completed, or when a text response from the model has been received. A full event reference can be found [here](https://platform.openai.com/docs/api-reference/realtime-client-events) and a guide can be found [here](https://platform.openai.com/docs/guides/realtime). + +Basic text based example: + +```py +import asyncio +from openai import AsyncOpenAI + +async def main(): + client = AsyncOpenAI() + + async with client.beta.realtime.connect(model="gpt-4o-realtime-preview") as connection: + await connection.session.update(session={'modalities': ['text']}) + + await connection.conversation.item.create( + item={ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Say hello!"}], + } + ) + await connection.response.create() + + async for event in connection: + if event.type == 'response.text.delta': + print(event.delta, flush=True, end="") + + elif event.type == 'response.text.done': + print() + + elif event.type == "response.done": + break + +asyncio.run(main()) +``` + +However the real magic of the Realtime API is handling audio inputs / outputs, see this example [TUI script](https://github.com/openai/openai-python/blob/main/examples/realtime/push_to_talk_app.py) for a fully fledged example. + +### Realtime error handling + +Whenever an error occurs, the Realtime API will send an [`error` event](https://platform.openai.com/docs/guides/realtime-model-capabilities#error-handling) and the connection will stay open and remain usable. This means you need to handle it yourself, as _no errors are raised directly_ by the SDK when an `error` event comes in. + +```py +client = AsyncOpenAI() + +async with client.beta.realtime.connect(model="gpt-4o-realtime-preview") as connection: + ... + async for event in connection: + if event.type == 'error': + print(event.error.type) + print(event.error.code) + print(event.error.event_id) + print(event.error.message) +``` + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## Pagination + +List methods in the OpenAI API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from openai import OpenAI + +client = OpenAI() + +all_jobs = [] +# Automatically fetches more pages as needed. +for job in client.fine_tuning.jobs.list( + limit=20, +): + # Do something with job here + all_jobs.append(job) +print(all_jobs) +``` + +Or, asynchronously: + +```python +import asyncio +from openai import AsyncOpenAI + +client = AsyncOpenAI() + + +async def main() -> None: + all_jobs = [] + # Iterate through items across all pages, issuing requests as needed. + async for job in client.fine_tuning.jobs.list( + limit=20, + ): + all_jobs.append(job) + print(all_jobs) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.fine_tuning.jobs.list( + limit=20, +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.data)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.fine_tuning.jobs.list( + limit=20, +) + +print(f"next page cursor: {first_page.after}") # => "next page cursor: ..." +for job in first_page.data: + print(job.id) + +# Remove `await` for non-async usage. +``` + +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from openai import OpenAI + +client = OpenAI() + +response = client.chat.responses.create( + input=[ + { + "role": "user", + "content": "How much ?", + } + ], + model="gpt-4o", + response_format={"type": "json_object"}, +) +``` + +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from openai import OpenAI + +client = OpenAI() + +client.files.create( + file=Path("input.jsonl"), + purpose="fine-tune", +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `openai.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `openai.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `openai.APIError`. + +```python +import openai +from openai import OpenAI + +client = OpenAI() + +try: + client.fine_tuning.jobs.create( + model="gpt-4o", + training_file="file-abc123", + ) +except openai.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except openai.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except openai.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as follows: + +| Status Code | Error Type | +| ----------- | -------------------------- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | +| N/A | `APIConnectionError` | + +## Request IDs + +> For more information on debugging requests, see [these docs](https://platform.openai.com/docs/api-reference/debugging-requests) + +All object responses in the SDK provide a `_request_id` property which is added from the `x-request-id` response header so that you can quickly log failing requests and report them back to OpenAI. + +```python +response = await client.responses.create( + model="gpt-4o-mini", + input="Say 'this is a test'.", +) +print(response._request_id) # req_123 +``` + +Note that unlike other properties that use an `_` prefix, the `_request_id` property +_is_ public. Unless documented otherwise, _all_ other `_` prefix properties, +methods and modules are _private_. + +> [!IMPORTANT] +> If you need to access request IDs for failed requests you must catch the `APIStatusError` exception + +```python +import openai + +try: + completion = await client.chat.completions.create( + messages=[{"role": "user", "content": "Say this is a test"}], model="gpt-4" + ) +except openai.APIStatusError as exc: + print(exc.request_id) # req_123 + raise exc +``` + +## Retries + +Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, +429 Rate Limit, and >=500 Internal errors are all retried by default. + +You can use the `max_retries` option to configure or disable retry settings: + +```python +from openai import OpenAI + +# Configure the default for all requests: +client = OpenAI( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).chat.completions.create( + messages=[ + { + "role": "user", + "content": "How can I get the name of the current day in JavaScript?", + } + ], + model="gpt-4o", +) +``` + +## Timeouts + +By default requests time out after 10 minutes. You can configure this with a `timeout` option, +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: + +```python +from openai import OpenAI + +# Configure the default for all requests: +client = OpenAI( + # 20 seconds (default is 10 minutes) + timeout=20.0, +) + +# More granular control: +client = OpenAI( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).chat.completions.create( + messages=[ + { + "role": "user", + "content": "How can I list all files in a directory using Python?", + } + ], + model="gpt-4o", +) +``` + +On timeout, an `APITimeoutError` is thrown. + +Note that requests that time out are [retried twice by default](https://github.com/openai/openai-python/tree/main/#retries). + +## Advanced + +### Logging + +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. + +You can enable logging by setting the environment variable `OPENAI_LOG` to `info`. + +```shell +$ export OPENAI_LOG=info +``` + +Or to `debug` for more verbose logging. + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: + +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` + +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., + +```py +from openai import OpenAI + +client = OpenAI() +response = client.chat.completions.with_raw_response.create( + messages=[{ + "role": "user", + "content": "Say this is a test", + }], + model="gpt-4o", +) +print(response.headers.get('X-My-Header')) + +completion = response.parse() # get the object that `chat.completions.create()` would have returned +print(completion) +``` + +These methods return a [`LegacyAPIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_legacy_response.py) object. This is a legacy class as we're changing it slightly in the next major version. + +For the sync client this will mostly be the same with the exception +of `content` & `text` will be methods instead of properties. In the +async client, all methods will be async. + +A migration script will be provided & the migration in general should +be smooth. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. + +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. + +As such, `.with_streaming_response` methods return a different [`APIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_response.py) object, and the async client returns an [`AsyncAPIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_response.py) object. + +```python +with client.chat.completions.with_streaming_response.create( + messages=[ + { + "role": "user", + "content": "Say this is a test", + } + ], + model="gpt-4o", +) as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. + +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) when making this request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). + +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality + +```python +import httpx +from openai import OpenAI, DefaultHttpxClient + +client = OpenAI( + # Or use the `OPENAI_BASE_URL` env var + base_url="http://my.test.server.example.com:8083/v1", + http_client=DefaultHttpxClient( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +```py +from openai import OpenAI + +with OpenAI() as client: + # make requests here + ... + +# HTTP client is now closed +``` + +## Microsoft Azure OpenAI + +To use this library with [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview), use the `AzureOpenAI` +class instead of the `OpenAI` class. + +> [!IMPORTANT] +> The Azure API shape differs from the core API shape which means that the static types for responses / params +> won't always be correct. + +```py +from openai import AzureOpenAI + +# gets the API Key from environment variable AZURE_OPENAI_API_KEY +client = AzureOpenAI( + # https://learn.microsoft.com/azure/ai-services/openai/reference#rest-api-versioning + api_version="2023-07-01-preview", + # https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource + azure_endpoint="https://example-endpoint.openai.azure.com", +) + +completion = client.chat.completions.create( + model="deployment-name", # e.g. gpt-35-instant + messages=[ + { + "role": "user", + "content": "How do I output all files in a directory using Python?", + }, + ], +) +print(completion.to_json()) +``` + +In addition to the options provided in the base `OpenAI` client, the following options are provided: + +- `azure_endpoint` (or the `AZURE_OPENAI_ENDPOINT` environment variable) +- `azure_deployment` +- `api_version` (or the `OPENAI_API_VERSION` environment variable) +- `azure_ad_token` (or the `AZURE_OPENAI_AD_TOKEN` environment variable) +- `azure_ad_token_provider` + +An example of using the client with Microsoft Entra ID (formerly known as Azure Active Directory) can be found [here](https://github.com/openai/openai-python/blob/main/examples/azure_ad.py). + +## Versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes that only affect static types, without breaking runtime behavior. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +3. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/openai/openai-python/issues) with questions, bugs, or suggestions. + +### Determining the installed version + +If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. + +You can determine the version that is being used at runtime with: + +```py +import openai +print(openai.__version__) +``` + +## Requirements + +Python 3.8 or higher. + +## Contributing + +See [the contributing documentation](https://github.com/openai/openai-python/tree/main/./CONTRIBUTING.md). diff --git a/lib/python3.12/site-packages/openai-1.75.0.dist-info/REQUESTED b/lib/python3.12/site-packages/openai-1.75.0.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/torio/__init__.py b/lib/python3.12/site-packages/torio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..23efa0b2fda2221d721e18a575dd75870e2aece4 --- /dev/null +++ b/lib/python3.12/site-packages/torio/__init__.py @@ -0,0 +1,8 @@ +from . import _extension # noqa # usort: skip +from . import io, utils + + +__all__ = [ + "io", + "utils", +] diff --git a/lib/python3.12/site-packages/torio/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torio/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1af06385eeb8ed8d4cce5069c59f4d9c35fc8593 Binary files /dev/null and b/lib/python3.12/site-packages/torio/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/_extension/__init__.py b/lib/python3.12/site-packages/torio/_extension/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f11ace88312abe5e395af0889551bb1e5b543950 --- /dev/null +++ b/lib/python3.12/site-packages/torio/_extension/__init__.py @@ -0,0 +1,13 @@ +from .utils import _init_ffmpeg, _LazyImporter + + +_FFMPEG_EXT = None + + +def lazy_import_ffmpeg_ext(): + """Load FFmpeg integration based on availability in lazy manner""" + + global _FFMPEG_EXT + if _FFMPEG_EXT is None: + _FFMPEG_EXT = _LazyImporter("_torio_ffmpeg", _init_ffmpeg) + return _FFMPEG_EXT diff --git a/lib/python3.12/site-packages/torio/_extension/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torio/_extension/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d9deda975fcf4c8620aeee4b5363c720e3f20f1 Binary files /dev/null and b/lib/python3.12/site-packages/torio/_extension/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/_extension/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/torio/_extension/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1b9c61b85790cfced109c533d6c5eeed5f3260d Binary files /dev/null and b/lib/python3.12/site-packages/torio/_extension/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/_extension/utils.py b/lib/python3.12/site-packages/torio/_extension/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c72d59c16f923721d3bfb4d08c426dba1b6a990f --- /dev/null +++ b/lib/python3.12/site-packages/torio/_extension/utils.py @@ -0,0 +1,147 @@ +import importlib +import logging +import os +import types +from pathlib import Path + +import torch + +_LG = logging.getLogger(__name__) +_LIB_DIR = Path(__file__).parent.parent / "lib" + + +class _LazyImporter(types.ModuleType): + """Lazily import module/extension.""" + + def __init__(self, name, import_func): + super().__init__(name) + self.import_func = import_func + self.module = None + + # Note: + # Python caches what was retrieved with `__getattr__`, so this method will not be + # called again for the same item. + def __getattr__(self, item): + self._import_once() + return getattr(self.module, item) + + def __repr__(self): + if self.module is None: + return f"" + return repr(self.module) + + def __dir__(self): + self._import_once() + return dir(self.module) + + def _import_once(self): + if self.module is None: + self.module = self.import_func() + # Note: + # By attaching the module attributes to self, + # module attributes are directly accessible. + # This allows to avoid calling __getattr__ for every attribute access. + self.__dict__.update(self.module.__dict__) + + def is_available(self): + try: + self._import_once() + except Exception: + return False + return True + + +def _get_lib_path(lib: str): + suffix = "pyd" if os.name == "nt" else "so" + path = _LIB_DIR / f"{lib}.{suffix}" + return path + + +def _load_lib(lib: str) -> bool: + """Load extension module + + Note: + In case `torio` is deployed with `pex` format, the library file + is not in a standard location. + In this case, we expect that `libtorio` is available somewhere + in the search path of dynamic loading mechanism, so that importing + `_torio` will have library loader find and load `libtorio`. + This is the reason why the function should not raising an error when the library + file is not found. + + Returns: + bool: + True if the library file is found AND the library loaded without failure. + False if the library file is not found (like in the case where torio + is deployed with pex format, thus the shared library file is + in a non-standard location.). + If the library file is found but there is an issue loading the library, + (such as missing dependency) then this function raises the exception as-is. + + Raises: + Exception: + If the library file is found, but there is an issue loading the library file, + (when underlying `ctype.DLL` throws an exception), this function will pass + the exception as-is, instead of catching it and returning bool. + The expected case is `OSError` thrown by `ctype.DLL` when a dynamic dependency + is not found. + This behavior was chosen because the expected failure case is not recoverable. + If a dependency is missing, then users have to install it. + """ + path = _get_lib_path(lib) + if not path.exists(): + return False + torch.ops.load_library(path) + return True + + +_FFMPEG_VERS = ["6", "5", "4", ""] + + +def _find_versionsed_ffmpeg_extension(version: str): + ext = f"torio.lib._torio_ffmpeg{version}" + lib = f"libtorio_ffmpeg{version}" + + if not importlib.util.find_spec(ext): + raise RuntimeError(f"FFmpeg{version} extension is not available.") + + _load_lib(lib) + return importlib.import_module(ext) + + +def _find_ffmpeg_extension(ffmpeg_vers): + for ffmpeg_ver in ffmpeg_vers: + _LG.debug("Loading FFmpeg%s", ffmpeg_ver) + try: + ext = _find_versionsed_ffmpeg_extension(ffmpeg_ver) + _LG.debug("Successfully loaded FFmpeg%s", ffmpeg_ver) + return ext + except Exception: + _LG.debug("Failed to load FFmpeg%s extension.", ffmpeg_ver, exc_info=True) + continue + raise ImportError( + f"Failed to intialize FFmpeg extension. Tried versions: {ffmpeg_vers}. " + "Enable DEBUG logging to see more details about the error." + ) + + +def _get_ffmpeg_versions(): + ffmpeg_vers = _FFMPEG_VERS + # User override + if (ffmpeg_ver := os.environ.get("TORIO_USE_FFMPEG_VERSION")) is not None: + if ffmpeg_ver not in ffmpeg_vers: + raise ValueError( + f"The FFmpeg version '{ffmpeg_ver}' (read from TORIO_USE_FFMPEG_VERSION) " + f"is not one of supported values. Possible values are {ffmpeg_vers}" + ) + ffmpeg_vers = [ffmpeg_ver] + return ffmpeg_vers + + +def _init_ffmpeg(): + ffmpeg_vers = _get_ffmpeg_versions() + ext = _find_ffmpeg_extension(ffmpeg_vers) + ext.init() + if ext.get_log_level() > 8: + ext.set_log_level(8) + return ext diff --git a/lib/python3.12/site-packages/torio/io/__init__.py b/lib/python3.12/site-packages/torio/io/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7fce6d775278a5e1470d4cf4e2daeeb39f5e5469 --- /dev/null +++ b/lib/python3.12/site-packages/torio/io/__init__.py @@ -0,0 +1,9 @@ +from ._streaming_media_decoder import StreamingMediaDecoder +from ._streaming_media_encoder import CodecConfig, StreamingMediaEncoder + + +__all__ = [ + "StreamingMediaDecoder", + "CodecConfig", + "StreamingMediaEncoder", +] diff --git a/lib/python3.12/site-packages/torio/io/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torio/io/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..baf5138dac739772c7bccc9e1449dd5b47017c32 Binary files /dev/null and b/lib/python3.12/site-packages/torio/io/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/io/__pycache__/_streaming_media_decoder.cpython-312.pyc b/lib/python3.12/site-packages/torio/io/__pycache__/_streaming_media_decoder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1e952be3e9da70d432239aab45dc32491296a1f Binary files /dev/null and b/lib/python3.12/site-packages/torio/io/__pycache__/_streaming_media_decoder.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/io/__pycache__/_streaming_media_encoder.cpython-312.pyc b/lib/python3.12/site-packages/torio/io/__pycache__/_streaming_media_encoder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..882058598e43d1fac9cc73167d076e9245cadae5 Binary files /dev/null and b/lib/python3.12/site-packages/torio/io/__pycache__/_streaming_media_encoder.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/io/_streaming_media_decoder.py b/lib/python3.12/site-packages/torio/io/_streaming_media_decoder.py new file mode 100644 index 0000000000000000000000000000000000000000..8e81b537e7cce56e38da1004e9d3acf01879f9bd --- /dev/null +++ b/lib/python3.12/site-packages/torio/io/_streaming_media_decoder.py @@ -0,0 +1,978 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path +from typing import BinaryIO, Dict, Iterator, Optional, Tuple, TypeVar, Union + +import torch +import torio +from torch.utils._pytree import tree_map + +ffmpeg_ext = torio._extension.lazy_import_ffmpeg_ext() + +__all__ = [ + "StreamingMediaDecoder", +] + + +@dataclass +class SourceStream: + """The metadata of a source stream, returned by :meth:`~torio.io.StreamingMediaDecoder.get_src_stream_info`. + + This class is used when representing streams of media type other than `audio` or `video`. + + When source stream is `audio` or `video` type, :class:`SourceAudioStream` and + :class:`SourceVideoStream`, which reports additional media-specific attributes, + are used respectively. + """ + + media_type: str + """The type of the stream. + One of ``"audio"``, ``"video"``, ``"data"``, ``"subtitle"``, ``"attachment"`` and empty string. + + .. note:: + Only audio and video streams are supported for output. + .. note:: + Still images, such as PNG and JPEG formats are reported as video. + """ + codec: str + """Short name of the codec. Such as ``"pcm_s16le"`` and ``"h264"``.""" + codec_long_name: str + """Detailed name of the codec. + + Such as "`PCM signed 16-bit little-endian`" and "`H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10`". + """ + format: Optional[str] + """Media format. Such as ``"s16"`` and ``"yuv420p"``. + + Commonly found audio values are; + + - ``"u8"``, ``"u8p"``: Unsigned 8-bit unsigned interger. + - ``"s16"``, ``"s16p"``: 16-bit signed integer. + - ``"s32"``, ``"s32p"``: 32-bit signed integer. + - ``"flt"``, ``"fltp"``: 32-bit floating-point. + + .. note:: + + `p` at the end indicates the format is `planar`. + Channels are grouped together instead of interspersed in memory. + """ + bit_rate: Optional[int] + """Bit rate of the stream in bits-per-second. + This is an estimated values based on the initial few frames of the stream. + For container formats and variable bit rate, it can be 0. + """ + num_frames: Optional[int] + """The number of frames in the stream""" + bits_per_sample: Optional[int] + """This is the number of valid bits in each output sample. + For compressed format, it can be 0. + """ + metadata: Dict[str, str] + """Metadata attached to the source stream.""" + + +@dataclass +class SourceAudioStream(SourceStream): + """The metadata of an audio source stream, returned by :meth:`~torio.io.StreamingMediaDecoder.get_src_stream_info`. + + This class is used when representing audio stream. + + In addition to the attributes reported by :class:`SourceStream`, + the following attributes are reported. + """ + + sample_rate: float + """Sample rate of the audio.""" + num_channels: int + """Number of channels.""" + + +@dataclass +class SourceVideoStream(SourceStream): + """The metadata of a video source stream, returned by :meth:`~torio.io.StreamingMediaDecoder.get_src_stream_info`. + + This class is used when representing video stream. + + In addition to the attributes reported by :class:`SourceStream`, + the following attributes are reported. + """ + + width: int + """Width of the video frame in pixel.""" + height: int + """Height of the video frame in pixel.""" + frame_rate: float + """Frame rate.""" + + +def _parse_si(i): + media_type = i.media_type + if media_type == "audio": + return SourceAudioStream( + media_type=i.media_type, + codec=i.codec_name, + codec_long_name=i.codec_long_name, + format=i.format, + bit_rate=i.bit_rate, + num_frames=i.num_frames, + bits_per_sample=i.bits_per_sample, + metadata=i.metadata, + sample_rate=i.sample_rate, + num_channels=i.num_channels, + ) + if media_type == "video": + return SourceVideoStream( + media_type=i.media_type, + codec=i.codec_name, + codec_long_name=i.codec_long_name, + format=i.format, + bit_rate=i.bit_rate, + num_frames=i.num_frames, + bits_per_sample=i.bits_per_sample, + metadata=i.metadata, + width=i.width, + height=i.height, + frame_rate=i.frame_rate, + ) + return SourceStream( + media_type=i.media_type, + codec=i.codec_name, + codec_long_name=i.codec_long_name, + format=None, + bit_rate=None, + num_frames=None, + bits_per_sample=None, + metadata=i.metadata, + ) + + +@dataclass +class OutputStream: + """Output stream configured on :class:`StreamingMediaDecoder`, + returned by :meth:`~torio.io.StreamingMediaDecoder.get_out_stream_info`. + """ + + source_index: int + """Index of the source stream that this output stream is connected.""" + filter_description: str + """Description of filter graph applied to the source stream.""" + media_type: str + """The type of the stream. ``"audio"`` or ``"video"``.""" + format: str + """Media format. Such as ``"s16"`` and ``"yuv420p"``. + + Commonly found audio values are; + + - ``"u8"``, ``"u8p"``: Unsigned 8-bit unsigned interger. + - ``"s16"``, ``"s16p"``: 16-bit signed integer. + - ``"s32"``, ``"s32p"``: 32-bit signed integer. + - ``"flt"``, ``"fltp"``: 32-bit floating-point. + + .. note:: + + `p` at the end indicates the format is `planar`. + Channels are grouped together instead of interspersed in memory.""" + + +@dataclass +class OutputAudioStream(OutputStream): + """Information about an audio output stream configured with + :meth:`~torio.io.StreamingMediaDecoder.add_audio_stream` or + :meth:`~torio.io.StreamingMediaDecoder.add_basic_audio_stream`. + + In addition to the attributes reported by :class:`OutputStream`, + the following attributes are reported. + """ + + sample_rate: float + """Sample rate of the audio.""" + num_channels: int + """Number of channels.""" + + +@dataclass +class OutputVideoStream(OutputStream): + """Information about a video output stream configured with + :meth:`~torio.io.StreamingMediaDecoder.add_video_stream` or + :meth:`~torio.io.StreamingMediaDecoder.add_basic_video_stream`. + + In addition to the attributes reported by :class:`OutputStream`, + the following attributes are reported. + """ + + width: int + """Width of the video frame in pixel.""" + height: int + """Height of the video frame in pixel.""" + frame_rate: float + """Frame rate.""" + + +def _parse_oi(i): + media_type = i.media_type + if media_type == "audio": + return OutputAudioStream( + source_index=i.source_index, + filter_description=i.filter_description, + media_type=i.media_type, + format=i.format, + sample_rate=i.sample_rate, + num_channels=i.num_channels, + ) + if media_type == "video": + return OutputVideoStream( + source_index=i.source_index, + filter_description=i.filter_description, + media_type=i.media_type, + format=i.format, + width=i.width, + height=i.height, + frame_rate=i.frame_rate, + ) + raise ValueError(f"Unexpected media_type: {i.media_type}({i})") + + +def _get_afilter_desc(sample_rate: Optional[int], fmt: Optional[str], num_channels: Optional[int]): + descs = [] + if sample_rate is not None: + descs.append(f"aresample={sample_rate}") + if fmt is not None or num_channels is not None: + parts = [] + if fmt is not None: + parts.append(f"sample_fmts={fmt}") + if num_channels is not None: + parts.append(f"channel_layouts={num_channels}c") + descs.append(f"aformat={':'.join(parts)}") + return ",".join(descs) if descs else None + + +def _get_vfilter_desc(frame_rate: Optional[float], width: Optional[int], height: Optional[int], fmt: Optional[str]): + descs = [] + if frame_rate is not None: + descs.append(f"fps={frame_rate}") + scales = [] + if width is not None: + scales.append(f"width={width}") + if height is not None: + scales.append(f"height={height}") + if scales: + descs.append(f"scale={':'.join(scales)}") + if fmt is not None: + descs.append(f"format=pix_fmts={fmt}") + return ",".join(descs) if descs else None + + +# Base class for ChunkTensor +# Based off of TrivialTensorViaComposition +# https://github.com/albanD/subclass_zoo/blob/0eeb1d68fb59879029c610bc407f2997ae43ba0a/trivial_tensors.py#L83 +class ChunkTensorBase(torch.Tensor): + __torch_function__ = torch._C._disabled_torch_function_impl + + @staticmethod + def __new__(cls, _elem, *_): + return super().__new__(cls, _elem) + + @classmethod + def __torch_dispatch__(cls, func, _, args=(), kwargs=None): + def unwrap(t): + return t._elem if isinstance(t, cls) else t + + return func(*tree_map(unwrap, args), **tree_map(unwrap, kwargs)) + + +@dataclass +class ChunkTensor(ChunkTensorBase): + """Decoded media frames with metadata. + + The instance of this class represents the decoded video/audio frames with + metadata, and the instance itself behave like :py:class:`~torch.Tensor`. + + Client codes can pass instance of this class as-if it's + :py:class:`~torch.Tensor` class, or call the methods defined on + :py:class:`~torch.Tensor` class. + + Example: + >>> # Define input streams + >>> reader = StreamingMediaDecoder(...) + >>> reader.add_audio_stream(frames_per_chunk=4000, sample_rate=8000) + >>> reader.add_video_stream(frames_per_chunk=7, frame_rate=28) + >>> # Decode the streams and fetch frames + >>> reader.fill_buffer() + >>> audio_chunk, video_chunk = reader.pop_chunks() + + >>> # Access metadata + >>> (audio_chunk.pts, video_chunks.pts) + (0.0, 0.0) + >>> + >>> # The second time the PTS is different + >>> reader.fill_buffer() + >>> audio_chunk, video_chunk = reader.pop_chunks() + >>> (audio_chunk.pts, video_chunks.pts) + (0.5, 0.25) + + >>> # Call PyTorch ops on chunk + >>> audio_chunk.shape + torch.Size([4000, 2] + >>> power = torch.pow(video_chunk, 2) + >>> + >>> # the result is a plain torch.Tensor class + >>> type(power) + + >>> + >>> # Metadata is not available on the result + >>> power.pts + AttributeError: 'Tensor' object has no attribute 'pts' + """ + + # Keep it private for now + _elem: torch.Tensor + + pts: float + """Presentation time stamp of the first frame in the chunk. + + Unit: second. + """ + + +def _format_doc(**kwargs): + def decorator(obj): + obj.__doc__ = obj.__doc__.format(**kwargs) + return obj + + return decorator + + +_frames_per_chunk = """Number of frames returned as one chunk. + If the source stream is exhausted before enough frames are buffered, + then the chunk is returned as-is. + + Providing ``-1`` disables chunking and :py:func:`pop_chunks` method + will concatenate all the buffered frames and return it.""" + +_buffer_chunk_size = """Internal buffer size. + When the number of chunks buffered exceeds this number, old frames are + dropped. For example, if ``frames_per_chunk`` is 5 and ``buffer_chunk_size`` is + 3, then frames older than ``15`` are dropped. + Providing ``-1`` disables this behavior. + + Default: ``3``.""" + +_audio_stream_index = """The source audio stream index. + If omitted, :py:attr:`default_audio_stream` is used.""" + + +_video_stream_index = """The source video stream index. + If omitted, :py:attr:`default_video_stream` is used.""" + +_decoder = """The name of the decoder to be used. + When provided, use the specified decoder instead of the default one. + + To list the available decoders, please use + :py:func:`~torio.utils.ffmpeg_utils.get_audio_decoders` for audio, and + :py:func:`~torio.utils.ffmpeg_utils.get_video_decoders` for video. + + Default: ``None``.""" + +_decoder_option = """Options passed to decoder. + Mapping from str to str. (Default: ``None``) + + To list decoder options for a decoder, you can use + ``ffmpeg -h decoder=`` command. + + | + + In addition to decoder-specific options, you can also pass options related + to multithreading. They are effective only if the decoder support them. + If neither of them are provided, StreamingMediaDecoder defaults to single thread. + + ``"threads"``: The number of threads (in str). + Providing the value ``"0"`` will let FFmpeg decides based on its heuristics. + + ``"thread_type"``: Which multithreading method to use. + The valid values are ``"frame"`` or ``"slice"``. + Note that each decoder supports different set of methods. + If not provided, a default value is used. + + - ``"frame"``: Decode more than one frame at once. + Each thread handles one frame. + This will increase decoding delay by one frame per thread + - ``"slice"``: Decode more than one part of a single frame at once. + + | + """ + + +_hw_accel = """Enable hardware acceleration. + + When video is decoded on CUDA hardware, for example + `decoder="h264_cuvid"`, passing CUDA device indicator to `hw_accel` + (i.e. `hw_accel="cuda:0"`) will make StreamingMediaDecoder place the resulting + frames directly on the specified CUDA device as CUDA tensor. + + If `None`, the frame will be moved to CPU memory. + Default: ``None``.""" + + +_format_audio_args = _format_doc( + frames_per_chunk=_frames_per_chunk, + buffer_chunk_size=_buffer_chunk_size, + stream_index=_audio_stream_index, + decoder=_decoder, + decoder_option=_decoder_option, +) + + +_format_video_args = _format_doc( + frames_per_chunk=_frames_per_chunk, + buffer_chunk_size=_buffer_chunk_size, + stream_index=_video_stream_index, + decoder=_decoder, + decoder_option=_decoder_option, + hw_accel=_hw_accel, +) + + +InputStreamTypes = TypeVar("InputStream", bound=SourceStream) +OutputStreamTypes = TypeVar("OutputStream", bound=OutputStream) + + +class StreamingMediaDecoder: + """Fetch and decode audio/video streams chunk by chunk. + + For the detailed usage of this class, please refer to the tutorial. + + Args: + src (str, path-like, bytes or file-like object): The media source. + If string-type, it must be a resource indicator that FFmpeg can + handle. This includes a file path, URL, device identifier or + filter expression. The supported value depends on the FFmpeg found + in the system. + + If bytes, it must be an encoded media data in contiguous memory. + + If file-like object, it must support `read` method with the signature + `read(size: int) -> bytes`. + Additionally, if the file-like object has `seek` method, it uses + the method when parsing media metadata. This improves the reliability + of codec detection. The signagure of `seek` method must be + `seek(offset: int, whence: int) -> int`. + + Please refer to the following for the expected signature and behavior + of `read` and `seek` method. + + - https://docs.python.org/3/library/io.html#io.BufferedIOBase.read + - https://docs.python.org/3/library/io.html#io.IOBase.seek + + format (str or None, optional): + Override the input format, or specify the source sound device. + Default: ``None`` (no override nor device input). + + This argument serves two different usecases. + + 1) Override the source format. + This is useful when the input data do not contain a header. + + 2) Specify the input source device. + This allows to load media stream from hardware devices, + such as microphone, camera and screen, or a virtual device. + + + .. note:: + + This option roughly corresponds to ``-f`` option of ``ffmpeg`` command. + Please refer to the ffmpeg documentations for the possible values. + + https://ffmpeg.org/ffmpeg-formats.html#Demuxers + + Please use :py:func:`~torio.utils.ffmpeg_utils.get_demuxers` to list the + demultiplexers available in the current environment. + + For device access, the available values vary based on hardware (AV device) and + software configuration (ffmpeg build). + + https://ffmpeg.org/ffmpeg-devices.html#Input-Devices + + Please use :py:func:`~torio.utils.ffmpeg_utils.get_input_devices` to list + the input devices available in the current environment. + + option (dict of str to str, optional): + Custom option passed when initializing format context (opening source). + + You can use this argument to change the input source before it is passed to decoder. + + Default: ``None``. + + buffer_size (int): + The internal buffer size in byte. Used only when `src` is file-like object. + + Default: `4096`. + """ + + def __init__( + self, + src: Union[str, Path, BinaryIO], + format: Optional[str] = None, + option: Optional[Dict[str, str]] = None, + buffer_size: int = 4096, + ): + self.src = src + if isinstance(src, bytes): + self._be = ffmpeg_ext.StreamingMediaDecoderBytes(src, format, option, buffer_size) + elif hasattr(src, "read"): + self._be = ffmpeg_ext.StreamingMediaDecoderFileObj(src, format, option, buffer_size) + else: + self._be = ffmpeg_ext.StreamingMediaDecoder(os.path.normpath(src), format, option) + + i = self._be.find_best_audio_stream() + self._default_audio_stream = None if i < 0 else i + i = self._be.find_best_video_stream() + self._default_video_stream = None if i < 0 else i + + @property + def num_src_streams(self): + """Number of streams found in the provided media source. + + :type: int + """ + return self._be.num_src_streams() + + @property + def num_out_streams(self): + """Number of output streams configured by client code. + + :type: int + """ + return self._be.num_out_streams() + + @property + def default_audio_stream(self): + """The index of default audio stream. ``None`` if there is no audio stream + + :type: Optional[int] + """ + return self._default_audio_stream + + @property + def default_video_stream(self): + """The index of default video stream. ``None`` if there is no video stream + + :type: Optional[int] + """ + return self._default_video_stream + + def get_metadata(self) -> Dict[str, str]: + """Get the metadata of the source media. + + Returns: + dict + """ + return self._be.get_metadata() + + def get_src_stream_info(self, i: int) -> InputStreamTypes: + """Get the metadata of source stream + + Args: + i (int): Stream index. + Returns: + InputStreamTypes: + Information about the source stream. + If the source stream is audio type, then + :class:`~torio.io._stream_reader.SourceAudioStream` is returned. + If it is video type, then + :class:`~torio.io._stream_reader.SourceVideoStream` is returned. + Otherwise :class:`~torio.io._stream_reader.SourceStream` class is returned. + """ + return _parse_si(self._be.get_src_stream_info(i)) + + def get_out_stream_info(self, i: int) -> OutputStreamTypes: + """Get the metadata of output stream + + Args: + i (int): Stream index. + Returns: + OutputStreamTypes + Information about the output stream. + If the output stream is audio type, then + :class:`~torio.io._stream_reader.OutputAudioStream` is returned. + If it is video type, then + :class:`~torio.io._stream_reader.OutputVideoStream` is returned. + """ + info = self._be.get_out_stream_info(i) + return _parse_oi(info) + + def seek(self, timestamp: float, mode: str = "precise"): + """Seek the stream to the given timestamp [second] + + Args: + timestamp (float): Target time in second. + mode (str): Controls how seek is done. + Valid choices are; + + * "key": Seek into the nearest key frame before the given timestamp. + * "any": Seek into any frame (including non-key frames) before the given timestamp. + * "precise": First seek into the nearest key frame before the given timestamp, then + decode frames until it reaches the closes frame to the given timestamp. + + Note: + All the modes invalidate and reset the internal state of decoder. + When using "any" mode and if it ends up seeking into non-key frame, + the image decoded may be invalid due to lack of key frame. + Using "precise" will workaround this issue by decoding frames from previous + key frame, but will be slower. + """ + modes = { + "key": 0, + "any": 1, + "precise": 2, + } + if mode not in modes: + raise ValueError(f"The value of mode must be one of {list(modes.keys())}. Found: {mode}") + self._be.seek(timestamp, modes[mode]) + + @_format_audio_args + def add_basic_audio_stream( + self, + frames_per_chunk: int, + buffer_chunk_size: int = 3, + *, + stream_index: Optional[int] = None, + decoder: Optional[str] = None, + decoder_option: Optional[Dict[str, str]] = None, + format: Optional[str] = "fltp", + sample_rate: Optional[int] = None, + num_channels: Optional[int] = None, + ): + """Add output audio stream + + Args: + frames_per_chunk (int): {frames_per_chunk} + + buffer_chunk_size (int, optional): {buffer_chunk_size} + + stream_index (int or None, optional): {stream_index} + + decoder (str or None, optional): {decoder} + + decoder_option (dict or None, optional): {decoder_option} + + format (str, optional): Output sample format (precision). + + If ``None``, the output chunk has dtype corresponding to + the precision of the source audio. + + Otherwise, the sample is converted and the output dtype is changed + as following. + + - ``"u8p"``: The output is ``torch.uint8`` type. + - ``"s16p"``: The output is ``torch.int16`` type. + - ``"s32p"``: The output is ``torch.int32`` type. + - ``"s64p"``: The output is ``torch.int64`` type. + - ``"fltp"``: The output is ``torch.float32`` type. + - ``"dblp"``: The output is ``torch.float64`` type. + + Default: ``"fltp"``. + + sample_rate (int or None, optional): If provided, resample the audio. + + num_channels (int, or None, optional): If provided, change the number of channels. + """ + self.add_audio_stream( + frames_per_chunk, + buffer_chunk_size, + stream_index=stream_index, + decoder=decoder, + decoder_option=decoder_option, + filter_desc=_get_afilter_desc(sample_rate, format, num_channels), + ) + + @_format_video_args + def add_basic_video_stream( + self, + frames_per_chunk: int, + buffer_chunk_size: int = 3, + *, + stream_index: Optional[int] = None, + decoder: Optional[str] = None, + decoder_option: Optional[Dict[str, str]] = None, + format: Optional[str] = "rgb24", + frame_rate: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + hw_accel: Optional[str] = None, + ): + """Add output video stream + + Args: + frames_per_chunk (int): {frames_per_chunk} + + buffer_chunk_size (int, optional): {buffer_chunk_size} + + stream_index (int or None, optional): {stream_index} + + decoder (str or None, optional): {decoder} + + decoder_option (dict or None, optional): {decoder_option} + + format (str, optional): Change the format of image channels. Valid values are, + + - ``"rgb24"``: 8 bits * 3 channels (R, G, B) + - ``"bgr24"``: 8 bits * 3 channels (B, G, R) + - ``"yuv420p"``: 8 bits * 3 channels (Y, U, V) + - ``"gray"``: 8 bits * 1 channels + + Default: ``"rgb24"``. + + frame_rate (int or None, optional): If provided, change the frame rate. + + width (int or None, optional): If provided, change the image width. Unit: Pixel. + + height (int or None, optional): If provided, change the image height. Unit: Pixel. + + hw_accel (str or None, optional): {hw_accel} + """ + self.add_video_stream( + frames_per_chunk, + buffer_chunk_size, + stream_index=stream_index, + decoder=decoder, + decoder_option=decoder_option, + filter_desc=_get_vfilter_desc(frame_rate, width, height, format), + hw_accel=hw_accel, + ) + + @_format_audio_args + def add_audio_stream( + self, + frames_per_chunk: int, + buffer_chunk_size: int = 3, + *, + stream_index: Optional[int] = None, + decoder: Optional[str] = None, + decoder_option: Optional[Dict[str, str]] = None, + filter_desc: Optional[str] = None, + ): + """Add output audio stream + + Args: + frames_per_chunk (int): {frames_per_chunk} + + buffer_chunk_size (int, optional): {buffer_chunk_size} + + stream_index (int or None, optional): {stream_index} + + decoder (str or None, optional): {decoder} + + decoder_option (dict or None, optional): {decoder_option} + + filter_desc (str or None, optional): Filter description. + The list of available filters can be found at + https://ffmpeg.org/ffmpeg-filters.html + Note that complex filters are not supported. + + """ + i = self.default_audio_stream if stream_index is None else stream_index + if i is None: + raise RuntimeError("There is no audio stream.") + self._be.add_audio_stream( + i, + frames_per_chunk, + buffer_chunk_size, + filter_desc, + decoder, + decoder_option or {}, + ) + + @_format_video_args + def add_video_stream( + self, + frames_per_chunk: int, + buffer_chunk_size: int = 3, + *, + stream_index: Optional[int] = None, + decoder: Optional[str] = None, + decoder_option: Optional[Dict[str, str]] = None, + filter_desc: Optional[str] = None, + hw_accel: Optional[str] = None, + ): + """Add output video stream + + Args: + frames_per_chunk (int): {frames_per_chunk} + + buffer_chunk_size (int, optional): {buffer_chunk_size} + + stream_index (int or None, optional): {stream_index} + + decoder (str or None, optional): {decoder} + + decoder_option (dict or None, optional): {decoder_option} + + hw_accel (str or None, optional): {hw_accel} + + filter_desc (str or None, optional): Filter description. + The list of available filters can be found at + https://ffmpeg.org/ffmpeg-filters.html + Note that complex filters are not supported. + """ + i = self.default_video_stream if stream_index is None else stream_index + if i is None: + raise RuntimeError("There is no video stream.") + self._be.add_video_stream( + i, + frames_per_chunk, + buffer_chunk_size, + filter_desc, + decoder, + decoder_option or {}, + hw_accel, + ) + + def remove_stream(self, i: int): + """Remove an output stream. + + Args: + i (int): Index of the output stream to be removed. + """ + self._be.remove_stream(i) + + def process_packet(self, timeout: Optional[float] = None, backoff: float = 10.0) -> int: + """Read the source media and process one packet. + + If a packet is read successfully, then the data in the packet will + be decoded and passed to corresponding output stream processors. + + If the packet belongs to a source stream that is not connected to + an output stream, then the data are discarded. + + When the source reaches EOF, then it triggers all the output stream + processors to enter drain mode. All the output stream processors + flush the pending frames. + + Args: + timeout (float or None, optional): Timeout in milli seconds. + + This argument changes the retry behavior when it failed to + process a packet due to the underlying media resource being + temporarily unavailable. + + When using a media device such as a microphone, there are cases + where the underlying buffer is not ready. + Calling this function in such case would cause the system to report + `EAGAIN (resource temporarily unavailable)`. + + * ``>=0``: Keep retrying until the given time passes. + + * ``0<``: Keep retrying forever. + + * ``None`` : No retrying and raise an exception immediately. + + Default: ``None``. + + Note: + + The retry behavior is applicable only when the reason is the + unavailable resource. It is not invoked if the reason of failure is + other. + + backoff (float, optional): Time to wait before retrying in milli seconds. + + This option is effective only when `timeout` is effective. (not ``None``) + + When `timeout` is effective, this `backoff` controls how long the function + should wait before retrying. Default: ``10.0``. + + Returns: + int: + ``0`` + A packet was processed properly. The caller can keep + calling this function to buffer more frames. + + ``1`` + The streamer reached EOF. All the output stream processors + flushed the pending frames. The caller should stop calling + this method. + """ + return self._be.process_packet(timeout, backoff) + + def process_all_packets(self): + """Process packets until it reaches EOF.""" + self._be.process_all_packets() + + def is_buffer_ready(self) -> bool: + """Returns true if all the output streams have at least one chunk filled.""" + return self._be.is_buffer_ready() + + def pop_chunks(self) -> Tuple[Optional[ChunkTensor]]: + """Pop one chunk from all the output stream buffers. + + Returns: + Tuple[Optional[ChunkTensor]]: + Buffer contents. + If a buffer does not contain any frame, then `None` is returned instead. + """ + ret = [] + for chunk in self._be.pop_chunks(): + if chunk is None: + ret.append(None) + else: + ret.append(ChunkTensor(chunk.frames, chunk.pts)) + return ret + + def fill_buffer(self, timeout: Optional[float] = None, backoff: float = 10.0) -> int: + """Keep processing packets until all buffers have at least one chunk + + Arguments: + timeout (float or None, optional): See + :py:func:`~StreamingMediaDecoder.process_packet`. (Default: ``None``) + + backoff (float, optional): See + :py:func:`~StreamingMediaDecoder.process_packet`. (Default: ``10.0``) + + Returns: + int: + ``0`` + Packets are processed properly and buffers are + ready to be popped once. + + ``1`` + The streamer reached EOF. All the output stream processors + flushed the pending frames. The caller should stop calling + this method. + """ + return self._be.fill_buffer(timeout, backoff) + + def stream( + self, timeout: Optional[float] = None, backoff: float = 10.0 + ) -> Iterator[Tuple[Optional[ChunkTensor], ...]]: + """Return an iterator that generates output tensors + + Arguments: + timeout (float or None, optional): See + :py:func:`~StreamingMediaDecoder.process_packet`. (Default: ``None``) + + backoff (float, optional): See + :py:func:`~StreamingMediaDecoder.process_packet`. (Default: ``10.0``) + + Returns: + Iterator[Tuple[Optional[ChunkTensor], ...]]: + Iterator that yields a tuple of chunks that correspond to the output + streams defined by client code. + If an output stream is exhausted, then the chunk Tensor is substituted + with ``None``. + The iterator stops if all the output streams are exhausted. + """ + if self.num_out_streams == 0: + raise RuntimeError("No output stream is configured.") + + while True: + if self.fill_buffer(timeout, backoff): + break + yield self.pop_chunks() + + while True: + chunks = self.pop_chunks() + if all(c is None for c in chunks): + return + yield chunks diff --git a/lib/python3.12/site-packages/torio/io/_streaming_media_encoder.py b/lib/python3.12/site-packages/torio/io/_streaming_media_encoder.py new file mode 100644 index 0000000000000000000000000000000000000000..bfbfe8791bb5bc56c13daf06dad79f753c1cce0d --- /dev/null +++ b/lib/python3.12/site-packages/torio/io/_streaming_media_encoder.py @@ -0,0 +1,502 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import BinaryIO, Dict, Optional, Union + +import torch +import torio + +ffmpeg_ext = torio._extension.lazy_import_ffmpeg_ext() + + +@dataclass +class CodecConfig: + """Codec configuration.""" + + bit_rate: int = -1 + """Bit rate""" + + compression_level: int = -1 + """Compression level""" + + qscale: Optional[int] = None + """Global quality factor. Enables variable bit rate. Valid values depend on encoder. + + For example: MP3 takes ``0`` - ``9`` (https://trac.ffmpeg.org/wiki/Encode/MP3) while + libvorbis takes ``-1`` - ``10``. + """ + + gop_size: int = -1 + """The number of pictures in a group of pictures, or 0 for intra_only""" + + max_b_frames: int = -1 + """maximum number of B-frames between non-B-frames.""" + + +def _convert_config(cfg: CodecConfig): + if cfg is None: + return None + # Convert the codecconfig to C++ compatible type. + # omitting the return type annotation so as not to access ffmpeg_ext here. + return ffmpeg_ext.CodecConfig( + cfg.bit_rate, + cfg.compression_level, + cfg.qscale, + cfg.gop_size, + cfg.max_b_frames, + ) + + +def _format_doc(**kwargs): + def decorator(obj): + obj.__doc__ = obj.__doc__.format(**kwargs) + return obj + + return decorator + + +_encoder = """The name of the encoder to be used. + When provided, use the specified encoder instead of the default one. + + To list the available encoders, please use + :py:func:`~torio.utils.ffmpeg_utils.get_audio_encoders` for audio, and + :py:func:`~torio.utils.ffmpeg_utils.get_video_encoders` for video. + + Default: ``None``.""" + + +_encoder_option = """Options passed to encoder. + Mapping from str to str. + + To list encoder options for a encoder, you can use + ``ffmpeg -h encoder=`` command. + + Default: ``None``. + + | + + In addition to encoder-specific options, you can also pass options related + to multithreading. They are effective only if the encoder support them. + If neither of them are provided, StreamReader defaults to single thread. + + ``"threads"``: The number of threads (in str). + Providing the value ``"0"`` will let FFmpeg decides based on its heuristics. + + ``"thread_type"``: Which multithreading method to use. + The valid values are ``"frame"`` or ``"slice"``. + Note that each encoder supports different set of methods. + If not provided, a default value is used. + + - ``"frame"``: Encode more than one frame at once. + Each thread handles one frame. + This will increase decoding delay by one frame per thread + - ``"slice"``: Encode more than one part of a single frame at once. + + | + """ + + +_encoder_format = """Format used to encode media. + When encoder supports multiple formats, passing this argument will override + the format used for encoding. + + To list supported formats for the encoder, you can use + ``ffmpeg -h encoder=`` command. + + Default: ``None``. + + Note: + When ``encoder_format`` option is not provided, encoder uses its default format. + + For example, when encoding audio into wav format, 16-bit signed integer is used, + and when encoding video into mp4 format (h264 encoder), one of YUV format is used. + + This is because typically, 32-bit or 16-bit floating point is used in audio models but + they are not commonly used in audio formats. Similarly, RGB24 is commonly used in vision + models, but video formats usually (and better) support YUV formats. + """ + +_codec_config = """Codec configuration. Please refer to :py:class:`CodecConfig` for + configuration options. + + Default: ``None``.""" + + +_filter_desc = """Additional processing to apply before encoding the input media. + """ + +_format_common_args = _format_doc( + encoder=_encoder, + encoder_option=_encoder_option, + encoder_format=_encoder_format, + codec_config=_codec_config, + filter_desc=_filter_desc, +) + + +class StreamingMediaEncoder: + """Encode and write audio/video streams chunk by chunk + + Args: + dst (str, path-like or file-like object): The destination where the encoded data are written. + If string-type, it must be a resource indicator that FFmpeg can + handle. The supported value depends on the FFmpeg found in the system. + + If file-like object, it must support `write` method with the signature + `write(data: bytes) -> int`. + + Please refer to the following for the expected signature and behavior of + `write` method. + + - https://docs.python.org/3/library/io.html#io.BufferedIOBase.write + + format (str or None, optional): + Override the output format, or specify the output media device. + Default: ``None`` (no override nor device output). + + This argument serves two different use cases. + + 1) Override the output format. + This is useful when writing raw data or in a format different from the extension. + + 2) Specify the output device. + This allows to output media streams to hardware devices, + such as speaker and video screen. + + .. note:: + + This option roughly corresponds to ``-f`` option of ``ffmpeg`` command. + Please refer to the ffmpeg documentations for possible values. + + https://ffmpeg.org/ffmpeg-formats.html#Muxers + + Please use :py:func:`~torio.utils.ffmpeg_utils.get_muxers` to list the + multiplexers available in the current environment. + + For device access, the available values vary based on hardware (AV device) and + software configuration (ffmpeg build). + Please refer to the ffmpeg documentations for possible values. + + https://ffmpeg.org/ffmpeg-devices.html#Output-Devices + + Please use :py:func:`~torio.utils.ffmpeg_utils.get_output_devices` to list + the output devices available in the current environment. + + buffer_size (int): + The internal buffer size in byte. Used only when `dst` is a file-like object. + + Default: `4096`. + """ + + def __init__( + self, + dst: Union[str, Path, BinaryIO], + format: Optional[str] = None, + buffer_size: int = 4096, + ): + if hasattr(dst, "write"): + self._s = ffmpeg_ext.StreamingMediaEncoderFileObj(dst, format, buffer_size) + else: + self._s = ffmpeg_ext.StreamingMediaEncoder(str(dst), format) + self._is_open = False + + @_format_common_args + def add_audio_stream( + self, + sample_rate: int, + num_channels: int, + format: str = "flt", + *, + encoder: Optional[str] = None, + encoder_option: Optional[Dict[str, str]] = None, + encoder_sample_rate: Optional[int] = None, + encoder_num_channels: Optional[int] = None, + encoder_format: Optional[str] = None, + codec_config: Optional[CodecConfig] = None, + filter_desc: Optional[str] = None, + ): + """Add an output audio stream. + + Args: + sample_rate (int): The sample rate. + + num_channels (int): The number of channels. + + format (str, optional): Input sample format, which determines the dtype + of the input tensor. + + - ``"u8"``: The input tensor must be ``torch.uint8`` type. + - ``"s16"``: The input tensor must be ``torch.int16`` type. + - ``"s32"``: The input tensor must be ``torch.int32`` type. + - ``"s64"``: The input tensor must be ``torch.int64`` type. + - ``"flt"``: The input tensor must be ``torch.float32`` type. + - ``"dbl"``: The input tensor must be ``torch.float64`` type. + + Default: ``"flt"``. + + encoder (str or None, optional): {encoder} + + encoder_option (dict or None, optional): {encoder_option} + + encoder_sample_rate (int or None, optional): Override the sample rate used for encoding time. + Some encoders pose restriction on the sample rate used for encoding. + If the source sample rate is not supported by the encoder, the source sample rate is used, + otherwise a default one is picked. + + For example, ``"opus"`` encoder only supports 48k Hz, so, when encoding a + waveform with ``"opus"`` encoder, it is always encoded as 48k Hz. + Meanwhile ``"mp3"`` (``"libmp3lame"``) supports 44.1k, 48k, 32k, 22.05k, + 24k, 16k, 11.025k, 12k and 8k Hz. + If the original sample rate is one of these, then the original sample rate + is used, otherwise it will be resampled to a default one (44.1k). + When encoding into WAV format, there is no restriction on sample rate, + so the original sample rate will be used. + + Providing ``encoder_sample_rate`` will override this behavior and + make encoder attempt to use the provided sample rate. + The provided value must be one support by the encoder. + + encoder_num_channels (int or None, optional): Override the number of channels used for encoding. + + Similar to sample rate, some encoders (such as ``"opus"``, + ``"vorbis"`` and ``"g722"``) pose restriction on + the numbe of channels that can be used for encoding. + + If the original number of channels is supported by encoder, + then it will be used, otherwise, the encoder attempts to + remix the channel to one of the supported ones. + + Providing ``encoder_num_channels`` will override this behavior and + make encoder attempt to use the provided number of channels. + The provided value must be one support by the encoder. + + encoder_format (str or None, optional): {encoder_format} + + codec_config (CodecConfig or None, optional): {codec_config} + + filter_desc (str or None, optional): {filter_desc} + """ + self._s.add_audio_stream( + sample_rate, + num_channels, + format, + encoder, + encoder_option, + encoder_format, + encoder_sample_rate, + encoder_num_channels, + _convert_config(codec_config), + filter_desc, + ) + + @_format_common_args + def add_video_stream( + self, + frame_rate: float, + width: int, + height: int, + format: str = "rgb24", + *, + encoder: Optional[str] = None, + encoder_option: Optional[Dict[str, str]] = None, + encoder_frame_rate: Optional[float] = None, + encoder_width: Optional[int] = None, + encoder_height: Optional[int] = None, + encoder_format: Optional[str] = None, + codec_config: Optional[CodecConfig] = None, + filter_desc: Optional[str] = None, + hw_accel: Optional[str] = None, + ): + """Add an output video stream. + + This method has to be called before `open` is called. + + Args: + frame_rate (float): Frame rate of the video. + + width (int): Width of the video frame. + + height (int): Height of the video frame. + + format (str, optional): Input pixel format, which determines the + color channel order of the input tensor. + + - ``"gray8"``: One channel, grayscale. + - ``"rgb24"``: Three channels in the order of RGB. + - ``"bgr24"``: Three channels in the order of BGR. + - ``"yuv444p"``: Three channels in the order of YUV. + + Default: ``"rgb24"``. + + In either case, the input tensor has to be ``torch.uint8`` type and + the shape must be (frame, channel, height, width). + + encoder (str or None, optional): {encoder} + + encoder_option (dict or None, optional): {encoder_option} + + encoder_frame_rate (float or None, optional): Override the frame rate used for encoding. + + Some encoders, (such as ``"mpeg1"`` and ``"mpeg2"``) pose restriction on the + frame rate that can be used for encoding. + If such case, if the source frame rate (provided as ``frame_rate``) is not + one of the supported frame rate, then a default one is picked, and the frame rate + is changed on-the-fly. Otherwise the source frame rate is used. + + Providing ``encoder_frame_rate`` will override this behavior and + make encoder attempts to use the provided sample rate. + The provided value must be one support by the encoder. + + encoder_width (int or None, optional): Width of the image used for encoding. + This allows to change the image size during encoding. + + encoder_height (int or None, optional): Height of the image used for encoding. + This allows to change the image size during encoding. + + encoder_format (str or None, optional): {encoder_format} + + codec_config (CodecConfig or None, optional): {codec_config} + + filter_desc (str or None, optional): {filter_desc} + + hw_accel (str or None, optional): Enable hardware acceleration. + + When video is encoded on CUDA hardware, for example + `encoder="h264_nvenc"`, passing CUDA device indicator to `hw_accel` + (i.e. `hw_accel="cuda:0"`) will make StreamingMediaEncoder expect video + chunk to be CUDA Tensor. Passing CPU Tensor will result in an error. + + If `None`, the video chunk Tensor has to be CPU Tensor. + Default: ``None``. + """ + self._s.add_video_stream( + frame_rate, + width, + height, + format, + encoder, + encoder_option, + encoder_format, + encoder_frame_rate, + encoder_width, + encoder_height, + hw_accel, + _convert_config(codec_config), + filter_desc, + ) + + def set_metadata(self, metadata: Dict[str, str]): + """Set file-level metadata + + Args: + metadata (dict or None, optional): File-level metadata. + """ + self._s.set_metadata(metadata) + + def _print_output_stream(self, i: int): + """[debug] Print the registered stream information to stdout.""" + self._s.dump_format(i) + + def open(self, option: Optional[Dict[str, str]] = None) -> "StreamingMediaEncoder": + """Open the output file / device and write the header. + + :py:class:`StreamingMediaEncoder` is also a context manager and therefore supports the + ``with`` statement. + This method returns the instance on which the method is called (i.e. `self`), + so that it can be used in `with` statement. + It is recommended to use context manager, as the file is closed automatically + when exiting from ``with`` clause. + + Args: + option (dict or None, optional): Private options for protocol, device and muxer. See example. + + Example - Protocol option + >>> s = StreamingMediaEncoder(dst="rtmp://localhost:1234/live/app", format="flv") + >>> s.add_video_stream(...) + >>> # Passing protocol option `listen=1` makes StreamingMediaEncoder act as RTMP server. + >>> with s.open(option={"listen": "1"}) as f: + >>> f.write_video_chunk(...) + + Example - Device option + >>> s = StreamingMediaEncoder("-", format="sdl") + >>> s.add_video_stream(..., encoder_format="rgb24") + >>> # Open SDL video player with fullscreen + >>> with s.open(option={"window_fullscreen": "1"}): + >>> f.write_video_chunk(...) + + Example - Muxer option + >>> s = StreamingMediaEncoder("foo.flac") + >>> s.add_audio_stream(...) + >>> s.set_metadata({"artist": "torio contributors"}) + >>> # FLAC muxer has a private option to not write the header. + >>> # The resulting file does not contain the above metadata. + >>> with s.open(option={"write_header": "false"}) as f: + >>> f.write_audio_chunk(...) + """ + if not self._is_open: + self._s.open(option) + self._is_open = True + return self + + def close(self): + """Close the output + + :py:class:`StreamingMediaEncoder` is also a context manager and therefore supports the + ``with`` statement. + It is recommended to use context manager, as the file is closed automatically + when exiting from ``with`` clause. + + See :py:meth:`StreamingMediaEncoder.open` for more detail. + """ + if self._is_open: + self._s.close() + self._is_open = False + + def write_audio_chunk(self, i: int, chunk: torch.Tensor, pts: Optional[float] = None): + """Write audio data + + Args: + i (int): Stream index. + chunk (Tensor): Waveform tensor. Shape: `(frame, channel)`. + The ``dtype`` must match what was passed to :py:meth:`add_audio_stream` method. + pts (float, optional, or None): If provided, overwrite the presentation timestamp. + + .. note:: + + The provided value is converted to integer value expressed in basis of + sample rate. Therefore, it is truncated to the nearest value of + ``n / sample_rate``. + """ + self._s.write_audio_chunk(i, chunk, pts) + + def write_video_chunk(self, i: int, chunk: torch.Tensor, pts: Optional[float] = None): + """Write video/image data + + Args: + i (int): Stream index. + chunk (Tensor): Video/image tensor. + Shape: `(time, channel, height, width)`. + The ``dtype`` must be ``torch.uint8``. + The shape (height, width and the number of channels) must match + what was configured when calling :py:meth:`add_video_stream` + pts (float, optional or None): If provided, overwrite the presentation timestamp. + + .. note:: + + The provided value is converted to integer value expressed in basis of + frame rate. Therefore, it is truncated to the nearest value of + ``n / frame_rate``. + """ + self._s.write_video_chunk(i, chunk, pts) + + def flush(self): + """Flush the frames from encoders and write the frames to the destination.""" + self._s.flush() + + def __enter__(self): + """Context manager so that the destination is closed and data are flushed automatically.""" + return self + + def __exit__(self, exception_type, exception_value, traceback): + """Context manager so that the destination is closed and data are flushed automatically.""" + self.flush() + self.close() diff --git a/lib/python3.12/site-packages/torio/lib/__init__.py b/lib/python3.12/site-packages/torio/lib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/torio/lib/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torio/lib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1929d17d7ca72126068a974f5d660d844d8837a2 Binary files /dev/null and b/lib/python3.12/site-packages/torio/lib/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/utils/__init__.py b/lib/python3.12/site-packages/torio/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a3dbc29a6a840214338e63074703b6f74debdc05 --- /dev/null +++ b/lib/python3.12/site-packages/torio/utils/__init__.py @@ -0,0 +1,4 @@ +from . import ffmpeg_utils + + +__all__ = ["ffmpeg_utils"] diff --git a/lib/python3.12/site-packages/torio/utils/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torio/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c91616233bff66afbc237500fd969c20214cbc6 Binary files /dev/null and b/lib/python3.12/site-packages/torio/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/utils/__pycache__/ffmpeg_utils.cpython-312.pyc b/lib/python3.12/site-packages/torio/utils/__pycache__/ffmpeg_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13cca2e5ce236a02f37e3a1fcda401d6a52028a8 Binary files /dev/null and b/lib/python3.12/site-packages/torio/utils/__pycache__/ffmpeg_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torio/utils/ffmpeg_utils.py b/lib/python3.12/site-packages/torio/utils/ffmpeg_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..06f20bd25112cbe79ed89c7b4c888826bccf8a9f --- /dev/null +++ b/lib/python3.12/site-packages/torio/utils/ffmpeg_utils.py @@ -0,0 +1,247 @@ +"""Module to change the configuration of FFmpeg libraries (such as libavformat). + +It affects functionalities in :py:mod:`torio.io`. +""" +from typing import Dict, List, Tuple + +import torio + +ffmpeg_ext = torio._extension.lazy_import_ffmpeg_ext() + + +def get_versions() -> Dict[str, Tuple[int]]: + """Get the versions of FFmpeg libraries + + Returns: + dict: mapping from library names to version string, + i.e. `"libavutil": (56, 22, 100)`. + """ + return ffmpeg_ext.get_versions() + + +def get_log_level() -> int: + """Get the log level of FFmpeg. + + See :py:func:`set_log_level` for the detail. + """ + return ffmpeg_ext.get_log_level() + + +def set_log_level(level: int): + """Set the log level of FFmpeg (libavformat etc) + + Arguments: + level (int): Log level. The larger, the more verbose. + + The following values are common values, the corresponding ``ffmpeg``'s + ``-loglevel`` option value and desription. + + * ``-8`` (``quiet``): + Print no output. + * ``0`` (``panic``): + Something went really wrong and we will crash now. + * ``8`` (``fatal``): + Something went wrong and recovery is not possible. + For example, no header was found for a format which depends + on headers or an illegal combination of parameters is used. + * ``16`` (``error``): + Something went wrong and cannot losslessly be recovered. + However, not all future data is affected. + * ``24`` (``warning``): + Something somehow does not look correct. + This may or may not lead to problems. + * ``32`` (``info``): + Standard information. + * ``40`` (``verbose``): + Detailed information. + * ``48`` (``debug``): + Stuff which is only useful for libav* developers. + * ``56`` (``trace``): + Extremely verbose debugging, useful for libav* development. + + """ + ffmpeg_ext.set_log_level(level) + + +def get_demuxers() -> Dict[str, str]: + """Get the available demuxers. + + Returns: + Dict[str, str]: Mapping from demuxer (format) short name to long name. + + Example + >>> for k, v in get_demuxers().items(): + >>> print(f"{k}: {v}") + ... aa: Audible AA format files + ... aac: raw ADTS AAC (Advanced Audio Coding) + ... aax: CRI AAX + ... ac3: raw AC-3 + """ + return ffmpeg_ext.get_demuxers() + + +def get_muxers() -> Dict[str, str]: + """Get the available muxers. + + Returns: + Dict[str, str]: Mapping from muxer (format) short name to long name. + + Example + >>> for k, v in get_muxers().items(): + >>> print(f"{k}: {v}") + ... a64: a64 - video for Commodore 64 + ... ac3: raw AC-3 + ... adts: ADTS AAC (Advanced Audio Coding) + ... adx: CRI ADX + ... aiff: Audio IFF + """ + return ffmpeg_ext.get_muxers() + + +def get_audio_decoders() -> Dict[str, str]: + """Get the available audio decoders. + + Returns: + Dict[str, str]: Mapping from decoder short name to long name. + + Example + >>> for k, v in get_audio_decoders().items(): + >>> print(f"{k}: {v}") + ... a64: a64 - video for Commodore 64 + ... ac3: raw AC-3 + ... adts: ADTS AAC (Advanced Audio Coding) + ... adx: CRI ADX + ... aiff: Audio IFF + """ + return ffmpeg_ext.get_audio_decoders() + + +def get_audio_encoders() -> Dict[str, str]: + """Get the available audio encoders. + + Returns: + Dict[str, str]: Mapping from encoder short name to long name. + + Example + >>> for k, v in get_audio_encoders().items(): + >>> print(f"{k}: {v}") + ... comfortnoise: RFC 3389 comfort noise generator + ... s302m: SMPTE 302M + ... aac: AAC (Advanced Audio Coding) + ... ac3: ATSC A/52A (AC-3) + ... ac3_fixed: ATSC A/52A (AC-3) + ... alac: ALAC (Apple Lossless Audio Codec) + """ + return ffmpeg_ext.get_audio_encoders() + + +def get_video_decoders() -> Dict[str, str]: + """Get the available video decoders. + + Returns: + Dict[str, str]: Mapping from decoder short name to long name. + + Example + >>> for k, v in get_video_decoders().items(): + >>> print(f"{k}: {v}") + ... aasc: Autodesk RLE + ... aic: Apple Intermediate Codec + ... alias_pix: Alias/Wavefront PIX image + ... agm: Amuse Graphics Movie + ... amv: AMV Video + ... anm: Deluxe Paint Animation + """ + return ffmpeg_ext.get_video_decoders() + + +def get_video_encoders() -> Dict[str, str]: + """Get the available video encoders. + + Returns: + Dict[str, str]: Mapping from encoder short name to long name. + + Example + >>> for k, v in get_audio_encoders().items(): + >>> print(f"{k}: {v}") + ... a64multi: Multicolor charset for Commodore 64 + ... a64multi5: Multicolor charset for Commodore 64, extended with 5th color (colram) + ... alias_pix: Alias/Wavefront PIX image + ... amv: AMV Video + ... apng: APNG (Animated Portable Network Graphics) image + ... asv1: ASUS V1 + ... asv2: ASUS V2 + """ + return ffmpeg_ext.get_video_encoders() + + +def get_input_devices() -> Dict[str, str]: + """Get the available input devices. + + Returns: + Dict[str, str]: Mapping from device short name to long name. + + Example + >>> for k, v in get_input_devices().items(): + >>> print(f"{k}: {v}") + ... avfoundation: AVFoundation input device + ... lavfi: Libavfilter virtual input device + """ + return ffmpeg_ext.get_input_devices() + + +def get_output_devices() -> Dict[str, str]: + """Get the available output devices. + + Returns: + Dict[str, str]: Mapping from device short name to long name. + + Example + >>> for k, v in get_output_devices().items(): + >>> print(f"{k}: {v}") + ... audiotoolbox: AudioToolbox output device + """ + return ffmpeg_ext.get_output_devices() + + +def get_input_protocols() -> List[str]: + """Get the supported input protocols. + + Returns: + List[str]: The names of supported input protocols + + Example + >>> print(get_input_protocols()) + ... ['file', 'ftp', 'hls', 'http','https', 'pipe', 'rtmp', 'tcp', 'tls', 'udp', 'unix'] + """ + return ffmpeg_ext.get_input_protocols() + + +def get_output_protocols() -> List[str]: + """Get the supported output protocols. + + Returns: + list of str: The names of supported output protocols + + Example + >>> print(get_output_protocols()) + ... ['file', 'ftp', 'http', 'https', 'md5', 'pipe', 'prompeg', 'rtmp', 'tee', 'tcp', 'tls', 'udp', 'unix'] + """ + return ffmpeg_ext.get_output_protocols() + + +def get_build_config() -> str: + """Get the FFmpeg build configuration + + Returns: + str: Build configuration string. + + Example + >>> print(get_build_config()) + --prefix=/Users/runner/miniforge3 --cc=arm64-apple-darwin20.0.0-clang --enable-gpl --enable-hardcoded-tables --enable-libfreetype --enable-libopenh264 --enable-neon --enable-libx264 --enable-libx265 --enable-libaom --enable-libsvtav1 --enable-libxml2 --enable-libvpx --enable-pic --enable-pthreads --enable-shared --disable-static --enable-version3 --enable-zlib --enable-libmp3lame --pkg-config=/Users/runner/miniforge3/conda-bld/ffmpeg_1646229390493/_build_env/bin/pkg-config --enable-cross-compile --arch=arm64 --target-os=darwin --cross-prefix=arm64-apple-darwin20.0.0- --host-cc=/Users/runner/miniforge3/conda-bld/ffmpeg_1646229390493/_build_env/bin/x86_64-apple-darwin13.4.0-clang # noqa + """ + return ffmpeg_ext.get_build_config() + + +def clear_cuda_context_cache(): + """Clear the CUDA context used by CUDA Hardware accelerated video decoding""" + ffmpeg_ext.clear_cuda_context_cache() diff --git a/lib/python3.12/site-packages/uvicorn/__init__.py b/lib/python3.12/site-packages/uvicorn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0e8f4b2be991fd10687d1ed1e29f98d6b1da0369 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/__init__.py @@ -0,0 +1,5 @@ +from uvicorn.config import Config +from uvicorn.main import Server, main, run + +__version__ = "0.34.2" +__all__ = ["main", "run", "Config", "Server"] diff --git a/lib/python3.12/site-packages/uvicorn/__main__.py b/lib/python3.12/site-packages/uvicorn/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..8a1dc979a39e36ce81e987843391479f125d73f3 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/__main__.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.main() diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d12364d4eb982fe17f7e13ebadedbaa8abfd5efd Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/__main__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51d8470c23fc7f8c7b79008dc39792fbc4d0d7ae Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/__main__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/_subprocess.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/_subprocess.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08ebc418b80e4bfae197fe67846879cad078ef04 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/_subprocess.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/_types.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/_types.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5aeed47eb471d92b4177a4afb83fa825453e2c2e Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/_types.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5aad93433bd55339860608bcb85dedeca41a23d Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/importer.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/importer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3863069336d01b80e9e176aa75f834207d5be3c0 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/importer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/logging.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/logging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1c16dab03d1c5a0eab111376d9f5e19f3f36bea Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/logging.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/main.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7e79be72784c2a52825615cae6f12e9af44752f Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/main.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/server.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8661f89d77d1f6a465c7c82088777692204bc24 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/server.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/__pycache__/workers.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/__pycache__/workers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9bb460a558e2a921594cdf8444e0352279b613e Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/__pycache__/workers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/_subprocess.py b/lib/python3.12/site-packages/uvicorn/_subprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..1c06844de5f1e1fa06394e42d11259c65555cd14 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/_subprocess.py @@ -0,0 +1,84 @@ +""" +Some light wrappers around Python's multiprocessing, to deal with cleanly +starting child processes. +""" + +from __future__ import annotations + +import multiprocessing +import os +import sys +from multiprocessing.context import SpawnProcess +from socket import socket +from typing import Callable + +from uvicorn.config import Config + +multiprocessing.allow_connection_pickling() +spawn = multiprocessing.get_context("spawn") + + +def get_subprocess( + config: Config, + target: Callable[..., None], + sockets: list[socket], +) -> SpawnProcess: + """ + Called in the parent process, to instantiate a new child process instance. + The child is not yet started at this point. + + * config - The Uvicorn configuration instance. + * target - A callable that accepts a list of sockets. In practice this will + be the `Server.run()` method. + * sockets - A list of sockets to pass to the server. Sockets are bound once + by the parent process, and then passed to the child processes. + """ + # We pass across the stdin fileno, and reopen it in the child process. + # This is required for some debugging environments. + try: + stdin_fileno = sys.stdin.fileno() + # The `sys.stdin` can be `None`, see https://docs.python.org/3/library/sys.html#sys.__stdin__. + except (AttributeError, OSError): + stdin_fileno = None + + kwargs = { + "config": config, + "target": target, + "sockets": sockets, + "stdin_fileno": stdin_fileno, + } + + return spawn.Process(target=subprocess_started, kwargs=kwargs) + + +def subprocess_started( + config: Config, + target: Callable[..., None], + sockets: list[socket], + stdin_fileno: int | None, +) -> None: + """ + Called when the child process starts. + + * config - The Uvicorn configuration instance. + * target - A callable that accepts a list of sockets. In practice this will + be the `Server.run()` method. + * sockets - A list of sockets to pass to the server. Sockets are bound once + by the parent process, and then passed to the child processes. + * stdin_fileno - The file number of sys.stdin, so that it can be reattached + to the child process. + """ + # Re-open stdin. + if stdin_fileno is not None: + sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage + + # Logging needs to be setup again for each child. + config.configure_logging() + + try: + # Now we can call into `Server.run(sockets=sockets)` + target(sockets=sockets) + except KeyboardInterrupt: # pragma: no cover + # supress the exception to avoid a traceback from subprocess.Popen + # the parent already expects us to end, so no vital information is lost + pass diff --git a/lib/python3.12/site-packages/uvicorn/_types.py b/lib/python3.12/site-packages/uvicorn/_types.py new file mode 100644 index 0000000000000000000000000000000000000000..c927cc11dc89cb57f69e62f7047140800d7ac5d2 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/_types.py @@ -0,0 +1,281 @@ +""" +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from __future__ import annotations + +import sys +import types +from collections.abc import Awaitable, Iterable, MutableMapping +from typing import Any, Callable, Literal, Optional, Protocol, TypedDict, Union + +if sys.version_info >= (3, 11): # pragma: py-lt-311 + from typing import NotRequired +else: # pragma: py-gte-311 + from typing_extensions import NotRequired + +# WSGI +Environ = MutableMapping[str, Any] +ExcInfo = tuple[type[BaseException], BaseException, Optional[types.TracebackType]] +StartResponse = Callable[[str, Iterable[tuple[str, str]], Optional[ExcInfo]], None] +WSGIApp = Callable[[Environ, StartResponse], Union[Iterable[bytes], BaseException]] + + +# ASGI +class ASGIVersions(TypedDict): + spec_version: str + version: Literal["2.0"] | Literal["3.0"] + + +class HTTPScope(TypedDict): + type: Literal["http"] + asgi: ASGIVersions + http_version: str + method: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[tuple[bytes, bytes]] + client: tuple[str, int] | None + server: tuple[str, int | None] | None + state: NotRequired[dict[str, Any]] + extensions: NotRequired[dict[str, dict[object, object]]] + + +class WebSocketScope(TypedDict): + type: Literal["websocket"] + asgi: ASGIVersions + http_version: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[tuple[bytes, bytes]] + client: tuple[str, int] | None + server: tuple[str, int | None] | None + subprotocols: Iterable[str] + state: NotRequired[dict[str, Any]] + extensions: NotRequired[dict[str, dict[object, object]]] + + +class LifespanScope(TypedDict): + type: Literal["lifespan"] + asgi: ASGIVersions + state: NotRequired[dict[str, Any]] + + +WWWScope = Union[HTTPScope, WebSocketScope] +Scope = Union[HTTPScope, WebSocketScope, LifespanScope] + + +class HTTPRequestEvent(TypedDict): + type: Literal["http.request"] + body: bytes + more_body: bool + + +class HTTPResponseDebugEvent(TypedDict): + type: Literal["http.response.debug"] + info: dict[str, object] + + +class HTTPResponseStartEvent(TypedDict): + type: Literal["http.response.start"] + status: int + headers: NotRequired[Iterable[tuple[bytes, bytes]]] + trailers: NotRequired[bool] + + +class HTTPResponseBodyEvent(TypedDict): + type: Literal["http.response.body"] + body: bytes + more_body: NotRequired[bool] + + +class HTTPResponseTrailersEvent(TypedDict): + type: Literal["http.response.trailers"] + headers: Iterable[tuple[bytes, bytes]] + more_trailers: bool + + +class HTTPServerPushEvent(TypedDict): + type: Literal["http.response.push"] + path: str + headers: Iterable[tuple[bytes, bytes]] + + +class HTTPDisconnectEvent(TypedDict): + type: Literal["http.disconnect"] + + +class WebSocketConnectEvent(TypedDict): + type: Literal["websocket.connect"] + + +class WebSocketAcceptEvent(TypedDict): + type: Literal["websocket.accept"] + subprotocol: NotRequired[str | None] + headers: NotRequired[Iterable[tuple[bytes, bytes]]] + + +class _WebSocketReceiveEventBytes(TypedDict): + type: Literal["websocket.receive"] + bytes: bytes + text: NotRequired[None] + + +class _WebSocketReceiveEventText(TypedDict): + type: Literal["websocket.receive"] + bytes: NotRequired[None] + text: str + + +WebSocketReceiveEvent = Union[_WebSocketReceiveEventBytes, _WebSocketReceiveEventText] + + +class _WebSocketSendEventBytes(TypedDict): + type: Literal["websocket.send"] + bytes: bytes + text: NotRequired[None] + + +class _WebSocketSendEventText(TypedDict): + type: Literal["websocket.send"] + bytes: NotRequired[None] + text: str + + +WebSocketSendEvent = Union[_WebSocketSendEventBytes, _WebSocketSendEventText] + + +class WebSocketResponseStartEvent(TypedDict): + type: Literal["websocket.http.response.start"] + status: int + headers: Iterable[tuple[bytes, bytes]] + + +class WebSocketResponseBodyEvent(TypedDict): + type: Literal["websocket.http.response.body"] + body: bytes + more_body: NotRequired[bool] + + +class WebSocketDisconnectEvent(TypedDict): + type: Literal["websocket.disconnect"] + code: int + reason: NotRequired[str | None] + + +class WebSocketCloseEvent(TypedDict): + type: Literal["websocket.close"] + code: NotRequired[int] + reason: NotRequired[str | None] + + +class LifespanStartupEvent(TypedDict): + type: Literal["lifespan.startup"] + + +class LifespanShutdownEvent(TypedDict): + type: Literal["lifespan.shutdown"] + + +class LifespanStartupCompleteEvent(TypedDict): + type: Literal["lifespan.startup.complete"] + + +class LifespanStartupFailedEvent(TypedDict): + type: Literal["lifespan.startup.failed"] + message: str + + +class LifespanShutdownCompleteEvent(TypedDict): + type: Literal["lifespan.shutdown.complete"] + + +class LifespanShutdownFailedEvent(TypedDict): + type: Literal["lifespan.shutdown.failed"] + message: str + + +WebSocketEvent = Union[WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent] + + +ASGIReceiveEvent = Union[ + HTTPRequestEvent, + HTTPDisconnectEvent, + WebSocketConnectEvent, + WebSocketReceiveEvent, + WebSocketDisconnectEvent, + LifespanStartupEvent, + LifespanShutdownEvent, +] + + +ASGISendEvent = Union[ + HTTPResponseStartEvent, + HTTPResponseBodyEvent, + HTTPResponseTrailersEvent, + HTTPServerPushEvent, + HTTPDisconnectEvent, + WebSocketAcceptEvent, + WebSocketSendEvent, + WebSocketResponseStartEvent, + WebSocketResponseBodyEvent, + WebSocketCloseEvent, + LifespanStartupCompleteEvent, + LifespanStartupFailedEvent, + LifespanShutdownCompleteEvent, + LifespanShutdownFailedEvent, +] + + +ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]] +ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]] + + +class ASGI2Protocol(Protocol): + def __init__(self, scope: Scope) -> None: ... # pragma: no cover + + async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover + + +ASGI2Application = type[ASGI2Protocol] +ASGI3Application = Callable[ + [ + Scope, + ASGIReceiveCallable, + ASGISendCallable, + ], + Awaitable[None], +] +ASGIApplication = Union[ASGI2Application, ASGI3Application] diff --git a/lib/python3.12/site-packages/uvicorn/config.py b/lib/python3.12/site-packages/uvicorn/config.py new file mode 100644 index 0000000000000000000000000000000000000000..ae996c1cbb6e82bc318ef97bb14edadc84b53d91 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/config.py @@ -0,0 +1,530 @@ +from __future__ import annotations + +import asyncio +import inspect +import json +import logging +import logging.config +import os +import socket +import ssl +import sys +from collections.abc import Awaitable +from configparser import RawConfigParser +from pathlib import Path +from typing import IO, Any, Callable, Literal + +import click + +from uvicorn._types import ASGIApplication +from uvicorn.importer import ImportFromStringError, import_from_string +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.middleware.asgi2 import ASGI2Middleware +from uvicorn.middleware.message_logger import MessageLoggerMiddleware +from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware +from uvicorn.middleware.wsgi import WSGIMiddleware + +HTTPProtocolType = Literal["auto", "h11", "httptools"] +WSProtocolType = Literal["auto", "none", "websockets", "wsproto"] +LifespanType = Literal["auto", "on", "off"] +LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"] +InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"] + +LOG_LEVELS: dict[str, int] = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, + "trace": TRACE_LOG_LEVEL, +} +HTTP_PROTOCOLS: dict[HTTPProtocolType, str] = { + "auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol", + "h11": "uvicorn.protocols.http.h11_impl:H11Protocol", + "httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol", +} +WS_PROTOCOLS: dict[WSProtocolType, str | None] = { + "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol", + "none": None, + "websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", + "wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol", +} +LIFESPAN: dict[LifespanType, str] = { + "auto": "uvicorn.lifespan.on:LifespanOn", + "on": "uvicorn.lifespan.on:LifespanOn", + "off": "uvicorn.lifespan.off:LifespanOff", +} +LOOP_SETUPS: dict[LoopSetupType, str | None] = { + "none": None, + "auto": "uvicorn.loops.auto:auto_loop_setup", + "asyncio": "uvicorn.loops.asyncio:asyncio_setup", + "uvloop": "uvicorn.loops.uvloop:uvloop_setup", +} +INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"] + +SSL_PROTOCOL_VERSION: int = ssl.PROTOCOL_TLS_SERVER + +LOGGING_CONFIG: dict[str, Any] = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "%(levelprefix)s %(message)s", + "use_colors": None, + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501 + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + "access": { + "formatter": "access", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False}, + "uvicorn.error": {"level": "INFO"}, + "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, + }, +} + +logger = logging.getLogger("uvicorn.error") + + +def create_ssl_context( + certfile: str | os.PathLike[str], + keyfile: str | os.PathLike[str] | None, + password: str | None, + ssl_version: int, + cert_reqs: int, + ca_certs: str | os.PathLike[str] | None, + ciphers: str | None, +) -> ssl.SSLContext: + ctx = ssl.SSLContext(ssl_version) + get_password = (lambda: password) if password else None + ctx.load_cert_chain(certfile, keyfile, get_password) + ctx.verify_mode = ssl.VerifyMode(cert_reqs) + if ca_certs: + ctx.load_verify_locations(ca_certs) + if ciphers: + ctx.set_ciphers(ciphers) + return ctx + + +def is_dir(path: Path) -> bool: + try: + if not path.is_absolute(): + path = path.resolve() + return path.is_dir() + except OSError: # pragma: full coverage + return False + + +def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str]) -> tuple[list[str], list[Path]]: + directories: list[Path] = list(set(map(Path, directories_list.copy()))) + patterns: list[str] = patterns_list.copy() + + current_working_directory = Path.cwd() + for pattern in patterns_list: + # Special case for the .* pattern, otherwise this would only match + # hidden directories which is probably undesired + if pattern == ".*": + continue # pragma: py-not-linux + patterns.append(pattern) + if is_dir(Path(pattern)): + directories.append(Path(pattern)) + else: + for match in current_working_directory.glob(pattern): + if is_dir(match): + directories.append(match) + + directories = list(set(directories)) + directories = list(map(Path, directories)) + directories = list(map(lambda x: x.resolve(), directories)) + directories = list({reload_path for reload_path in directories if is_dir(reload_path)}) + + children = [] + for j in range(len(directories)): + for k in range(j + 1, len(directories)): # pragma: full coverage + if directories[j] in directories[k].parents: + children.append(directories[k]) + elif directories[k] in directories[j].parents: + children.append(directories[j]) + + directories = list(set(directories).difference(set(children))) + + return list(set(patterns)), directories + + +def _normalize_dirs(dirs: list[str] | str | None) -> list[str]: + if dirs is None: + return [] + if isinstance(dirs, str): + return [dirs] + return list(set(dirs)) + + +class Config: + def __init__( + self, + app: ASGIApplication | Callable[..., Any] | str, + host: str = "127.0.0.1", + port: int = 8000, + uds: str | None = None, + fd: int | None = None, + loop: LoopSetupType = "auto", + http: type[asyncio.Protocol] | HTTPProtocolType = "auto", + ws: type[asyncio.Protocol] | WSProtocolType = "auto", + ws_max_size: int = 16 * 1024 * 1024, + ws_max_queue: int = 32, + ws_ping_interval: float | None = 20.0, + ws_ping_timeout: float | None = 20.0, + ws_per_message_deflate: bool = True, + lifespan: LifespanType = "auto", + env_file: str | os.PathLike[str] | None = None, + log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG, + log_level: str | int | None = None, + access_log: bool = True, + use_colors: bool | None = None, + interface: InterfaceType = "auto", + reload: bool = False, + reload_dirs: list[str] | str | None = None, + reload_delay: float = 0.25, + reload_includes: list[str] | str | None = None, + reload_excludes: list[str] | str | None = None, + workers: int | None = None, + proxy_headers: bool = True, + server_header: bool = True, + date_header: bool = True, + forwarded_allow_ips: list[str] | str | None = None, + root_path: str = "", + limit_concurrency: int | None = None, + limit_max_requests: int | None = None, + backlog: int = 2048, + timeout_keep_alive: int = 5, + timeout_notify: int = 30, + timeout_graceful_shutdown: int | None = None, + callback_notify: Callable[..., Awaitable[None]] | None = None, + ssl_keyfile: str | os.PathLike[str] | None = None, + ssl_certfile: str | os.PathLike[str] | None = None, + ssl_keyfile_password: str | None = None, + ssl_version: int = SSL_PROTOCOL_VERSION, + ssl_cert_reqs: int = ssl.CERT_NONE, + ssl_ca_certs: str | None = None, + ssl_ciphers: str = "TLSv1", + headers: list[tuple[str, str]] | None = None, + factory: bool = False, + h11_max_incomplete_event_size: int | None = None, + ): + self.app = app + self.host = host + self.port = port + self.uds = uds + self.fd = fd + self.loop = loop + self.http = http + self.ws = ws + self.ws_max_size = ws_max_size + self.ws_max_queue = ws_max_queue + self.ws_ping_interval = ws_ping_interval + self.ws_ping_timeout = ws_ping_timeout + self.ws_per_message_deflate = ws_per_message_deflate + self.lifespan = lifespan + self.log_config = log_config + self.log_level = log_level + self.access_log = access_log + self.use_colors = use_colors + self.interface = interface + self.reload = reload + self.reload_delay = reload_delay + self.workers = workers or 1 + self.proxy_headers = proxy_headers + self.server_header = server_header + self.date_header = date_header + self.root_path = root_path + self.limit_concurrency = limit_concurrency + self.limit_max_requests = limit_max_requests + self.backlog = backlog + self.timeout_keep_alive = timeout_keep_alive + self.timeout_notify = timeout_notify + self.timeout_graceful_shutdown = timeout_graceful_shutdown + self.callback_notify = callback_notify + self.ssl_keyfile = ssl_keyfile + self.ssl_certfile = ssl_certfile + self.ssl_keyfile_password = ssl_keyfile_password + self.ssl_version = ssl_version + self.ssl_cert_reqs = ssl_cert_reqs + self.ssl_ca_certs = ssl_ca_certs + self.ssl_ciphers = ssl_ciphers + self.headers: list[tuple[str, str]] = headers or [] + self.encoded_headers: list[tuple[bytes, bytes]] = [] + self.factory = factory + self.h11_max_incomplete_event_size = h11_max_incomplete_event_size + + self.loaded = False + self.configure_logging() + + self.reload_dirs: list[Path] = [] + self.reload_dirs_excludes: list[Path] = [] + self.reload_includes: list[str] = [] + self.reload_excludes: list[str] = [] + + if (reload_dirs or reload_includes or reload_excludes) and not self.should_reload: + logger.warning( + "Current configuration will not reload as not all conditions are met, please refer to documentation." + ) + + if self.should_reload: + reload_dirs = _normalize_dirs(reload_dirs) + reload_includes = _normalize_dirs(reload_includes) + reload_excludes = _normalize_dirs(reload_excludes) + + self.reload_includes, self.reload_dirs = resolve_reload_patterns(reload_includes, reload_dirs) + + self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(reload_excludes, []) + + reload_dirs_tmp = self.reload_dirs.copy() + + for directory in self.reload_dirs_excludes: + for reload_directory in reload_dirs_tmp: + if directory == reload_directory or directory in reload_directory.parents: + try: + self.reload_dirs.remove(reload_directory) + except ValueError: # pragma: full coverage + pass + + for pattern in self.reload_excludes: + if pattern in self.reload_includes: + self.reload_includes.remove(pattern) # pragma: full coverage + + if not self.reload_dirs: + if reload_dirs: + logger.warning( + "Provided reload directories %s did not contain valid " + + "directories, watching current working directory.", + reload_dirs, + ) + self.reload_dirs = [Path(os.getcwd())] + + logger.info( + "Will watch for changes in these directories: %s", + sorted(list(map(str, self.reload_dirs))), + ) + + if env_file is not None: + from dotenv import load_dotenv + + logger.info("Loading environment from '%s'", env_file) + load_dotenv(dotenv_path=env_file) + + if workers is None and "WEB_CONCURRENCY" in os.environ: + self.workers = int(os.environ["WEB_CONCURRENCY"]) + + self.forwarded_allow_ips: list[str] | str + if forwarded_allow_ips is None: + self.forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1") + else: + self.forwarded_allow_ips = forwarded_allow_ips # pragma: full coverage + + if self.reload and self.workers > 1: + logger.warning('"workers" flag is ignored when reloading is enabled.') + + @property + def asgi_version(self) -> Literal["2.0", "3.0"]: + mapping: dict[str, Literal["2.0", "3.0"]] = { + "asgi2": "2.0", + "asgi3": "3.0", + "wsgi": "3.0", + } + return mapping[self.interface] + + @property + def is_ssl(self) -> bool: + return bool(self.ssl_keyfile or self.ssl_certfile) + + @property + def use_subprocess(self) -> bool: + return bool(self.reload or self.workers > 1) + + def configure_logging(self) -> None: + logging.addLevelName(TRACE_LOG_LEVEL, "TRACE") + + if self.log_config is not None: + if isinstance(self.log_config, dict): + if self.use_colors in (True, False): + self.log_config["formatters"]["default"]["use_colors"] = self.use_colors + self.log_config["formatters"]["access"]["use_colors"] = self.use_colors + logging.config.dictConfig(self.log_config) + elif isinstance(self.log_config, str) and self.log_config.endswith(".json"): + with open(self.log_config) as file: + loaded_config = json.load(file) + logging.config.dictConfig(loaded_config) + elif isinstance(self.log_config, str) and self.log_config.endswith((".yaml", ".yml")): + # Install the PyYAML package or the uvicorn[standard] optional + # dependencies to enable this functionality. + import yaml + + with open(self.log_config) as file: + loaded_config = yaml.safe_load(file) + logging.config.dictConfig(loaded_config) + else: + # See the note about fileConfig() here: + # https://docs.python.org/3/library/logging.config.html#configuration-file-format + logging.config.fileConfig(self.log_config, disable_existing_loggers=False) + + if self.log_level is not None: + if isinstance(self.log_level, str): + log_level = LOG_LEVELS[self.log_level] + else: + log_level = self.log_level + logging.getLogger("uvicorn.error").setLevel(log_level) + logging.getLogger("uvicorn.access").setLevel(log_level) + logging.getLogger("uvicorn.asgi").setLevel(log_level) + if self.access_log is False: + logging.getLogger("uvicorn.access").handlers = [] + logging.getLogger("uvicorn.access").propagate = False + + def load(self) -> None: + assert not self.loaded + + if self.is_ssl: + assert self.ssl_certfile + self.ssl: ssl.SSLContext | None = create_ssl_context( + keyfile=self.ssl_keyfile, + certfile=self.ssl_certfile, + password=self.ssl_keyfile_password, + ssl_version=self.ssl_version, + cert_reqs=self.ssl_cert_reqs, + ca_certs=self.ssl_ca_certs, + ciphers=self.ssl_ciphers, + ) + else: + self.ssl = None + + encoded_headers = [(key.lower().encode("latin1"), value.encode("latin1")) for key, value in self.headers] + self.encoded_headers = ( + [(b"server", b"uvicorn")] + encoded_headers + if b"server" not in dict(encoded_headers) and self.server_header + else encoded_headers + ) + + if isinstance(self.http, str): + http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http]) + self.http_protocol_class: type[asyncio.Protocol] = http_protocol_class + else: + self.http_protocol_class = self.http + + if isinstance(self.ws, str): + ws_protocol_class = import_from_string(WS_PROTOCOLS[self.ws]) + self.ws_protocol_class: type[asyncio.Protocol] | None = ws_protocol_class + else: + self.ws_protocol_class = self.ws + + self.lifespan_class = import_from_string(LIFESPAN[self.lifespan]) + + try: + self.loaded_app = import_from_string(self.app) + except ImportFromStringError as exc: + logger.error("Error loading ASGI app. %s" % exc) + sys.exit(1) + + try: + self.loaded_app = self.loaded_app() + except TypeError as exc: + if self.factory: + logger.error("Error loading ASGI app factory: %s", exc) + sys.exit(1) + else: + if not self.factory: + logger.warning( + "ASGI app factory detected. Using it, but please consider setting the --factory flag explicitly." + ) + + if self.interface == "auto": + if inspect.isclass(self.loaded_app): + use_asgi_3 = hasattr(self.loaded_app, "__await__") + elif inspect.isfunction(self.loaded_app): + use_asgi_3 = asyncio.iscoroutinefunction(self.loaded_app) + else: + call = getattr(self.loaded_app, "__call__", None) + use_asgi_3 = asyncio.iscoroutinefunction(call) + self.interface = "asgi3" if use_asgi_3 else "asgi2" + + if self.interface == "wsgi": + self.loaded_app = WSGIMiddleware(self.loaded_app) + self.ws_protocol_class = None + elif self.interface == "asgi2": + self.loaded_app = ASGI2Middleware(self.loaded_app) + + if logger.getEffectiveLevel() <= TRACE_LOG_LEVEL: + self.loaded_app = MessageLoggerMiddleware(self.loaded_app) + if self.proxy_headers: + self.loaded_app = ProxyHeadersMiddleware(self.loaded_app, trusted_hosts=self.forwarded_allow_ips) + + self.loaded = True + + def setup_event_loop(self) -> None: + loop_setup: Callable | None = import_from_string(LOOP_SETUPS[self.loop]) + if loop_setup is not None: + loop_setup(use_subprocess=self.use_subprocess) + + def bind_socket(self) -> socket.socket: + logger_args: list[str | int] + if self.uds: # pragma: py-win32 + path = self.uds + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.bind(path) + uds_perms = 0o666 + os.chmod(self.uds, uds_perms) + except OSError as exc: # pragma: full coverage + logger.error(exc) + sys.exit(1) + + message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)" + sock_name_format = "%s" + color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)" + logger_args = [self.uds] + elif self.fd: # pragma: py-win32 + sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM) + message = "Uvicorn running on socket %s (Press CTRL+C to quit)" + fd_name_format = "%s" + color_message = "Uvicorn running on " + click.style(fd_name_format, bold=True) + " (Press CTRL+C to quit)" + logger_args = [sock.getsockname()] + else: + family = socket.AF_INET + addr_format = "%s://%s:%d" + + if self.host and ":" in self.host: # pragma: full coverage + # It's an IPv6 address. + family = socket.AF_INET6 + addr_format = "%s://[%s]:%d" + + sock = socket.socket(family=family) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind((self.host, self.port)) + except OSError as exc: # pragma: full coverage + logger.error(exc) + sys.exit(1) + + message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" + color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)" + protocol_name = "https" if self.is_ssl else "http" + logger_args = [protocol_name, self.host, sock.getsockname()[1]] + logger.info(message, *logger_args, extra={"color_message": color_message}) + sock.set_inheritable(True) + return sock + + @property + def should_reload(self) -> bool: + return isinstance(self.app, str) and self.reload diff --git a/lib/python3.12/site-packages/uvicorn/importer.py b/lib/python3.12/site-packages/uvicorn/importer.py new file mode 100644 index 0000000000000000000000000000000000000000..f77520ee106deeedee30a48005479806ff9a19e2 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/importer.py @@ -0,0 +1,34 @@ +import importlib +from typing import Any + + +class ImportFromStringError(Exception): + pass + + +def import_from_string(import_str: Any) -> Any: + if not isinstance(import_str, str): + return import_str + + module_str, _, attrs_str = import_str.partition(":") + if not module_str or not attrs_str: + message = 'Import string "{import_str}" must be in format ":".' + raise ImportFromStringError(message.format(import_str=import_str)) + + try: + module = importlib.import_module(module_str) + except ModuleNotFoundError as exc: + if exc.name != module_str: + raise exc from None + message = 'Could not import module "{module_str}".' + raise ImportFromStringError(message.format(module_str=module_str)) + + instance = module + try: + for attr_str in attrs_str.split("."): + instance = getattr(instance, attr_str) + except AttributeError: + message = 'Attribute "{attrs_str}" not found in module "{module_str}".' + raise ImportFromStringError(message.format(attrs_str=attrs_str, module_str=module_str)) + + return instance diff --git a/lib/python3.12/site-packages/uvicorn/lifespan/__init__.py b/lib/python3.12/site-packages/uvicorn/lifespan/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac4ca441ccf655490d3e0ae54b89c3d598e23fff Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/off.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/off.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24e7c170de77fa4992e841a1ae30827400e32aa8 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/off.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/on.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/on.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0741d0b6f852449273bff4c7d913df38caca4d95 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/lifespan/__pycache__/on.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/lifespan/off.py b/lib/python3.12/site-packages/uvicorn/lifespan/off.py new file mode 100644 index 0000000000000000000000000000000000000000..74554b1e2a149c37131168fbe283f3e2476a8f75 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/lifespan/off.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +from uvicorn import Config + + +class LifespanOff: + def __init__(self, config: Config) -> None: + self.should_exit = False + self.state: dict[str, Any] = {} + + async def startup(self) -> None: + pass + + async def shutdown(self) -> None: + pass diff --git a/lib/python3.12/site-packages/uvicorn/lifespan/on.py b/lib/python3.12/site-packages/uvicorn/lifespan/on.py new file mode 100644 index 0000000000000000000000000000000000000000..09df984ea1b184c0af9666c98d0709364de024c5 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/lifespan/on.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import asyncio +import logging +from asyncio import Queue +from typing import Any, Union + +from uvicorn import Config +from uvicorn._types import ( + LifespanScope, + LifespanShutdownCompleteEvent, + LifespanShutdownEvent, + LifespanShutdownFailedEvent, + LifespanStartupCompleteEvent, + LifespanStartupEvent, + LifespanStartupFailedEvent, +) + +LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent] +LifespanSendMessage = Union[ + LifespanStartupFailedEvent, + LifespanShutdownFailedEvent, + LifespanStartupCompleteEvent, + LifespanShutdownCompleteEvent, +] + + +STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol." + + +class LifespanOn: + def __init__(self, config: Config) -> None: + if not config.loaded: + config.load() + + self.config = config + self.logger = logging.getLogger("uvicorn.error") + self.startup_event = asyncio.Event() + self.shutdown_event = asyncio.Event() + self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue() + self.error_occured = False + self.startup_failed = False + self.shutdown_failed = False + self.should_exit = False + self.state: dict[str, Any] = {} + + async def startup(self) -> None: + self.logger.info("Waiting for application startup.") + + loop = asyncio.get_event_loop() + main_lifespan_task = loop.create_task(self.main()) # noqa: F841 + # Keep a hard reference to prevent garbage collection + # See https://github.com/encode/uvicorn/pull/972 + startup_event: LifespanStartupEvent = {"type": "lifespan.startup"} + await self.receive_queue.put(startup_event) + await self.startup_event.wait() + + if self.startup_failed or (self.error_occured and self.config.lifespan == "on"): + self.logger.error("Application startup failed. Exiting.") + self.should_exit = True + else: + self.logger.info("Application startup complete.") + + async def shutdown(self) -> None: + if self.error_occured: + return + self.logger.info("Waiting for application shutdown.") + shutdown_event: LifespanShutdownEvent = {"type": "lifespan.shutdown"} + await self.receive_queue.put(shutdown_event) + await self.shutdown_event.wait() + + if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"): + self.logger.error("Application shutdown failed. Exiting.") + self.should_exit = True + else: + self.logger.info("Application shutdown complete.") + + async def main(self) -> None: + try: + app = self.config.loaded_app + scope: LifespanScope = { + "type": "lifespan", + "asgi": {"version": self.config.asgi_version, "spec_version": "2.0"}, + "state": self.state, + } + await app(scope, self.receive, self.send) + except BaseException as exc: + self.asgi = None + self.error_occured = True + if self.startup_failed or self.shutdown_failed: + return + if self.config.lifespan == "auto": + msg = "ASGI 'lifespan' protocol appears unsupported." + self.logger.info(msg) + else: + msg = "Exception in 'lifespan' protocol\n" + self.logger.error(msg, exc_info=exc) + finally: + self.startup_event.set() + self.shutdown_event.set() + + async def send(self, message: LifespanSendMessage) -> None: + assert message["type"] in ( + "lifespan.startup.complete", + "lifespan.startup.failed", + "lifespan.shutdown.complete", + "lifespan.shutdown.failed", + ) + + if message["type"] == "lifespan.startup.complete": + assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.startup_event.set() + + elif message["type"] == "lifespan.startup.failed": + assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.startup_event.set() + self.startup_failed = True + if message.get("message"): + self.logger.error(message["message"]) + + elif message["type"] == "lifespan.shutdown.complete": + assert self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.shutdown_event.set() + + elif message["type"] == "lifespan.shutdown.failed": + assert self.startup_event.is_set(), STATE_TRANSITION_ERROR + assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR + self.shutdown_event.set() + self.shutdown_failed = True + if message.get("message"): + self.logger.error(message["message"]) + + async def receive(self) -> LifespanReceiveMessage: + return await self.receive_queue.get() diff --git a/lib/python3.12/site-packages/uvicorn/logging.py b/lib/python3.12/site-packages/uvicorn/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..02f455d8eefd53ef88da5424315589610bf7edd7 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/logging.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import http +import logging +import sys +from copy import copy +from typing import Literal + +import click + +TRACE_LOG_LEVEL = 5 + + +class ColourizedFormatter(logging.Formatter): + """ + A custom log formatter class that: + + * Outputs the LOG_LEVEL with an appropriate color. + * If a log call includes an `extra={"color_message": ...}` it will be used + for formatting the output, instead of the plain text message. + """ + + level_name_colors = { + TRACE_LOG_LEVEL: lambda level_name: click.style(str(level_name), fg="blue"), + logging.DEBUG: lambda level_name: click.style(str(level_name), fg="cyan"), + logging.INFO: lambda level_name: click.style(str(level_name), fg="green"), + logging.WARNING: lambda level_name: click.style(str(level_name), fg="yellow"), + logging.ERROR: lambda level_name: click.style(str(level_name), fg="red"), + logging.CRITICAL: lambda level_name: click.style(str(level_name), fg="bright_red"), + } + + def __init__( + self, + fmt: str | None = None, + datefmt: str | None = None, + style: Literal["%", "{", "$"] = "%", + use_colors: bool | None = None, + ): + if use_colors in (True, False): + self.use_colors = use_colors + else: + self.use_colors = sys.stdout.isatty() + super().__init__(fmt=fmt, datefmt=datefmt, style=style) + + def color_level_name(self, level_name: str, level_no: int) -> str: + def default(level_name: str) -> str: + return str(level_name) # pragma: no cover + + func = self.level_name_colors.get(level_no, default) + return func(level_name) + + def should_use_colors(self) -> bool: + return True # pragma: no cover + + def formatMessage(self, record: logging.LogRecord) -> str: + recordcopy = copy(record) + levelname = recordcopy.levelname + seperator = " " * (8 - len(recordcopy.levelname)) + if self.use_colors: + levelname = self.color_level_name(levelname, recordcopy.levelno) + if "color_message" in recordcopy.__dict__: + recordcopy.msg = recordcopy.__dict__["color_message"] + recordcopy.__dict__["message"] = recordcopy.getMessage() + recordcopy.__dict__["levelprefix"] = levelname + ":" + seperator + return super().formatMessage(recordcopy) + + +class DefaultFormatter(ColourizedFormatter): + def should_use_colors(self) -> bool: + return sys.stderr.isatty() # pragma: no cover + + +class AccessFormatter(ColourizedFormatter): + status_code_colours = { + 1: lambda code: click.style(str(code), fg="bright_white"), + 2: lambda code: click.style(str(code), fg="green"), + 3: lambda code: click.style(str(code), fg="yellow"), + 4: lambda code: click.style(str(code), fg="red"), + 5: lambda code: click.style(str(code), fg="bright_red"), + } + + def get_status_code(self, status_code: int) -> str: + try: + status_phrase = http.HTTPStatus(status_code).phrase + except ValueError: + status_phrase = "" + status_and_phrase = f"{status_code} {status_phrase}" + if self.use_colors: + + def default(code: int) -> str: + return status_and_phrase # pragma: no cover + + func = self.status_code_colours.get(status_code // 100, default) + return func(status_and_phrase) + return status_and_phrase + + def formatMessage(self, record: logging.LogRecord) -> str: + recordcopy = copy(record) + ( + client_addr, + method, + full_path, + http_version, + status_code, + ) = recordcopy.args # type: ignore[misc] + status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type] + request_line = f"{method} {full_path} HTTP/{http_version}" + if self.use_colors: + request_line = click.style(request_line, bold=True) + recordcopy.__dict__.update( + { + "client_addr": client_addr, + "request_line": request_line, + "status_code": status_code, + } + ) + return super().formatMessage(recordcopy) diff --git a/lib/python3.12/site-packages/uvicorn/loops/__init__.py b/lib/python3.12/site-packages/uvicorn/loops/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/uvicorn/loops/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..949d46d98c2d2e26033ccd20d2f1375806fb99ce Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/loops/__pycache__/asyncio.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/asyncio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74fda8e51610989aa488935e95e22b1e73c3f070 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/asyncio.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/loops/__pycache__/auto.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/auto.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c7e0cb5b300014249d454b74ad2485975b4e824 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/auto.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/loops/__pycache__/uvloop.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/uvloop.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae302075955fefe0711c4046a7f0c149a4b8908b Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/loops/__pycache__/uvloop.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/loops/asyncio.py b/lib/python3.12/site-packages/uvicorn/loops/asyncio.py new file mode 100644 index 0000000000000000000000000000000000000000..1bead4a061cc0989ffcd999ffeb0a7279ec25b4f --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/loops/asyncio.py @@ -0,0 +1,10 @@ +import asyncio +import logging +import sys + +logger = logging.getLogger("uvicorn.error") + + +def asyncio_setup(use_subprocess: bool = False) -> None: + if sys.platform == "win32" and use_subprocess: + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # pragma: full coverage diff --git a/lib/python3.12/site-packages/uvicorn/loops/auto.py b/lib/python3.12/site-packages/uvicorn/loops/auto.py new file mode 100644 index 0000000000000000000000000000000000000000..2285457bf181d31d36b74ba6c368eac604178645 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/loops/auto.py @@ -0,0 +1,11 @@ +def auto_loop_setup(use_subprocess: bool = False) -> None: + try: + import uvloop # noqa + except ImportError: # pragma: no cover + from uvicorn.loops.asyncio import asyncio_setup as loop_setup + + loop_setup(use_subprocess=use_subprocess) + else: # pragma: no cover + from uvicorn.loops.uvloop import uvloop_setup + + uvloop_setup(use_subprocess=use_subprocess) diff --git a/lib/python3.12/site-packages/uvicorn/loops/uvloop.py b/lib/python3.12/site-packages/uvicorn/loops/uvloop.py new file mode 100644 index 0000000000000000000000000000000000000000..0e2fd1eb05efc526f660bcf10e865d2fe781a239 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/loops/uvloop.py @@ -0,0 +1,7 @@ +import asyncio + +import uvloop + + +def uvloop_setup(use_subprocess: bool = False) -> None: + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) diff --git a/lib/python3.12/site-packages/uvicorn/main.py b/lib/python3.12/site-packages/uvicorn/main.py new file mode 100644 index 0000000000000000000000000000000000000000..afa4be0f168224e559b601558feea913aca77a97 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/main.py @@ -0,0 +1,604 @@ +from __future__ import annotations + +import asyncio +import logging +import os +import platform +import ssl +import sys +import warnings +from configparser import RawConfigParser +from typing import IO, Any, Callable + +import click + +import uvicorn +from uvicorn._types import ASGIApplication +from uvicorn.config import ( + HTTP_PROTOCOLS, + INTERFACES, + LIFESPAN, + LOG_LEVELS, + LOGGING_CONFIG, + LOOP_SETUPS, + SSL_PROTOCOL_VERSION, + WS_PROTOCOLS, + Config, + HTTPProtocolType, + InterfaceType, + LifespanType, + LoopSetupType, + WSProtocolType, +) +from uvicorn.server import Server +from uvicorn.supervisors import ChangeReload, Multiprocess + +LEVEL_CHOICES = click.Choice(list(LOG_LEVELS.keys())) +HTTP_CHOICES = click.Choice(list(HTTP_PROTOCOLS.keys())) +WS_CHOICES = click.Choice(list(WS_PROTOCOLS.keys())) +LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys())) +LOOP_CHOICES = click.Choice([key for key in LOOP_SETUPS.keys() if key != "none"]) +INTERFACE_CHOICES = click.Choice(INTERFACES) + +STARTUP_FAILURE = 3 + +logger = logging.getLogger("uvicorn.error") + + +def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + click.echo( + "Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032 + version=uvicorn.__version__, + py_implementation=platform.python_implementation(), + py_version=platform.python_version(), + system=platform.system(), + ) + ) + ctx.exit() + + +@click.command(context_settings={"auto_envvar_prefix": "UVICORN"}) +@click.argument("app", envvar="UVICORN_APP") +@click.option( + "--host", + type=str, + default="127.0.0.1", + help="Bind socket to this host.", + show_default=True, +) +@click.option( + "--port", + type=int, + default=8000, + help="Bind socket to this port. If 0, an available port will be picked.", + show_default=True, +) +@click.option("--uds", type=str, default=None, help="Bind to a UNIX domain socket.") +@click.option("--fd", type=int, default=None, help="Bind to socket from this file descriptor.") +@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.") +@click.option( + "--reload-dir", + "reload_dirs", + multiple=True, + help="Set reload directories explicitly, instead of using the current working directory.", + type=click.Path(exists=True), +) +@click.option( + "--reload-include", + "reload_includes", + multiple=True, + help="Set glob patterns to include while watching for files. Includes '*.py' " + "by default; these defaults can be overridden with `--reload-exclude`. " + "This option has no effect unless watchfiles is installed.", +) +@click.option( + "--reload-exclude", + "reload_excludes", + multiple=True, + help="Set glob patterns to exclude while watching for files. Includes " + "'.*, .py[cod], .sw.*, ~*' by default; these defaults can be overridden " + "with `--reload-include`. This option has no effect unless watchfiles is " + "installed.", +) +@click.option( + "--reload-delay", + type=float, + default=0.25, + show_default=True, + help="Delay between previous and next check if application needs to be. Defaults to 0.25s.", +) +@click.option( + "--workers", + default=None, + type=int, + help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment" + " variable if available, or 1. Not valid with --reload.", +) +@click.option( + "--loop", + type=LOOP_CHOICES, + default="auto", + help="Event loop implementation.", + show_default=True, +) +@click.option( + "--http", + type=HTTP_CHOICES, + default="auto", + help="HTTP protocol implementation.", + show_default=True, +) +@click.option( + "--ws", + type=WS_CHOICES, + default="auto", + help="WebSocket protocol implementation.", + show_default=True, +) +@click.option( + "--ws-max-size", + type=int, + default=16777216, + help="WebSocket max size message in bytes", + show_default=True, +) +@click.option( + "--ws-max-queue", + type=int, + default=32, + help="The maximum length of the WebSocket message queue.", + show_default=True, +) +@click.option( + "--ws-ping-interval", + type=float, + default=20.0, + help="WebSocket ping interval in seconds.", + show_default=True, +) +@click.option( + "--ws-ping-timeout", + type=float, + default=20.0, + help="WebSocket ping timeout in seconds.", + show_default=True, +) +@click.option( + "--ws-per-message-deflate", + type=bool, + default=True, + help="WebSocket per-message-deflate compression", + show_default=True, +) +@click.option( + "--lifespan", + type=LIFESPAN_CHOICES, + default="auto", + help="Lifespan implementation.", + show_default=True, +) +@click.option( + "--interface", + type=INTERFACE_CHOICES, + default="auto", + help="Select ASGI3, ASGI2, or WSGI as the application interface.", + show_default=True, +) +@click.option( + "--env-file", + type=click.Path(exists=True), + default=None, + help="Environment configuration file.", + show_default=True, +) +@click.option( + "--log-config", + type=click.Path(exists=True), + default=None, + help="Logging configuration file. Supported formats: .ini, .json, .yaml.", + show_default=True, +) +@click.option( + "--log-level", + type=LEVEL_CHOICES, + default=None, + help="Log level. [default: info]", + show_default=True, +) +@click.option( + "--access-log/--no-access-log", + is_flag=True, + default=True, + help="Enable/Disable access log.", +) +@click.option( + "--use-colors/--no-use-colors", + is_flag=True, + default=None, + help="Enable/Disable colorized logging.", +) +@click.option( + "--proxy-headers/--no-proxy-headers", + is_flag=True, + default=True, + help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info.", +) +@click.option( + "--server-header/--no-server-header", + is_flag=True, + default=True, + help="Enable/Disable default Server header.", +) +@click.option( + "--date-header/--no-date-header", + is_flag=True, + default=True, + help="Enable/Disable default Date header.", +) +@click.option( + "--forwarded-allow-ips", + type=str, + default=None, + help="Comma separated list of IP Addresses, IP Networks, or literals " + "(e.g. UNIX Socket path) to trust with proxy headers. Defaults to the " + "$FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. " + "The literal '*' means trust everything.", +) +@click.option( + "--root-path", + type=str, + default="", + help="Set the ASGI 'root_path' for applications submounted below a given URL path.", +) +@click.option( + "--limit-concurrency", + type=int, + default=None, + help="Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses.", +) +@click.option( + "--backlog", + type=int, + default=2048, + help="Maximum number of connections to hold in backlog", +) +@click.option( + "--limit-max-requests", + type=int, + default=None, + help="Maximum number of requests to service before terminating the process.", +) +@click.option( + "--timeout-keep-alive", + type=int, + default=5, + help="Close Keep-Alive connections if no new data is received within this timeout.", + show_default=True, +) +@click.option( + "--timeout-graceful-shutdown", + type=int, + default=None, + help="Maximum number of seconds to wait for graceful shutdown.", +) +@click.option("--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True) +@click.option( + "--ssl-certfile", + type=str, + default=None, + help="SSL certificate file", + show_default=True, +) +@click.option( + "--ssl-keyfile-password", + type=str, + default=None, + help="SSL keyfile password", + show_default=True, +) +@click.option( + "--ssl-version", + type=int, + default=int(SSL_PROTOCOL_VERSION), + help="SSL version to use (see stdlib ssl module's)", + show_default=True, +) +@click.option( + "--ssl-cert-reqs", + type=int, + default=int(ssl.CERT_NONE), + help="Whether client certificate is required (see stdlib ssl module's)", + show_default=True, +) +@click.option( + "--ssl-ca-certs", + type=str, + default=None, + help="CA certificates file", + show_default=True, +) +@click.option( + "--ssl-ciphers", + type=str, + default="TLSv1", + help="Ciphers to use (see stdlib ssl module's)", + show_default=True, +) +@click.option( + "--header", + "headers", + multiple=True, + help="Specify custom default HTTP response headers as a Name:Value pair", +) +@click.option( + "--version", + is_flag=True, + callback=print_version, + expose_value=False, + is_eager=True, + help="Display the uvicorn version and exit.", +) +@click.option( + "--app-dir", + default="", + show_default=True, + help="Look for APP in the specified directory, by adding this to the PYTHONPATH." + " Defaults to the current working directory.", +) +@click.option( + "--h11-max-incomplete-event-size", + "h11_max_incomplete_event_size", + type=int, + default=None, + help="For h11, the maximum number of bytes to buffer of an incomplete event.", +) +@click.option( + "--factory", + is_flag=True, + default=False, + help="Treat APP as an application factory, i.e. a () -> callable.", + show_default=True, +) +def main( + app: str, + host: str, + port: int, + uds: str, + fd: int, + loop: LoopSetupType, + http: HTTPProtocolType, + ws: WSProtocolType, + ws_max_size: int, + ws_max_queue: int, + ws_ping_interval: float, + ws_ping_timeout: float, + ws_per_message_deflate: bool, + lifespan: LifespanType, + interface: InterfaceType, + reload: bool, + reload_dirs: list[str], + reload_includes: list[str], + reload_excludes: list[str], + reload_delay: float, + workers: int, + env_file: str, + log_config: str, + log_level: str, + access_log: bool, + proxy_headers: bool, + server_header: bool, + date_header: bool, + forwarded_allow_ips: str, + root_path: str, + limit_concurrency: int, + backlog: int, + limit_max_requests: int, + timeout_keep_alive: int, + timeout_graceful_shutdown: int | None, + ssl_keyfile: str, + ssl_certfile: str, + ssl_keyfile_password: str, + ssl_version: int, + ssl_cert_reqs: int, + ssl_ca_certs: str, + ssl_ciphers: str, + headers: list[str], + use_colors: bool, + app_dir: str, + h11_max_incomplete_event_size: int | None, + factory: bool, +) -> None: + run( + app, + host=host, + port=port, + uds=uds, + fd=fd, + loop=loop, + http=http, + ws=ws, + ws_max_size=ws_max_size, + ws_max_queue=ws_max_queue, + ws_ping_interval=ws_ping_interval, + ws_ping_timeout=ws_ping_timeout, + ws_per_message_deflate=ws_per_message_deflate, + lifespan=lifespan, + env_file=env_file, + log_config=LOGGING_CONFIG if log_config is None else log_config, + log_level=log_level, + access_log=access_log, + interface=interface, + reload=reload, + reload_dirs=reload_dirs or None, + reload_includes=reload_includes or None, + reload_excludes=reload_excludes or None, + reload_delay=reload_delay, + workers=workers, + proxy_headers=proxy_headers, + server_header=server_header, + date_header=date_header, + forwarded_allow_ips=forwarded_allow_ips, + root_path=root_path, + limit_concurrency=limit_concurrency, + backlog=backlog, + limit_max_requests=limit_max_requests, + timeout_keep_alive=timeout_keep_alive, + timeout_graceful_shutdown=timeout_graceful_shutdown, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + ssl_keyfile_password=ssl_keyfile_password, + ssl_version=ssl_version, + ssl_cert_reqs=ssl_cert_reqs, + ssl_ca_certs=ssl_ca_certs, + ssl_ciphers=ssl_ciphers, + headers=[header.split(":", 1) for header in headers], # type: ignore[misc] + use_colors=use_colors, + factory=factory, + app_dir=app_dir, + h11_max_incomplete_event_size=h11_max_incomplete_event_size, + ) + + +def run( + app: ASGIApplication | Callable[..., Any] | str, + *, + host: str = "127.0.0.1", + port: int = 8000, + uds: str | None = None, + fd: int | None = None, + loop: LoopSetupType = "auto", + http: type[asyncio.Protocol] | HTTPProtocolType = "auto", + ws: type[asyncio.Protocol] | WSProtocolType = "auto", + ws_max_size: int = 16777216, + ws_max_queue: int = 32, + ws_ping_interval: float | None = 20.0, + ws_ping_timeout: float | None = 20.0, + ws_per_message_deflate: bool = True, + lifespan: LifespanType = "auto", + interface: InterfaceType = "auto", + reload: bool = False, + reload_dirs: list[str] | str | None = None, + reload_includes: list[str] | str | None = None, + reload_excludes: list[str] | str | None = None, + reload_delay: float = 0.25, + workers: int | None = None, + env_file: str | os.PathLike[str] | None = None, + log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG, + log_level: str | int | None = None, + access_log: bool = True, + proxy_headers: bool = True, + server_header: bool = True, + date_header: bool = True, + forwarded_allow_ips: list[str] | str | None = None, + root_path: str = "", + limit_concurrency: int | None = None, + backlog: int = 2048, + limit_max_requests: int | None = None, + timeout_keep_alive: int = 5, + timeout_graceful_shutdown: int | None = None, + ssl_keyfile: str | os.PathLike[str] | None = None, + ssl_certfile: str | os.PathLike[str] | None = None, + ssl_keyfile_password: str | None = None, + ssl_version: int = SSL_PROTOCOL_VERSION, + ssl_cert_reqs: int = ssl.CERT_NONE, + ssl_ca_certs: str | None = None, + ssl_ciphers: str = "TLSv1", + headers: list[tuple[str, str]] | None = None, + use_colors: bool | None = None, + app_dir: str | None = None, + factory: bool = False, + h11_max_incomplete_event_size: int | None = None, +) -> None: + if app_dir is not None: + sys.path.insert(0, app_dir) + + config = Config( + app, + host=host, + port=port, + uds=uds, + fd=fd, + loop=loop, + http=http, + ws=ws, + ws_max_size=ws_max_size, + ws_max_queue=ws_max_queue, + ws_ping_interval=ws_ping_interval, + ws_ping_timeout=ws_ping_timeout, + ws_per_message_deflate=ws_per_message_deflate, + lifespan=lifespan, + interface=interface, + reload=reload, + reload_dirs=reload_dirs, + reload_includes=reload_includes, + reload_excludes=reload_excludes, + reload_delay=reload_delay, + workers=workers, + env_file=env_file, + log_config=log_config, + log_level=log_level, + access_log=access_log, + proxy_headers=proxy_headers, + server_header=server_header, + date_header=date_header, + forwarded_allow_ips=forwarded_allow_ips, + root_path=root_path, + limit_concurrency=limit_concurrency, + backlog=backlog, + limit_max_requests=limit_max_requests, + timeout_keep_alive=timeout_keep_alive, + timeout_graceful_shutdown=timeout_graceful_shutdown, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + ssl_keyfile_password=ssl_keyfile_password, + ssl_version=ssl_version, + ssl_cert_reqs=ssl_cert_reqs, + ssl_ca_certs=ssl_ca_certs, + ssl_ciphers=ssl_ciphers, + headers=headers, + use_colors=use_colors, + factory=factory, + h11_max_incomplete_event_size=h11_max_incomplete_event_size, + ) + server = Server(config=config) + + if (config.reload or config.workers > 1) and not isinstance(app, str): + logger = logging.getLogger("uvicorn.error") + logger.warning("You must pass the application as an import string to enable 'reload' or 'workers'.") + sys.exit(1) + + try: + if config.should_reload: + sock = config.bind_socket() + ChangeReload(config, target=server.run, sockets=[sock]).run() + elif config.workers > 1: + sock = config.bind_socket() + Multiprocess(config, target=server.run, sockets=[sock]).run() + else: + server.run() + except KeyboardInterrupt: + pass # pragma: full coverage + finally: + if config.uds and os.path.exists(config.uds): + os.remove(config.uds) # pragma: py-win32 + + if not server.started and not config.should_reload and config.workers == 1: + sys.exit(STARTUP_FAILURE) + + +def __getattr__(name: str) -> Any: + if name == "ServerState": + warnings.warn( + "uvicorn.main.ServerState is deprecated, use uvicorn.server.ServerState instead.", + DeprecationWarning, + ) + from uvicorn.server import ServerState + + return ServerState + raise AttributeError(f"module {__name__} has no attribute {name}") + + +if __name__ == "__main__": + main() # pragma: no cover diff --git a/lib/python3.12/site-packages/uvicorn/middleware/__init__.py b/lib/python3.12/site-packages/uvicorn/middleware/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e9a341b35e6c0c4206c00539ad04833e7d5726d Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/asgi2.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/asgi2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebc231850aa1e34c428bc1c64729a70a27397ba4 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/asgi2.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/message_logger.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/message_logger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e26f0b3e115520a8d84708c3de6d5c5c66e7cb5f Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/message_logger.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/proxy_headers.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/proxy_headers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65bd1754594679227816cba9946201fd62ade9ee Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/proxy_headers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/wsgi.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80930d47345091024eeb542c605a220cb1943568 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/middleware/__pycache__/wsgi.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/middleware/asgi2.py b/lib/python3.12/site-packages/uvicorn/middleware/asgi2.py new file mode 100644 index 0000000000000000000000000000000000000000..4e15d1599bab04ac38f56cf271d2ff9b1b9984c8 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/middleware/asgi2.py @@ -0,0 +1,15 @@ +from uvicorn._types import ( + ASGI2Application, + ASGIReceiveCallable, + ASGISendCallable, + Scope, +) + + +class ASGI2Middleware: + def __init__(self, app: "ASGI2Application"): + self.app = app + + async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None: + instance = self.app(scope) + await instance(receive, send) diff --git a/lib/python3.12/site-packages/uvicorn/middleware/message_logger.py b/lib/python3.12/site-packages/uvicorn/middleware/message_logger.py new file mode 100644 index 0000000000000000000000000000000000000000..0174bcce6d48de946214d59bcae5543d38d89c3a --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/middleware/message_logger.py @@ -0,0 +1,87 @@ +import logging +from typing import Any + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + WWWScope, +) +from uvicorn.logging import TRACE_LOG_LEVEL + +PLACEHOLDER_FORMAT = { + "body": "<{length} bytes>", + "bytes": "<{length} bytes>", + "text": "<{length} chars>", + "headers": "<...>", +} + + +def message_with_placeholders(message: Any) -> Any: + """ + Return an ASGI message, with any body-type content omitted and replaced + with a placeholder. + """ + new_message = message.copy() + for attr in PLACEHOLDER_FORMAT.keys(): + if message.get(attr) is not None: + content = message[attr] + placeholder = PLACEHOLDER_FORMAT[attr].format(length=len(content)) + new_message[attr] = placeholder + return new_message + + +class MessageLoggerMiddleware: + def __init__(self, app: "ASGI3Application"): + self.task_counter = 0 + self.app = app + self.logger = logging.getLogger("uvicorn.asgi") + + def trace(message: Any, *args: Any, **kwargs: Any) -> None: + self.logger.log(TRACE_LOG_LEVEL, message, *args, **kwargs) + + self.logger.trace = trace # type: ignore + + async def __call__( + self, + scope: "WWWScope", + receive: "ASGIReceiveCallable", + send: "ASGISendCallable", + ) -> None: + self.task_counter += 1 + + task_counter = self.task_counter + client = scope.get("client") + prefix = "%s:%d - ASGI" % (client[0], client[1]) if client else "ASGI" + + async def inner_receive() -> "ASGIReceiveEvent": + message = await receive() + logged_message = message_with_placeholders(message) + log_text = "%s [%d] Receive %s" + self.logger.trace( # type: ignore + log_text, prefix, task_counter, logged_message + ) + return message + + async def inner_send(message: "ASGISendEvent") -> None: + logged_message = message_with_placeholders(message) + log_text = "%s [%d] Send %s" + self.logger.trace( # type: ignore + log_text, prefix, task_counter, logged_message + ) + await send(message) + + logged_scope = message_with_placeholders(scope) + log_text = "%s [%d] Started scope=%s" + self.logger.trace(log_text, prefix, task_counter, logged_scope) # type: ignore + try: + await self.app(scope, inner_receive, inner_send) + except BaseException as exc: + log_text = "%s [%d] Raised exception" + self.logger.trace(log_text, prefix, task_counter) # type: ignore + raise exc from None + else: + log_text = "%s [%d] Completed" + self.logger.trace(log_text, prefix, task_counter) # type: ignore diff --git a/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py b/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py new file mode 100644 index 0000000000000000000000000000000000000000..7c3609de6707ee3707ef65902ac75879fbe05c86 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import ipaddress + +from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope + + +class ProxyHeadersMiddleware: + """Middleware for handling known proxy headers + + This middleware can be used when a known proxy is fronting the application, + and is trusted to be properly setting the `X-Forwarded-Proto` and + `X-Forwarded-For` headers with the connecting client information. + + Modifies the `client` and `scheme` information so that they reference + the connecting client, rather that the connecting proxy. + + References: + - + - + """ + + def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None: + self.app = app + self.trusted_hosts = _TrustedHosts(trusted_hosts) + + async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + if scope["type"] == "lifespan": + return await self.app(scope, receive, send) + + client_addr = scope.get("client") + client_host = client_addr[0] if client_addr else None + + if client_host in self.trusted_hosts: + headers = dict(scope["headers"]) + + if b"x-forwarded-proto" in headers: + x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip() + + if x_forwarded_proto in {"http", "https", "ws", "wss"}: + if scope["type"] == "websocket": + scope["scheme"] = x_forwarded_proto.replace("http", "ws") + else: + scope["scheme"] = x_forwarded_proto + + if b"x-forwarded-for" in headers: + x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1") + host = self.trusted_hosts.get_trusted_client_host(x_forwarded_for) + + if host: + # If the x-forwarded-for header is empty then host is an empty string. + # Only set the client if we actually got something usable. + # See: https://github.com/encode/uvicorn/issues/1068 + + # We've lost the connecting client's port information by now, + # so only include the host. + port = 0 + scope["client"] = (host, port) + + return await self.app(scope, receive, send) + + +def _parse_raw_hosts(value: str) -> list[str]: + return [item.strip() for item in value.split(",")] + + +class _TrustedHosts: + """Container for trusted hosts and networks""" + + def __init__(self, trusted_hosts: list[str] | str) -> None: + self.always_trust: bool = trusted_hosts in ("*", ["*"]) + + self.trusted_literals: set[str] = set() + self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set() + self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set() + + # Notes: + # - We separate hosts from literals as there are many ways to write + # an IPv6 Address so we need to compare by object. + # - We don't convert IP Address to single host networks (e.g. /32 / 128) as + # it more efficient to do an address lookup in a set than check for + # membership in each network. + # - We still allow literals as it might be possible that we receive a + # something that isn't an IP Address e.g. a unix socket. + + if not self.always_trust: + if isinstance(trusted_hosts, str): + trusted_hosts = _parse_raw_hosts(trusted_hosts) + + for host in trusted_hosts: + # Note: because we always convert invalid IP types to literals it + # is not possible for the user to know they provided a malformed IP + # type - this may lead to unexpected / difficult to debug behaviour. + + if "/" in host: + # Looks like a network + try: + self.trusted_networks.add(ipaddress.ip_network(host)) + except ValueError: + # Was not a valid IP Network + self.trusted_literals.add(host) + else: + try: + self.trusted_hosts.add(ipaddress.ip_address(host)) + except ValueError: + # Was not a valid IP Address + self.trusted_literals.add(host) + + def __contains__(self, host: str | None) -> bool: + if self.always_trust: + return True + + if not host: + return False + + try: + ip = ipaddress.ip_address(host) + if ip in self.trusted_hosts: + return True + return any(ip in net for net in self.trusted_networks) + + except ValueError: + return host in self.trusted_literals + + def get_trusted_client_host(self, x_forwarded_for: str) -> str: + """Extract the client host from x_forwarded_for header + + In general this is the first "untrusted" host in the forwarded for list. + """ + x_forwarded_for_hosts = _parse_raw_hosts(x_forwarded_for) + + if self.always_trust: + return x_forwarded_for_hosts[0] + + # Note: each proxy appends to the header list so check it in reverse order + for host in reversed(x_forwarded_for_hosts): + if host not in self: + return host + + # All hosts are trusted meaning that the client was also a trusted proxy + # See https://github.com/encode/uvicorn/issues/1068#issuecomment-855371576 + return x_forwarded_for_hosts[0] diff --git a/lib/python3.12/site-packages/uvicorn/middleware/wsgi.py b/lib/python3.12/site-packages/uvicorn/middleware/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..d53a8b06d933ef4bbc43b4149d0ebe61b7433eb1 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/middleware/wsgi.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import asyncio +import concurrent.futures +import io +import sys +import warnings +from collections import deque +from collections.abc import Iterable + +from uvicorn._types import ( + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + Environ, + ExcInfo, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, + StartResponse, + WSGIApp, +) + + +def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO) -> Environ: + """ + Builds a scope and request message into a WSGI environ object. + """ + script_name = scope.get("root_path", "").encode("utf8").decode("latin1") + path_info = scope["path"].encode("utf8").decode("latin1") + if path_info.startswith(script_name): + path_info = path_info[len(script_name) :] + environ = { + "REQUEST_METHOD": scope["method"], + "SCRIPT_NAME": script_name, + "PATH_INFO": path_info, + "QUERY_STRING": scope["query_string"].decode("ascii"), + "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], + "wsgi.version": (1, 0), + "wsgi.url_scheme": scope.get("scheme", "http"), + "wsgi.input": body, + "wsgi.errors": sys.stdout, + "wsgi.multithread": True, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + } + + # Get server name and port - required in WSGI, not in ASGI + server = scope.get("server") + if server is None: + server = ("localhost", 80) + environ["SERVER_NAME"] = server[0] + environ["SERVER_PORT"] = server[1] + + # Get client IP address + client = scope.get("client") + if client is not None: + environ["REMOTE_ADDR"] = client[0] + + # Go through headers and make them into environ entries + for name, value in scope.get("headers", []): + name_str: str = name.decode("latin1") + if name_str == "content-length": + corrected_name = "CONTENT_LENGTH" + elif name_str == "content-type": + corrected_name = "CONTENT_TYPE" + else: + corrected_name = "HTTP_%s" % name_str.upper().replace("-", "_") + # HTTPbis say only ASCII chars are allowed in headers, but we latin1 + # just in case + value_str: str = value.decode("latin1") + if corrected_name in environ: + corrected_name_environ = environ[corrected_name] + assert isinstance(corrected_name_environ, str) + value_str = corrected_name_environ + "," + value_str + environ[corrected_name] = value_str + return environ + + +class _WSGIMiddleware: + def __init__(self, app: WSGIApp, workers: int = 10): + warnings.warn( + "Uvicorn's native WSGI implementation is deprecated, you should switch to a2wsgi (`pip install a2wsgi`).", + DeprecationWarning, + ) + self.app = app + self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=workers) + + async def __call__( + self, + scope: HTTPScope, + receive: ASGIReceiveCallable, + send: ASGISendCallable, + ) -> None: + assert scope["type"] == "http" + instance = WSGIResponder(self.app, self.executor, scope) + await instance(receive, send) + + +class WSGIResponder: + def __init__( + self, + app: WSGIApp, + executor: concurrent.futures.ThreadPoolExecutor, + scope: HTTPScope, + ): + self.app = app + self.executor = executor + self.scope = scope + self.status = None + self.response_headers = None + self.send_event = asyncio.Event() + self.send_queue: deque[ASGISendEvent | None] = deque() + self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() + self.response_started = False + self.exc_info: ExcInfo | None = None + + async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + message: HTTPRequestEvent = await receive() # type: ignore[assignment] + body = io.BytesIO(message.get("body", b"")) + more_body = message.get("more_body", False) + if more_body: + body.seek(0, io.SEEK_END) + while more_body: + body_message: HTTPRequestEvent = ( + await receive() # type: ignore[assignment] + ) + body.write(body_message.get("body", b"")) + more_body = body_message.get("more_body", False) + body.seek(0) + environ = build_environ(self.scope, message, body) + self.loop = asyncio.get_event_loop() + wsgi = self.loop.run_in_executor(self.executor, self.wsgi, environ, self.start_response) + sender = self.loop.create_task(self.sender(send)) + try: + await asyncio.wait_for(wsgi, None) + finally: + self.send_queue.append(None) + self.send_event.set() + await asyncio.wait_for(sender, None) + if self.exc_info is not None: + raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) + + async def sender(self, send: ASGISendCallable) -> None: + while True: + if self.send_queue: + message = self.send_queue.popleft() + if message is None: + return + await send(message) + else: + await self.send_event.wait() + self.send_event.clear() + + def start_response( + self, + status: str, + response_headers: Iterable[tuple[str, str]], + exc_info: ExcInfo | None = None, + ) -> None: + self.exc_info = exc_info + if not self.response_started: + self.response_started = True + status_code_str, _ = status.split(" ", 1) + status_code = int(status_code_str) + headers = [(name.encode("ascii"), value.encode("ascii")) for name, value in response_headers] + http_response_start_event: HTTPResponseStartEvent = { + "type": "http.response.start", + "status": status_code, + "headers": headers, + } + self.send_queue.append(http_response_start_event) + self.loop.call_soon_threadsafe(self.send_event.set) + + def wsgi(self, environ: Environ, start_response: StartResponse) -> None: + for chunk in self.app(environ, start_response): # type: ignore + response_body: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": chunk, + "more_body": True, + } + self.send_queue.append(response_body) + self.loop.call_soon_threadsafe(self.send_event.set) + + empty_body: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": b"", + "more_body": False, + } + self.send_queue.append(empty_body) + self.loop.call_soon_threadsafe(self.send_event.set) + + +try: + from a2wsgi import WSGIMiddleware +except ModuleNotFoundError: # pragma: no cover + WSGIMiddleware = _WSGIMiddleware # type: ignore[misc, assignment] diff --git a/lib/python3.12/site-packages/uvicorn/protocols/__init__.py b/lib/python3.12/site-packages/uvicorn/protocols/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/uvicorn/protocols/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8dd66a88ce03c8f9692dab10872962130203e831 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f36a16cb42a16f6247cabde501d5cf44c5a0c207 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/__init__.py b/lib/python3.12/site-packages/uvicorn/protocols/http/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..535aa2740f8f2bf41706815fde81361942ec5e4c Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/auto.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/auto.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff7d5c1ae1e93b3a3eb52b5012b959396aecb18a Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/auto.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/flow_control.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/flow_control.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88d4f91dc96dd3e84efdba28fdbbdce98d76b593 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/flow_control.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/h11_impl.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/h11_impl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9c094af6918ce98bc163ece6a1c3b819a36bf56 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/h11_impl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/httptools_impl.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/httptools_impl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ef5598459ed78c058bed6285d6c3052867b35fb Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/http/__pycache__/httptools_impl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/auto.py b/lib/python3.12/site-packages/uvicorn/protocols/http/auto.py new file mode 100644 index 0000000000000000000000000000000000000000..a14bec144a97a5e3718a768abe3b6a9e7e93d2c1 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/http/auto.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import asyncio + +AutoHTTPProtocol: type[asyncio.Protocol] +try: + import httptools # noqa +except ImportError: # pragma: no cover + from uvicorn.protocols.http.h11_impl import H11Protocol + + AutoHTTPProtocol = H11Protocol +else: # pragma: no cover + from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol + + AutoHTTPProtocol = HttpToolsProtocol diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/flow_control.py b/lib/python3.12/site-packages/uvicorn/protocols/http/flow_control.py new file mode 100644 index 0000000000000000000000000000000000000000..2d1b5fa2d3366fd6584181a26c8569a0207900cc --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/http/flow_control.py @@ -0,0 +1,54 @@ +import asyncio + +from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope + +CLOSE_HEADER = (b"connection", b"close") + +HIGH_WATER_LIMIT = 65536 + + +class FlowControl: + def __init__(self, transport: asyncio.Transport) -> None: + self._transport = transport + self.read_paused = False + self.write_paused = False + self._is_writable_event = asyncio.Event() + self._is_writable_event.set() + + async def drain(self) -> None: + await self._is_writable_event.wait() # pragma: full coverage + + def pause_reading(self) -> None: + if not self.read_paused: + self.read_paused = True + self._transport.pause_reading() + + def resume_reading(self) -> None: + if self.read_paused: + self.read_paused = False + self._transport.resume_reading() + + def pause_writing(self) -> None: + if not self.write_paused: # pragma: full coverage + self.write_paused = True + self._is_writable_event.clear() + + def resume_writing(self) -> None: + if self.write_paused: # pragma: full coverage + self.write_paused = False + self._is_writable_event.set() + + +async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + await send( + { + "type": "http.response.start", + "status": 503, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", b"19"), + (b"connection", b"close"), + ], + } + ) + await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False}) diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py b/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py new file mode 100644 index 0000000000000000000000000000000000000000..b8cdde3abf6045ad99f3c43d312719b078a75369 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py @@ -0,0 +1,543 @@ +from __future__ import annotations + +import asyncio +import http +import logging +from typing import Any, Callable, Literal, cast +from urllib.parse import unquote + +import h11 +from h11._connection import DEFAULT_MAX_INCOMPLETE_EVENT_SIZE + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPRequestEvent, + HTTPResponseBodyEvent, + HTTPResponseStartEvent, + HTTPScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.server import ServerState + + +def _get_status_phrase(status_code: int) -> bytes: + try: + return http.HTTPStatus(status_code).phrase.encode() + except ValueError: + return b"" + + +STATUS_PHRASES = {status_code: _get_status_phrase(status_code) for status_code in range(100, 600)} + + +class H11Protocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.conn = h11.Connection( + h11.SERVER, + config.h11_max_incomplete_event_size + if config.h11_max_incomplete_event_size is not None + else DEFAULT_MAX_INCOMPLETE_EVENT_SIZE, + ) + self.ws_protocol_class = config.ws_protocol_class + self.root_path = config.root_path + self.limit_concurrency = config.limit_concurrency + self.app_state = app_state + + # Timeouts + self.timeout_keep_alive_task: asyncio.TimerHandle | None = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Shared server state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Per-connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.flow: FlowControl = None # type: ignore[assignment] + self.server: tuple[str, int] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["http", "https"] | None = None + + # Per-request state + self.scope: HTTPScope = None # type: ignore[assignment] + self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment] + self.cycle: RequestResponseCycle = None # type: ignore[assignment] + + # Protocol interface + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix) + + if self.cycle and not self.cycle.response_complete: + self.cycle.disconnected = True + if self.conn.our_state != h11.ERROR: + event = h11.ConnectionClosed() + try: + self.conn.send(event) + except h11.LocalProtocolError: + # Premature client disconnect + pass + + if self.cycle is not None: + self.cycle.message_event.set() + if self.flow is not None: + self.flow.resume_writing() + if exc is None: + self.transport.close() + self._unset_keepalive_if_required() + + def eof_received(self) -> None: + pass + + def _unset_keepalive_if_required(self) -> None: + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def _get_upgrade(self) -> bytes | None: + connection = [] + upgrade = None + for name, value in self.headers: + if name == b"connection": + connection = [token.lower().strip() for token in value.split(b",")] + if name == b"upgrade": + upgrade = value.lower() + if b"upgrade" in connection: + return upgrade + return None + + def _should_upgrade_to_ws(self) -> bool: + if self.ws_protocol_class is None: + return False + return True + + def _unsupported_upgrade_warning(self) -> None: + msg = "Unsupported upgrade request." + self.logger.warning(msg) + if not self._should_upgrade_to_ws(): + msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501 + self.logger.warning(msg) + + def _should_upgrade(self) -> bool: + upgrade = self._get_upgrade() + if upgrade == b"websocket" and self._should_upgrade_to_ws(): + return True + if upgrade is not None: + self._unsupported_upgrade_warning() + return False + + def data_received(self, data: bytes) -> None: + self._unset_keepalive_if_required() + + self.conn.receive_data(data) + self.handle_events() + + def handle_events(self) -> None: + while True: + try: + event = self.conn.next_event() + except h11.RemoteProtocolError: + msg = "Invalid HTTP request received." + self.logger.warning(msg) + self.send_400_response(msg) + return + + if event is h11.NEED_DATA: + break + + elif event is h11.PAUSED: + # This case can occur in HTTP pipelining, so we need to + # stop reading any more data, and ensure that at the end + # of the active request/response cycle we handle any + # events that have been buffered up. + self.flow.pause_reading() + break + + elif isinstance(event, h11.Request): + self.headers = [(key.lower(), value) for key, value in event.headers] + raw_path, _, query_string = event.target.partition(b"?") + path = unquote(raw_path.decode("ascii")) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + raw_path + self.scope = { + "type": "http", + "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"}, + "http_version": event.http_version.decode("ascii"), + "server": self.server, + "client": self.client, + "scheme": self.scheme, # type: ignore[typeddict-item] + "method": event.method.decode("ascii"), + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": query_string, + "headers": self.headers, + "state": self.app_state.copy(), + } + if self._should_upgrade(): + self.handle_websocket_upgrade(event) + return + + # Handle 503 responses when 'limit_concurrency' is exceeded. + if self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + # When starting to process a request, disable the keep-alive + # timeout. Normally we disable this when receiving data from + # client and set back when finishing processing its request. + # However, for pipelined requests processing finishes after + # already receiving the next request and thus the timer may + # be set here, which we don't want. + self._unset_keepalive_if_required() + + self.cycle = RequestResponseCycle( + scope=self.scope, + conn=self.conn, + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.server_state.default_headers, + message_event=asyncio.Event(), + on_response=self.on_response_complete, + ) + task = self.loop.create_task(self.cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + + elif isinstance(event, h11.Data): + if self.conn.our_state is h11.DONE: + continue + self.cycle.body += event.data + if len(self.cycle.body) > HIGH_WATER_LIMIT: + self.flow.pause_reading() + self.cycle.message_event.set() + + elif isinstance(event, h11.EndOfMessage): + if self.conn.our_state is h11.DONE: + self.transport.resume_reading() + self.conn.start_next_cycle() + continue + self.cycle.more_body = False + self.cycle.message_event.set() + if self.conn.their_state == h11.MUST_CLOSE: + break + + def handle_websocket_upgrade(self, event: h11.Request) -> None: + if self.logger.level <= TRACE_LOG_LEVEL: # pragma: full coverage + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix) + + self.connections.discard(self) + output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"] + for name, value in self.headers: + output += [name, b": ", value, b"\r\n"] + output.append(b"\r\n") + protocol = self.ws_protocol_class( # type: ignore[call-arg, misc] + config=self.config, + server_state=self.server_state, + app_state=self.app_state, + ) + protocol.connection_made(self.transport) + protocol.data_received(b"".join(output)) + self.transport.set_protocol(protocol) + + def send_400_response(self, msg: str) -> None: + reason = STATUS_PHRASES[400] + headers: list[tuple[bytes, bytes]] = [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ] + event = h11.Response(status_code=400, headers=headers, reason=reason) + output = self.conn.send(event) + self.transport.write(output) + + output = self.conn.send(event=h11.Data(data=msg.encode("ascii"))) + self.transport.write(output) + + output = self.conn.send(event=h11.EndOfMessage()) + self.transport.write(output) + + self.transport.close() + + def on_response_complete(self) -> None: + self.server_state.total_requests += 1 + + if self.transport.is_closing(): + return + + # Set a short Keep-Alive timeout. + self._unset_keepalive_if_required() + + self.timeout_keep_alive_task = self.loop.call_later(self.timeout_keep_alive, self.timeout_keep_alive_handler) + + # Unpause data reads if needed. + self.flow.resume_reading() + + # Unblock any pipelined events. + if self.conn.our_state is h11.DONE and self.conn.their_state is h11.DONE: + self.conn.start_next_cycle() + self.handle_events() + + def shutdown(self) -> None: + """ + Called by the server to commence a graceful shutdown. + """ + if self.cycle is None or self.cycle.response_complete: + event = h11.ConnectionClosed() + self.conn.send(event) + self.transport.close() + else: + self.cycle.keep_alive = False + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() # pragma: full coverage + + def timeout_keep_alive_handler(self) -> None: + """ + Called on a keep-alive connection if no new data is received after a short + delay. + """ + if not self.transport.is_closing(): + event = h11.ConnectionClosed() + self.conn.send(event) + self.transport.close() + + +class RequestResponseCycle: + def __init__( + self, + scope: HTTPScope, + conn: h11.Connection, + transport: asyncio.Transport, + flow: FlowControl, + logger: logging.Logger, + access_logger: logging.Logger, + access_log: bool, + default_headers: list[tuple[bytes, bytes]], + message_event: asyncio.Event, + on_response: Callable[..., None], + ) -> None: + self.scope = scope + self.conn = conn + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + + # Connection state + self.disconnected = False + self.keep_alive = True + self.waiting_for_100_continue = conn.they_are_waiting_for_100_continue + + # Request state + self.body = b"" + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + + # ASGI exception wrapper + async def run_asgi(self, app: ASGI3Application) -> None: + try: + result = await app( # type: ignore[func-returns-value] + self.scope, self.receive, self.send + ) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.transport.close() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.transport.close() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.transport.close() + finally: + self.on_response = lambda: None + + async def send_500_response(self) -> None: + response_start_event: HTTPResponseStartEvent = { + "type": "http.response.start", + "status": 500, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + ], + } + await self.send(response_start_event) + response_body_event: HTTPResponseBodyEvent = { + "type": "http.response.body", + "body": b"Internal Server Error", + "more_body": False, + } + await self.send(response_body_event) + + # ASGI interface + async def send(self, message: ASGISendEvent) -> None: + message_type = message["type"] + + if self.flow.write_paused and not self.disconnected: + await self.flow.drain() # pragma: full coverage + + if self.disconnected: + return # pragma: full coverage + + if not self.response_started: + # Sending response status line and headers + if message_type != "http.response.start": + msg = "Expected ASGI message 'http.response.start', but got '%s'." + raise RuntimeError(msg % message_type) + message = cast("HTTPResponseStartEvent", message) + + self.response_started = True + self.waiting_for_100_continue = False + + status = message["status"] + headers = self.default_headers + list(message.get("headers", [])) + + if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers: + headers = headers + [CLOSE_HEADER] + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status, + ) + + # Write response status line and headers + reason = STATUS_PHRASES[status] + response = h11.Response(status_code=status, headers=headers, reason=reason) + output = self.conn.send(event=response) + self.transport.write(output) + + elif not self.response_complete: + # Sending response body + if message_type != "http.response.body": + msg = "Expected ASGI message 'http.response.body', but got '%s'." + raise RuntimeError(msg % message_type) + message = cast("HTTPResponseBodyEvent", message) + + body = message.get("body", b"") + more_body = message.get("more_body", False) + + # Write response body + data = b"" if self.scope["method"] == "HEAD" else body + output = self.conn.send(event=h11.Data(data=data)) + self.transport.write(output) + + # Handle response completion + if not more_body: + self.response_complete = True + self.message_event.set() + output = self.conn.send(event=h11.EndOfMessage()) + self.transport.write(output) + + else: + # Response already sent + msg = "Unexpected ASGI message '%s' sent, after response already completed." + raise RuntimeError(msg % message_type) + + if self.response_complete: + if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive: + self.conn.send(event=h11.ConnectionClosed()) + self.transport.close() + self.on_response() + + async def receive(self) -> ASGIReceiveEvent: + if self.waiting_for_100_continue and not self.transport.is_closing(): + headers: list[tuple[str, str]] = [] + event = h11.InformationalResponse(status_code=100, headers=headers, reason="Continue") + output = self.conn.send(event=event) + self.transport.write(output) + self.waiting_for_100_continue = False + + if not self.disconnected and not self.response_complete: + self.flow.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + return {"type": "http.disconnect"} + + message: HTTPRequestEvent = { + "type": "http.request", + "body": self.body, + "more_body": self.more_body, + } + self.body = b"" + return message diff --git a/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py b/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py new file mode 100644 index 0000000000000000000000000000000000000000..e8795ed35c598d9635961bac628c4129b83844b7 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py @@ -0,0 +1,570 @@ +from __future__ import annotations + +import asyncio +import http +import logging +import re +import urllib +from asyncio.events import TimerHandle +from collections import deque +from typing import Any, Callable, Literal, cast + +import httptools + +from uvicorn._types import ( + ASGI3Application, + ASGIReceiveEvent, + ASGISendEvent, + HTTPRequestEvent, + HTTPResponseStartEvent, + HTTPScope, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable +from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl +from uvicorn.server import ServerState + +HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]') +HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]") + + +def _get_status_line(status_code: int) -> bytes: + try: + phrase = http.HTTPStatus(status_code).phrase.encode() + except ValueError: + phrase = b"" + return b"".join([b"HTTP/1.1 ", str(status_code).encode(), b" ", phrase, b"\r\n"]) + + +STATUS_LINE = {status_code: _get_status_line(status_code) for status_code in range(100, 600)} + + +class HttpToolsProtocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() + + self.config = config + self.app = config.loaded_app + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.access_logger = logging.getLogger("uvicorn.access") + self.access_log = self.access_logger.hasHandlers() + self.parser = httptools.HttpRequestParser(self) + + try: + # Enable dangerous leniencies to allow server to a response on the first request from a pipelined request. + self.parser.set_dangerous_leniencies(lenient_data_after_close=True) + except AttributeError: # pragma: no cover + # httptools < 0.6.3 + pass + + self.ws_protocol_class = config.ws_protocol_class + self.root_path = config.root_path + self.limit_concurrency = config.limit_concurrency + self.app_state = app_state + + # Timeouts + self.timeout_keep_alive_task: TimerHandle | None = None + self.timeout_keep_alive = config.timeout_keep_alive + + # Global state + self.server_state = server_state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Per-connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.flow: FlowControl = None # type: ignore[assignment] + self.server: tuple[str, int] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["http", "https"] | None = None + self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque() + + # Per-request state + self.scope: HTTPScope = None # type: ignore[assignment] + self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment] + self.expect_100_continue = False + self.cycle: RequestResponseCycle = None # type: ignore[assignment] + + # Protocol interface + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + + self.transport = transport + self.flow = FlowControl(transport) + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "https" if is_ssl(transport) else "http" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.discard(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix) + + if self.cycle and not self.cycle.response_complete: + self.cycle.disconnected = True + if self.cycle is not None: + self.cycle.message_event.set() + if self.flow is not None: + self.flow.resume_writing() + if exc is None: + self.transport.close() + self._unset_keepalive_if_required() + + self.parser = None + + def eof_received(self) -> None: + pass + + def _unset_keepalive_if_required(self) -> None: + if self.timeout_keep_alive_task is not None: + self.timeout_keep_alive_task.cancel() + self.timeout_keep_alive_task = None + + def _get_upgrade(self) -> bytes | None: + connection = [] + upgrade = None + for name, value in self.headers: + if name == b"connection": + connection = [token.lower().strip() for token in value.split(b",")] + if name == b"upgrade": + upgrade = value.lower() + if b"upgrade" in connection: + return upgrade + return None # pragma: full coverage + + def _should_upgrade_to_ws(self) -> bool: + if self.ws_protocol_class is None: + return False + return True + + def _unsupported_upgrade_warning(self) -> None: + self.logger.warning("Unsupported upgrade request.") + if not self._should_upgrade_to_ws(): + msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501 + self.logger.warning(msg) + + def _should_upgrade(self) -> bool: + upgrade = self._get_upgrade() + return upgrade == b"websocket" and self._should_upgrade_to_ws() + + def data_received(self, data: bytes) -> None: + self._unset_keepalive_if_required() + + try: + self.parser.feed_data(data) + except httptools.HttpParserError: + msg = "Invalid HTTP request received." + self.logger.warning(msg) + self.send_400_response(msg) + return + except httptools.HttpParserUpgrade: + if self._should_upgrade(): + self.handle_websocket_upgrade() + else: + self._unsupported_upgrade_warning() + + def handle_websocket_upgrade(self) -> None: + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix) + + self.connections.discard(self) + method = self.scope["method"].encode() + output = [method, b" ", self.url, b" HTTP/1.1\r\n"] + for name, value in self.scope["headers"]: + output += [name, b": ", value, b"\r\n"] + output.append(b"\r\n") + protocol = self.ws_protocol_class( # type: ignore[call-arg, misc] + config=self.config, + server_state=self.server_state, + app_state=self.app_state, + ) + protocol.connection_made(self.transport) + protocol.data_received(b"".join(output)) + self.transport.set_protocol(protocol) + + def send_400_response(self, msg: str) -> None: + content = [STATUS_LINE[400]] + for name, value in self.server_state.default_headers: + content.extend([name, b": ", value, b"\r\n"]) # pragma: full coverage + content.extend( + [ + b"content-type: text/plain; charset=utf-8\r\n", + b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n", + b"connection: close\r\n", + b"\r\n", + msg.encode("ascii"), + ] + ) + self.transport.write(b"".join(content)) + self.transport.close() + + def on_message_begin(self) -> None: + self.url = b"" + self.expect_100_continue = False + self.headers = [] + self.scope = { # type: ignore[typeddict-item] + "type": "http", + "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"}, + "http_version": "1.1", + "server": self.server, + "client": self.client, + "scheme": self.scheme, # type: ignore[typeddict-item] + "root_path": self.root_path, + "headers": self.headers, + "state": self.app_state.copy(), + } + + # Parser callbacks + def on_url(self, url: bytes) -> None: + self.url += url + + def on_header(self, name: bytes, value: bytes) -> None: + name = name.lower() + if name == b"expect" and value.lower() == b"100-continue": + self.expect_100_continue = True + self.headers.append((name, value)) + + def on_headers_complete(self) -> None: + http_version = self.parser.get_http_version() + method = self.parser.get_method() + self.scope["method"] = method.decode("ascii") + if http_version != "1.1": + self.scope["http_version"] = http_version + if self.parser.should_upgrade() and self._should_upgrade(): + return + parsed_url = httptools.parse_url(self.url) + raw_path = parsed_url.path + path = raw_path.decode("ascii") + if "%" in path: + path = urllib.parse.unquote(path) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + raw_path + self.scope["path"] = full_path + self.scope["raw_path"] = full_raw_path + self.scope["query_string"] = parsed_url.query or b"" + + # Handle 503 responses when 'limit_concurrency' is exceeded. + if self.limit_concurrency is not None and ( + len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency + ): + app = service_unavailable + message = "Exceeded concurrency limit." + self.logger.warning(message) + else: + app = self.app + + existing_cycle = self.cycle + self.cycle = RequestResponseCycle( + scope=self.scope, + transport=self.transport, + flow=self.flow, + logger=self.logger, + access_logger=self.access_logger, + access_log=self.access_log, + default_headers=self.server_state.default_headers, + message_event=asyncio.Event(), + expect_100_continue=self.expect_100_continue, + keep_alive=http_version != "1.0", + on_response=self.on_response_complete, + ) + if existing_cycle is None or existing_cycle.response_complete: + # Standard case - start processing the request. + task = self.loop.create_task(self.cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + else: + # Pipelined HTTP requests need to be queued up. + self.flow.pause_reading() + self.pipeline.appendleft((self.cycle, app)) + + def on_body(self, body: bytes) -> None: + if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete: + return + self.cycle.body += body + if len(self.cycle.body) > HIGH_WATER_LIMIT: + self.flow.pause_reading() + self.cycle.message_event.set() + + def on_message_complete(self) -> None: + if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete: + return + self.cycle.more_body = False + self.cycle.message_event.set() + + def on_response_complete(self) -> None: + # Callback for pipelined HTTP requests to be started. + self.server_state.total_requests += 1 + + if self.transport.is_closing(): + return + + self._unset_keepalive_if_required() + + # Unpause data reads if needed. + self.flow.resume_reading() + + # Unblock any pipelined events. If there are none, arm the + # Keep-Alive timeout instead. + if self.pipeline: + cycle, app = self.pipeline.pop() + task = self.loop.create_task(cycle.run_asgi(app)) + task.add_done_callback(self.tasks.discard) + self.tasks.add(task) + else: + self.timeout_keep_alive_task = self.loop.call_later( + self.timeout_keep_alive, self.timeout_keep_alive_handler + ) + + def shutdown(self) -> None: + """ + Called by the server to commence a graceful shutdown. + """ + if self.cycle is None or self.cycle.response_complete: + self.transport.close() + else: + self.cycle.keep_alive = False + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.flow.pause_writing() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.flow.resume_writing() # pragma: full coverage + + def timeout_keep_alive_handler(self) -> None: + """ + Called on a keep-alive connection if no new data is received after a short + delay. + """ + if not self.transport.is_closing(): + self.transport.close() + + +class RequestResponseCycle: + def __init__( + self, + scope: HTTPScope, + transport: asyncio.Transport, + flow: FlowControl, + logger: logging.Logger, + access_logger: logging.Logger, + access_log: bool, + default_headers: list[tuple[bytes, bytes]], + message_event: asyncio.Event, + expect_100_continue: bool, + keep_alive: bool, + on_response: Callable[..., None], + ): + self.scope = scope + self.transport = transport + self.flow = flow + self.logger = logger + self.access_logger = access_logger + self.access_log = access_log + self.default_headers = default_headers + self.message_event = message_event + self.on_response = on_response + + # Connection state + self.disconnected = False + self.keep_alive = keep_alive + self.waiting_for_100_continue = expect_100_continue + + # Request state + self.body = b"" + self.more_body = True + + # Response state + self.response_started = False + self.response_complete = False + self.chunked_encoding: bool | None = None + self.expected_content_length = 0 + + # ASGI exception wrapper + async def run_asgi(self, app: ASGI3Application) -> None: + try: + result = await app( # type: ignore[func-returns-value] + self.scope, self.receive, self.send + ) + except BaseException as exc: + msg = "Exception in ASGI application\n" + self.logger.error(msg, exc_info=exc) + if not self.response_started: + await self.send_500_response() + else: + self.transport.close() + else: + if result is not None: + msg = "ASGI callable should return None, but returned '%s'." + self.logger.error(msg, result) + self.transport.close() + elif not self.response_started and not self.disconnected: + msg = "ASGI callable returned without starting response." + self.logger.error(msg) + await self.send_500_response() + elif not self.response_complete and not self.disconnected: + msg = "ASGI callable returned without completing response." + self.logger.error(msg) + self.transport.close() + finally: + self.on_response = lambda: None + + async def send_500_response(self) -> None: + await self.send( + { + "type": "http.response.start", + "status": 500, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", b"21"), + (b"connection", b"close"), + ], + } + ) + await self.send({"type": "http.response.body", "body": b"Internal Server Error", "more_body": False}) + + # ASGI interface + async def send(self, message: ASGISendEvent) -> None: + message_type = message["type"] + + if self.flow.write_paused and not self.disconnected: + await self.flow.drain() # pragma: full coverage + + if self.disconnected: + return # pragma: full coverage + + if not self.response_started: + # Sending response status line and headers + if message_type != "http.response.start": + msg = "Expected ASGI message 'http.response.start', but got '%s'." + raise RuntimeError(msg % message_type) + message = cast("HTTPResponseStartEvent", message) + + self.response_started = True + self.waiting_for_100_continue = False + + status_code = message["status"] + headers = self.default_headers + list(message.get("headers", [])) + + if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers: + headers = headers + [CLOSE_HEADER] + + if self.access_log: + self.access_logger.info( + '%s - "%s %s HTTP/%s" %d', + get_client_addr(self.scope), + self.scope["method"], + get_path_with_query_string(self.scope), + self.scope["http_version"], + status_code, + ) + + # Write response status line and headers + content = [STATUS_LINE[status_code]] + + for name, value in headers: + if HEADER_RE.search(name): + raise RuntimeError("Invalid HTTP header name.") # pragma: full coverage + if HEADER_VALUE_RE.search(value): + raise RuntimeError("Invalid HTTP header value.") + + name = name.lower() + if name == b"content-length" and self.chunked_encoding is None: + self.expected_content_length = int(value.decode()) + self.chunked_encoding = False + elif name == b"transfer-encoding" and value.lower() == b"chunked": + self.expected_content_length = 0 + self.chunked_encoding = True + elif name == b"connection" and value.lower() == b"close": + self.keep_alive = False + content.extend([name, b": ", value, b"\r\n"]) + + if self.chunked_encoding is None and self.scope["method"] != "HEAD" and status_code not in (204, 304): + # Neither content-length nor transfer-encoding specified + self.chunked_encoding = True + content.append(b"transfer-encoding: chunked\r\n") + + content.append(b"\r\n") + self.transport.write(b"".join(content)) + + elif not self.response_complete: + # Sending response body + if message_type != "http.response.body": + msg = "Expected ASGI message 'http.response.body', but got '%s'." + raise RuntimeError(msg % message_type) + + body = cast(bytes, message.get("body", b"")) + more_body = message.get("more_body", False) + + # Write response body + if self.scope["method"] == "HEAD": + self.expected_content_length = 0 + elif self.chunked_encoding: + if body: + content = [b"%x\r\n" % len(body), body, b"\r\n"] + else: + content = [] + if not more_body: + content.append(b"0\r\n\r\n") + self.transport.write(b"".join(content)) + else: + num_bytes = len(body) + if num_bytes > self.expected_content_length: + raise RuntimeError("Response content longer than Content-Length") + else: + self.expected_content_length -= num_bytes + self.transport.write(body) + + # Handle response completion + if not more_body: + if self.expected_content_length != 0: + raise RuntimeError("Response content shorter than Content-Length") + self.response_complete = True + self.message_event.set() + if not self.keep_alive: + self.transport.close() + self.on_response() + + else: + # Response already sent + msg = "Unexpected ASGI message '%s' sent, after response already completed." + raise RuntimeError(msg % message_type) + + async def receive(self) -> ASGIReceiveEvent: + if self.waiting_for_100_continue and not self.transport.is_closing(): + self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n") + self.waiting_for_100_continue = False + + if not self.disconnected and not self.response_complete: + self.flow.resume_reading() + await self.message_event.wait() + self.message_event.clear() + + if self.disconnected or self.response_complete: + return {"type": "http.disconnect"} + message: HTTPRequestEvent = {"type": "http.request", "body": self.body, "more_body": self.more_body} + self.body = b"" + return message diff --git a/lib/python3.12/site-packages/uvicorn/protocols/utils.py b/lib/python3.12/site-packages/uvicorn/protocols/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e1d6f01d53b33f0cba9d61b8f7ba2e8c079e101f --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/utils.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import asyncio +import urllib.parse + +from uvicorn._types import WWWScope + + +class ClientDisconnected(OSError): ... + + +def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None: + socket_info = transport.get_extra_info("socket") + if socket_info is not None: + try: + info = socket_info.getpeername() + return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None + except OSError: # pragma: no cover + # This case appears to inconsistently occur with uvloop + # bound to a unix domain socket. + return None + + info = transport.get_extra_info("peername") + if info is not None and isinstance(info, (list, tuple)) and len(info) == 2: + return (str(info[0]), int(info[1])) + return None + + +def get_local_addr(transport: asyncio.Transport) -> tuple[str, int] | None: + socket_info = transport.get_extra_info("socket") + if socket_info is not None: + info = socket_info.getsockname() + + return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None + info = transport.get_extra_info("sockname") + if info is not None and isinstance(info, (list, tuple)) and len(info) == 2: + return (str(info[0]), int(info[1])) + return None + + +def is_ssl(transport: asyncio.Transport) -> bool: + return bool(transport.get_extra_info("sslcontext")) + + +def get_client_addr(scope: WWWScope) -> str: + client = scope.get("client") + if not client: + return "" + return "%s:%d" % client + + +def get_path_with_query_string(scope: WWWScope) -> str: + path_with_query_string = urllib.parse.quote(scope["path"]) + if scope["query_string"]: + path_with_query_string = "{}?{}".format(path_with_query_string, scope["query_string"].decode("ascii")) + return path_with_query_string diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/__init__.py b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..afc0ce4f51a44d8db6c1e290e0962b19582fbf35 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/auto.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/auto.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2f0c883e9308d21cdd9ffe06aba2bf1a895bdc7 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/auto.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/websockets_impl.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/websockets_impl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83f0d31d4fc5ecd76b839af2fd9d2a258681ebd8 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/websockets_impl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/wsproto_impl.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/wsproto_impl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca932174de97688cd8ebd44f2271db41313632af Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/protocols/websockets/__pycache__/wsproto_impl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/auto.py b/lib/python3.12/site-packages/uvicorn/protocols/websockets/auto.py new file mode 100644 index 0000000000000000000000000000000000000000..08fd13678a1c4dffe3e126acf0167c17d62c2ed8 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/websockets/auto.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import asyncio +import typing + +AutoWebSocketsProtocol: typing.Callable[..., asyncio.Protocol] | None +try: + import websockets # noqa +except ImportError: # pragma: no cover + try: + import wsproto # noqa + except ImportError: + AutoWebSocketsProtocol = None + else: + from uvicorn.protocols.websockets.wsproto_impl import WSProtocol + + AutoWebSocketsProtocol = WSProtocol +else: + from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol + + AutoWebSocketsProtocol = WebSocketProtocol diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py b/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py new file mode 100644 index 0000000000000000000000000000000000000000..19c11d65aaac4e58940f9082f6abe5ffa1a4daea --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py @@ -0,0 +1,386 @@ +from __future__ import annotations + +import asyncio +import http +import logging +from collections.abc import Sequence +from typing import Any, Literal, Optional, cast +from urllib.parse import unquote + +import websockets +import websockets.legacy.handshake +from websockets.datastructures import Headers +from websockets.exceptions import ConnectionClosed +from websockets.extensions.base import ServerExtensionFactory +from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory +from websockets.legacy.server import HTTPResponse +from websockets.server import WebSocketServerProtocol +from websockets.typing import Subprotocol + +from uvicorn._types import ( + ASGI3Application, + ASGISendEvent, + WebSocketAcceptEvent, + WebSocketCloseEvent, + WebSocketConnectEvent, + WebSocketDisconnectEvent, + WebSocketReceiveEvent, + WebSocketResponseBodyEvent, + WebSocketResponseStartEvent, + WebSocketScope, + WebSocketSendEvent, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.utils import ( + ClientDisconnected, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + is_ssl, +) +from uvicorn.server import ServerState + + +class Server: + closing = False + + def register(self, ws: WebSocketServerProtocol) -> None: + pass + + def unregister(self, ws: WebSocketServerProtocol) -> None: + pass + + def is_serving(self) -> bool: + return not self.closing + + +class WebSocketProtocol(WebSocketServerProtocol): + extra_headers: list[tuple[str, str]] + logger: logging.Logger | logging.LoggerAdapter[Any] + + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, Any], + _loop: asyncio.AbstractEventLoop | None = None, + ): + if not config.loaded: + config.load() + + self.config = config + self.app = cast(ASGI3Application, config.loaded_app) + self.loop = _loop or asyncio.get_event_loop() + self.root_path = config.root_path + self.app_state = app_state + + # Shared server state + self.connections = server_state.connections + self.tasks = server_state.tasks + + # Connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.server: tuple[str, int] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment] + + # Connection events + self.scope: WebSocketScope + self.handshake_started_event = asyncio.Event() + self.handshake_completed_event = asyncio.Event() + self.closed_event = asyncio.Event() + self.initial_response: HTTPResponse | None = None + self.connect_sent = False + self.lost_connection_before_handshake = False + self.accepted_subprotocol: Subprotocol | None = None + + self.ws_server: Server = Server() # type: ignore[assignment] + + extensions: list[ServerExtensionFactory] = [] + if self.config.ws_per_message_deflate: + extensions.append(ServerPerMessageDeflateFactory()) + + super().__init__( + ws_handler=self.ws_handler, + ws_server=self.ws_server, # type: ignore[arg-type] + max_size=self.config.ws_max_size, + max_queue=self.config.ws_max_queue, + ping_interval=self.config.ws_ping_interval, + ping_timeout=self.config.ws_ping_timeout, + extensions=extensions, + logger=logging.getLogger("uvicorn.error"), + ) + self.server_header = None + self.extra_headers = [ + (name.decode("latin-1"), value.decode("latin-1")) for name, value in server_state.default_headers + ] + + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + self.transport = transport + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "wss" if is_ssl(transport) else "ws" + + if self.logger.isEnabledFor(TRACE_LOG_LEVEL): + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix) + + super().connection_made(transport) + + def connection_lost(self, exc: Exception | None) -> None: + self.connections.remove(self) + + if self.logger.isEnabledFor(TRACE_LOG_LEVEL): + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix) + + self.lost_connection_before_handshake = not self.handshake_completed_event.is_set() + self.handshake_completed_event.set() + super().connection_lost(exc) + if exc is None: + self.transport.close() + + def shutdown(self) -> None: + self.ws_server.closing = True + if self.handshake_completed_event.is_set(): + self.fail_connection(1012) + else: + self.send_500_response() + self.transport.close() + + def on_task_complete(self, task: asyncio.Task[None]) -> None: + self.tasks.discard(task) + + async def process_request(self, path: str, request_headers: Headers) -> HTTPResponse | None: + """ + This hook is called to determine if the websocket should return + an HTTP response and close. + + Our behavior here is to start the ASGI application, and then wait + for either `accept` or `close` in order to determine if we should + close the connection. + """ + path_portion, _, query_string = path.partition("?") + + websockets.legacy.handshake.check_request(request_headers) + + subprotocols: list[str] = [] + for header in request_headers.get_all("Sec-WebSocket-Protocol"): + subprotocols.extend([token.strip() for token in header.split(",")]) + + asgi_headers = [ + (name.encode("ascii"), value.encode("ascii", errors="surrogateescape")) + for name, value in request_headers.raw_items() + ] + path = unquote(path_portion) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + path_portion.encode("ascii") + + self.scope = { + "type": "websocket", + "asgi": {"version": self.config.asgi_version, "spec_version": "2.4"}, + "http_version": "1.1", + "scheme": self.scheme, + "server": self.server, + "client": self.client, + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": query_string.encode("ascii"), + "headers": asgi_headers, + "subprotocols": subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + task = self.loop.create_task(self.run_asgi()) + task.add_done_callback(self.on_task_complete) + self.tasks.add(task) + await self.handshake_started_event.wait() + return self.initial_response + + def process_subprotocol( + self, headers: Headers, available_subprotocols: Sequence[Subprotocol] | None + ) -> Subprotocol | None: + """ + We override the standard 'process_subprotocol' behavior here so that + we return whatever subprotocol is sent in the 'accept' message. + """ + return self.accepted_subprotocol + + def send_500_response(self) -> None: + msg = b"Internal Server Error" + content = [ + b"HTTP/1.1 500 Internal Server Error\r\ncontent-type: text/plain; charset=utf-8\r\n", + b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n", + b"connection: close\r\n", + b"\r\n", + msg, + ] + self.transport.write(b"".join(content)) + # Allow handler task to terminate cleanly, as websockets doesn't cancel it by + # itself (see https://github.com/encode/uvicorn/issues/920) + self.handshake_started_event.set() + + async def ws_handler(self, protocol: WebSocketServerProtocol, path: str) -> Any: # type: ignore[override] + """ + This is the main handler function for the 'websockets' implementation + to call into. We just wait for close then return, and instead allow + 'send' and 'receive' events to drive the flow. + """ + self.handshake_completed_event.set() + await self.wait_closed() + + async def run_asgi(self) -> None: + """ + Wrapper around the ASGI callable, handling exceptions and unexpected + termination states. + """ + try: + result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value] + except ClientDisconnected: # pragma: full coverage + self.closed_event.set() + self.transport.close() + except BaseException: + self.closed_event.set() + self.logger.exception("Exception in ASGI application\n") + if not self.handshake_started_event.is_set(): + self.send_500_response() + else: + await self.handshake_completed_event.wait() + self.transport.close() + else: + self.closed_event.set() + if not self.handshake_started_event.is_set(): + self.logger.error("ASGI callable returned without sending handshake.") + self.send_500_response() + self.transport.close() + elif result is not None: + self.logger.error("ASGI callable should return None, but returned '%s'.", result) + await self.handshake_completed_event.wait() + self.transport.close() + + async def asgi_send(self, message: ASGISendEvent) -> None: + message_type = message["type"] + + if not self.handshake_started_event.is_set(): + if message_type == "websocket.accept": + message = cast("WebSocketAcceptEvent", message) + self.logger.info( + '%s - "WebSocket %s" [accepted]', + self.scope["client"], + get_path_with_query_string(self.scope), + ) + self.initial_response = None + self.accepted_subprotocol = cast(Optional[Subprotocol], message.get("subprotocol")) + if "headers" in message: + self.extra_headers.extend( + # ASGI spec requires bytes + # But for compatibility we need to convert it to strings + (name.decode("latin-1"), value.decode("latin-1")) + for name, value in message["headers"] + ) + self.handshake_started_event.set() + + elif message_type == "websocket.close": + message = cast("WebSocketCloseEvent", message) + self.logger.info( + '%s - "WebSocket %s" 403', + self.scope["client"], + get_path_with_query_string(self.scope), + ) + self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"") + self.handshake_started_event.set() + self.closed_event.set() + + elif message_type == "websocket.http.response.start": + message = cast("WebSocketResponseStartEvent", message) + self.logger.info( + '%s - "WebSocket %s" %d', + self.scope["client"], + get_path_with_query_string(self.scope), + message["status"], + ) + # websockets requires the status to be an enum. look it up. + status = http.HTTPStatus(message["status"]) + headers = [ + (name.decode("latin-1"), value.decode("latin-1")) for name, value in message.get("headers", []) + ] + self.initial_response = (status, headers, b"") + self.handshake_started_event.set() + + else: + msg = ( + "Expected ASGI message 'websocket.accept', 'websocket.close', " + "or 'websocket.http.response.start' but got '%s'." + ) + raise RuntimeError(msg % message_type) + + elif not self.closed_event.is_set() and self.initial_response is None: + await self.handshake_completed_event.wait() + + try: + if message_type == "websocket.send": + message = cast("WebSocketSendEvent", message) + bytes_data = message.get("bytes") + text_data = message.get("text") + data = text_data if bytes_data is None else bytes_data + await self.send(data) # type: ignore[arg-type] + + elif message_type == "websocket.close": + message = cast("WebSocketCloseEvent", message) + code = message.get("code", 1000) + reason = message.get("reason", "") or "" + await self.close(code, reason) + self.closed_event.set() + + else: + msg = "Expected ASGI message 'websocket.send' or 'websocket.close', but got '%s'." + raise RuntimeError(msg % message_type) + except ConnectionClosed as exc: + raise ClientDisconnected from exc + + elif self.initial_response is not None: + if message_type == "websocket.http.response.body": + message = cast("WebSocketResponseBodyEvent", message) + body = self.initial_response[2] + message["body"] + self.initial_response = self.initial_response[:2] + (body,) + if not message.get("more_body", False): + self.closed_event.set() + else: + msg = "Expected ASGI message 'websocket.http.response.body' but got '%s'." + raise RuntimeError(msg % message_type) + + else: + msg = "Unexpected ASGI message '%s', after sending 'websocket.close' or response already completed." + raise RuntimeError(msg % message_type) + + async def asgi_receive(self) -> WebSocketDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent: + if not self.connect_sent: + self.connect_sent = True + return {"type": "websocket.connect"} + + await self.handshake_completed_event.wait() + + if self.lost_connection_before_handshake: + # If the handshake failed or the app closed before handshake completion, + # use 1006 Abnormal Closure. + return {"type": "websocket.disconnect", "code": 1006} + + if self.closed_event.is_set(): + return {"type": "websocket.disconnect", "code": 1005} + + try: + data = await self.recv() + except ConnectionClosed: + self.closed_event.set() + if self.ws_server.closing: + return {"type": "websocket.disconnect", "code": 1012} + return {"type": "websocket.disconnect", "code": self.close_code or 1005, "reason": self.close_reason} + + if isinstance(data, str): + return {"type": "websocket.receive", "text": data} + return {"type": "websocket.receive", "bytes": data} diff --git a/lib/python3.12/site-packages/uvicorn/protocols/websockets/wsproto_impl.py b/lib/python3.12/site-packages/uvicorn/protocols/websockets/wsproto_impl.py new file mode 100644 index 0000000000000000000000000000000000000000..c2db5541ad2ba154675aaa296718ee5dcee3198c --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/protocols/websockets/wsproto_impl.py @@ -0,0 +1,377 @@ +from __future__ import annotations + +import asyncio +import logging +import typing +from typing import Literal, cast +from urllib.parse import unquote + +import wsproto +from wsproto import ConnectionType, events +from wsproto.connection import ConnectionState +from wsproto.extensions import Extension, PerMessageDeflate +from wsproto.utilities import LocalProtocolError, RemoteProtocolError + +from uvicorn._types import ( + ASGI3Application, + ASGISendEvent, + WebSocketAcceptEvent, + WebSocketCloseEvent, + WebSocketEvent, + WebSocketResponseBodyEvent, + WebSocketResponseStartEvent, + WebSocketScope, + WebSocketSendEvent, +) +from uvicorn.config import Config +from uvicorn.logging import TRACE_LOG_LEVEL +from uvicorn.protocols.utils import ( + ClientDisconnected, + get_local_addr, + get_path_with_query_string, + get_remote_addr, + is_ssl, +) +from uvicorn.server import ServerState + + +class WSProtocol(asyncio.Protocol): + def __init__( + self, + config: Config, + server_state: ServerState, + app_state: dict[str, typing.Any], + _loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + if not config.loaded: + config.load() # pragma: full coverage + + self.config = config + self.app = cast(ASGI3Application, config.loaded_app) + self.loop = _loop or asyncio.get_event_loop() + self.logger = logging.getLogger("uvicorn.error") + self.root_path = config.root_path + self.app_state = app_state + + # Shared server state + self.connections = server_state.connections + self.tasks = server_state.tasks + self.default_headers = server_state.default_headers + + # Connection state + self.transport: asyncio.Transport = None # type: ignore[assignment] + self.server: tuple[str, int] | None = None + self.client: tuple[str, int] | None = None + self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment] + + # WebSocket state + self.queue: asyncio.Queue[WebSocketEvent] = asyncio.Queue() + self.handshake_complete = False + self.close_sent = False + + # Rejection state + self.response_started = False + + self.conn = wsproto.WSConnection(connection_type=ConnectionType.SERVER) + + self.read_paused = False + self.writable = asyncio.Event() + self.writable.set() + + # Buffers + self.bytes = b"" + self.text = "" + + # Protocol interface + + def connection_made( # type: ignore[override] + self, transport: asyncio.Transport + ) -> None: + self.connections.add(self) + self.transport = transport + self.server = get_local_addr(transport) + self.client = get_remote_addr(transport) + self.scheme = "wss" if is_ssl(transport) else "ws" + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix) + + def connection_lost(self, exc: Exception | None) -> None: + code = 1005 if self.handshake_complete else 1006 + self.queue.put_nowait({"type": "websocket.disconnect", "code": code}) + self.connections.remove(self) + + if self.logger.level <= TRACE_LOG_LEVEL: + prefix = "%s:%d - " % self.client if self.client else "" + self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix) + + self.handshake_complete = True + if exc is None: + self.transport.close() + + def eof_received(self) -> None: + pass + + def data_received(self, data: bytes) -> None: + try: + self.conn.receive_data(data) + except RemoteProtocolError as err: + # TODO: Remove `type: ignore` when wsproto fixes the type annotation. + self.transport.write(self.conn.send(err.event_hint)) # type: ignore[arg-type] # noqa: E501 + self.transport.close() + else: + self.handle_events() + + def handle_events(self) -> None: + for event in self.conn.events(): + if isinstance(event, events.Request): + self.handle_connect(event) + elif isinstance(event, events.TextMessage): + self.handle_text(event) + elif isinstance(event, events.BytesMessage): + self.handle_bytes(event) + elif isinstance(event, events.CloseConnection): + self.handle_close(event) + elif isinstance(event, events.Ping): + self.handle_ping(event) + + def pause_writing(self) -> None: + """ + Called by the transport when the write buffer exceeds the high water mark. + """ + self.writable.clear() # pragma: full coverage + + def resume_writing(self) -> None: + """ + Called by the transport when the write buffer drops below the low water mark. + """ + self.writable.set() # pragma: full coverage + + def shutdown(self) -> None: + if self.handshake_complete: + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) + output = self.conn.send(wsproto.events.CloseConnection(code=1012)) + self.transport.write(output) + else: + self.send_500_response() + self.transport.close() + + def on_task_complete(self, task: asyncio.Task[None]) -> None: + self.tasks.discard(task) + + # Event handlers + + def handle_connect(self, event: events.Request) -> None: + headers = [(b"host", event.host.encode())] + headers += [(key.lower(), value) for key, value in event.extra_headers] + raw_path, _, query_string = event.target.partition("?") + path = unquote(raw_path) + full_path = self.root_path + path + full_raw_path = self.root_path.encode("ascii") + raw_path.encode("ascii") + self.scope: WebSocketScope = { + "type": "websocket", + "asgi": {"version": self.config.asgi_version, "spec_version": "2.4"}, + "http_version": "1.1", + "scheme": self.scheme, + "server": self.server, + "client": self.client, + "root_path": self.root_path, + "path": full_path, + "raw_path": full_raw_path, + "query_string": query_string.encode("ascii"), + "headers": headers, + "subprotocols": event.subprotocols, + "state": self.app_state.copy(), + "extensions": {"websocket.http.response": {}}, + } + self.queue.put_nowait({"type": "websocket.connect"}) + task = self.loop.create_task(self.run_asgi()) + task.add_done_callback(self.on_task_complete) + self.tasks.add(task) + + def handle_text(self, event: events.TextMessage) -> None: + self.text += event.data + if event.message_finished: + self.queue.put_nowait({"type": "websocket.receive", "text": self.text}) + self.text = "" + if not self.read_paused: + self.read_paused = True + self.transport.pause_reading() + + def handle_bytes(self, event: events.BytesMessage) -> None: + self.bytes += event.data + # todo: we may want to guard the size of self.bytes and self.text + if event.message_finished: + self.queue.put_nowait({"type": "websocket.receive", "bytes": self.bytes}) + self.bytes = b"" + if not self.read_paused: + self.read_paused = True + self.transport.pause_reading() + + def handle_close(self, event: events.CloseConnection) -> None: + if self.conn.state == ConnectionState.REMOTE_CLOSING: + self.transport.write(self.conn.send(event.response())) + self.queue.put_nowait({"type": "websocket.disconnect", "code": event.code, "reason": event.reason}) + self.transport.close() + + def handle_ping(self, event: events.Ping) -> None: + self.transport.write(self.conn.send(event.response())) + + def send_500_response(self) -> None: + if self.response_started or self.handshake_complete: + return # we cannot send responses anymore + headers: list[tuple[bytes, bytes]] = [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"connection", b"close"), + (b"content-length", b"21"), + ] + output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True)) + output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error")) + self.transport.write(output) + + async def run_asgi(self) -> None: + try: + result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value] + except ClientDisconnected: + self.transport.close() # pragma: full coverage + except BaseException: + self.logger.exception("Exception in ASGI application\n") + self.send_500_response() + self.transport.close() + else: + if not self.handshake_complete: + self.logger.error("ASGI callable returned without completing handshake.") + self.send_500_response() + self.transport.close() + elif result is not None: + self.logger.error("ASGI callable should return None, but returned '%s'.", result) + self.transport.close() + + async def send(self, message: ASGISendEvent) -> None: + await self.writable.wait() + + message_type = message["type"] + + if not self.handshake_complete: + if message_type == "websocket.accept": + message = typing.cast(WebSocketAcceptEvent, message) + self.logger.info( + '%s - "WebSocket %s" [accepted]', + self.scope["client"], + get_path_with_query_string(self.scope), + ) + subprotocol = message.get("subprotocol") + extra_headers = self.default_headers + list(message.get("headers", [])) + extensions: list[Extension] = [] + if self.config.ws_per_message_deflate: + extensions.append(PerMessageDeflate()) + if not self.transport.is_closing(): + self.handshake_complete = True + output = self.conn.send( + wsproto.events.AcceptConnection( + subprotocol=subprotocol, + extensions=extensions, + extra_headers=extra_headers, + ) + ) + self.transport.write(output) + + elif message_type == "websocket.close": + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) + self.logger.info( + '%s - "WebSocket %s" 403', + self.scope["client"], + get_path_with_query_string(self.scope), + ) + self.handshake_complete = True + self.close_sent = True + event = events.RejectConnection(status_code=403, headers=[]) + output = self.conn.send(event) + self.transport.write(output) + self.transport.close() + + elif message_type == "websocket.http.response.start": + message = typing.cast(WebSocketResponseStartEvent, message) + # ensure status code is in the valid range + if not (100 <= message["status"] < 600): + msg = "Invalid HTTP status code '%d' in response." + raise RuntimeError(msg % message["status"]) + self.logger.info( + '%s - "WebSocket %s" %d', + self.scope["client"], + get_path_with_query_string(self.scope), + message["status"], + ) + self.handshake_complete = True + event = events.RejectConnection( + status_code=message["status"], + headers=list(message["headers"]), + has_body=True, + ) + output = self.conn.send(event) + self.transport.write(output) + self.response_started = True + + else: + msg = ( + "Expected ASGI message 'websocket.accept', 'websocket.close' " + "or 'websocket.http.response.start' " + "but got '%s'." + ) + raise RuntimeError(msg % message_type) + + elif not self.close_sent and not self.response_started: + try: + if message_type == "websocket.send": + message = typing.cast(WebSocketSendEvent, message) + bytes_data = message.get("bytes") + text_data = message.get("text") + data = text_data if bytes_data is None else bytes_data + output = self.conn.send(wsproto.events.Message(data=data)) # type: ignore + if not self.transport.is_closing(): + self.transport.write(output) + + elif message_type == "websocket.close": + message = typing.cast(WebSocketCloseEvent, message) + self.close_sent = True + code = message.get("code", 1000) + reason = message.get("reason", "") or "" + self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason}) + output = self.conn.send(wsproto.events.CloseConnection(code=code, reason=reason)) + if not self.transport.is_closing(): + self.transport.write(output) + self.transport.close() + + else: + msg = "Expected ASGI message 'websocket.send' or 'websocket.close', but got '%s'." + raise RuntimeError(msg % message_type) + except LocalProtocolError as exc: + raise ClientDisconnected from exc + elif self.response_started: + if message_type == "websocket.http.response.body": + message = typing.cast("WebSocketResponseBodyEvent", message) + body_finished = not message.get("more_body", False) + reject_data = events.RejectData(data=message["body"], body_finished=body_finished) + output = self.conn.send(reject_data) + self.transport.write(output) + + if body_finished: + self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) + self.close_sent = True + self.transport.close() + + else: + msg = "Expected ASGI message 'websocket.http.response.body' but got '%s'." + raise RuntimeError(msg % message_type) + + else: + msg = "Unexpected ASGI message '%s', after sending 'websocket.close'." + raise RuntimeError(msg % message_type) + + async def receive(self) -> WebSocketEvent: + message = await self.queue.get() + if self.read_paused and self.queue.empty(): + self.read_paused = False + self.transport.resume_reading() + return message diff --git a/lib/python3.12/site-packages/uvicorn/py.typed b/lib/python3.12/site-packages/uvicorn/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/py.typed @@ -0,0 +1 @@ + diff --git a/lib/python3.12/site-packages/uvicorn/server.py b/lib/python3.12/site-packages/uvicorn/server.py new file mode 100644 index 0000000000000000000000000000000000000000..f0e2ce2cfecb201bace6150edcff71f83efc27d8 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/server.py @@ -0,0 +1,337 @@ +from __future__ import annotations + +import asyncio +import contextlib +import logging +import os +import platform +import signal +import socket +import sys +import threading +import time +from collections.abc import Generator, Sequence +from email.utils import formatdate +from types import FrameType +from typing import TYPE_CHECKING, Union + +import click + +from uvicorn.config import Config + +if TYPE_CHECKING: + from uvicorn.protocols.http.h11_impl import H11Protocol + from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol + from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol + from uvicorn.protocols.websockets.wsproto_impl import WSProtocol + + Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol] + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill `. +) +if sys.platform == "win32": # pragma: py-not-win32 + HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break. + +logger = logging.getLogger("uvicorn.error") + + +class ServerState: + """ + Shared servers state that is available between all protocol instances. + """ + + def __init__(self) -> None: + self.total_requests = 0 + self.connections: set[Protocols] = set() + self.tasks: set[asyncio.Task[None]] = set() + self.default_headers: list[tuple[bytes, bytes]] = [] + + +class Server: + def __init__(self, config: Config) -> None: + self.config = config + self.server_state = ServerState() + + self.started = False + self.should_exit = False + self.force_exit = False + self.last_notified = 0.0 + + self._captured_signals: list[int] = [] + + def run(self, sockets: list[socket.socket] | None = None) -> None: + self.config.setup_event_loop() + return asyncio.run(self.serve(sockets=sockets)) + + async def serve(self, sockets: list[socket.socket] | None = None) -> None: + with self.capture_signals(): + await self._serve(sockets) + + async def _serve(self, sockets: list[socket.socket] | None = None) -> None: + process_id = os.getpid() + + config = self.config + if not config.loaded: + config.load() + + self.lifespan = config.lifespan_class(config) + + message = "Started server process [%d]" + color_message = "Started server process [" + click.style("%d", fg="cyan") + "]" + logger.info(message, process_id, extra={"color_message": color_message}) + + await self.startup(sockets=sockets) + if self.should_exit: + return + await self.main_loop() + await self.shutdown(sockets=sockets) + + message = "Finished server process [%d]" + color_message = "Finished server process [" + click.style("%d", fg="cyan") + "]" + logger.info(message, process_id, extra={"color_message": color_message}) + + async def startup(self, sockets: list[socket.socket] | None = None) -> None: + await self.lifespan.startup() + if self.lifespan.should_exit: + self.should_exit = True + return + + config = self.config + + def create_protocol( + _loop: asyncio.AbstractEventLoop | None = None, + ) -> asyncio.Protocol: + return config.http_protocol_class( # type: ignore[call-arg] + config=config, + server_state=self.server_state, + app_state=self.lifespan.state, + _loop=_loop, + ) + + loop = asyncio.get_running_loop() + + listeners: Sequence[socket.SocketType] + if sockets is not None: # pragma: full coverage + # Explicitly passed a list of open sockets. + # We use this when the server is run from a Gunicorn worker. + + def _share_socket( + sock: socket.SocketType, + ) -> socket.SocketType: # pragma py-not-win32 + # Windows requires the socket be explicitly shared across + # multiple workers (processes). + from socket import fromshare # type: ignore[attr-defined] + + sock_data = sock.share(os.getpid()) # type: ignore[attr-defined] + return fromshare(sock_data) + + self.servers: list[asyncio.base_events.Server] = [] + for sock in sockets: + is_windows = platform.system() == "Windows" + if config.workers > 1 and is_windows: # pragma: py-not-win32 + sock = _share_socket(sock) # type: ignore[assignment] + server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog) + self.servers.append(server) + listeners = sockets + + elif config.fd is not None: # pragma: py-win32 + # Use an existing socket, from a file descriptor. + sock = socket.fromfd(config.fd, socket.AF_UNIX, socket.SOCK_STREAM) + server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog) + assert server.sockets is not None # mypy + listeners = server.sockets + self.servers = [server] + + elif config.uds is not None: # pragma: py-win32 + # Create a socket using UNIX domain socket. + uds_perms = 0o666 + if os.path.exists(config.uds): + uds_perms = os.stat(config.uds).st_mode # pragma: full coverage + server = await loop.create_unix_server( + create_protocol, path=config.uds, ssl=config.ssl, backlog=config.backlog + ) + os.chmod(config.uds, uds_perms) + assert server.sockets is not None # mypy + listeners = server.sockets + self.servers = [server] + + else: + # Standard case. Create a socket from a host/port pair. + try: + server = await loop.create_server( + create_protocol, + host=config.host, + port=config.port, + ssl=config.ssl, + backlog=config.backlog, + ) + except OSError as exc: + logger.error(exc) + await self.lifespan.shutdown() + sys.exit(1) + + assert server.sockets is not None + listeners = server.sockets + self.servers = [server] + + if sockets is None: + self._log_started_message(listeners) + else: + # We're most likely running multiple workers, so a message has already been + # logged by `config.bind_socket()`. + pass # pragma: full coverage + + self.started = True + + def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None: + config = self.config + + if config.fd is not None: # pragma: py-win32 + sock = listeners[0] + logger.info( + "Uvicorn running on socket %s (Press CTRL+C to quit)", + sock.getsockname(), + ) + + elif config.uds is not None: # pragma: py-win32 + logger.info("Uvicorn running on unix socket %s (Press CTRL+C to quit)", config.uds) + + else: + addr_format = "%s://%s:%d" + host = "0.0.0.0" if config.host is None else config.host + if ":" in host: + # It's an IPv6 address. + addr_format = "%s://[%s]:%d" + + port = config.port + if port == 0: + port = listeners[0].getsockname()[1] + + protocol_name = "https" if config.ssl else "http" + message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)" + color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)" + logger.info( + message, + protocol_name, + host, + port, + extra={"color_message": color_message}, + ) + + async def main_loop(self) -> None: + counter = 0 + should_exit = await self.on_tick(counter) + while not should_exit: + counter += 1 + counter = counter % 864000 + await asyncio.sleep(0.1) + should_exit = await self.on_tick(counter) + + async def on_tick(self, counter: int) -> bool: + # Update the default headers, once per second. + if counter % 10 == 0: + current_time = time.time() + current_date = formatdate(current_time, usegmt=True).encode() + + if self.config.date_header: + date_header = [(b"date", current_date)] + else: + date_header = [] + + self.server_state.default_headers = date_header + self.config.encoded_headers + + # Callback to `callback_notify` once every `timeout_notify` seconds. + if self.config.callback_notify is not None: + if current_time - self.last_notified > self.config.timeout_notify: # pragma: full coverage + self.last_notified = current_time + await self.config.callback_notify() + + # Determine if we should exit. + if self.should_exit: + return True + + max_requests = self.config.limit_max_requests + if max_requests is not None and self.server_state.total_requests >= max_requests: + logger.warning(f"Maximum request limit of {max_requests} exceeded. Terminating process.") + return True + + return False + + async def shutdown(self, sockets: list[socket.socket] | None = None) -> None: + logger.info("Shutting down") + + # Stop accepting new connections. + for server in self.servers: + server.close() + for sock in sockets or []: + sock.close() # pragma: full coverage + + # Request shutdown on all existing connections. + for connection in list(self.server_state.connections): + connection.shutdown() + await asyncio.sleep(0.1) + + # When 3.10 is not supported anymore, use `async with asyncio.timeout(...):`. + try: + await asyncio.wait_for( + self._wait_tasks_to_complete(), + timeout=self.config.timeout_graceful_shutdown, + ) + except asyncio.TimeoutError: + logger.error( + "Cancel %s running task(s), timeout graceful shutdown exceeded", + len(self.server_state.tasks), + ) + for t in self.server_state.tasks: + t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded") + + # Send the lifespan shutdown event, and wait for application shutdown. + if not self.force_exit: + await self.lifespan.shutdown() + + async def _wait_tasks_to_complete(self) -> None: + # Wait for existing connections to finish sending responses. + if self.server_state.connections and not self.force_exit: + msg = "Waiting for connections to close. (CTRL+C to force quit)" + logger.info(msg) + while self.server_state.connections and not self.force_exit: + await asyncio.sleep(0.1) + + # Wait for existing tasks to complete. + if self.server_state.tasks and not self.force_exit: + msg = "Waiting for background tasks to complete. (CTRL+C to force quit)" + logger.info(msg) + while self.server_state.tasks and not self.force_exit: + await asyncio.sleep(0.1) + + for server in self.servers: + await server.wait_closed() + + @contextlib.contextmanager + def capture_signals(self) -> Generator[None, None, None]: + # Signals can only be listened to from the main thread. + if threading.current_thread() is not threading.main_thread(): + yield + return + # always use signal.signal, even if loop.add_signal_handler is available + # this allows to restore previous signal handlers later on + original_handlers = {sig: signal.signal(sig, self.handle_exit) for sig in HANDLED_SIGNALS} + try: + yield + finally: + for sig, handler in original_handlers.items(): + signal.signal(sig, handler) + # If we did gracefully shut down due to a signal, try to + # trigger the expected behaviour now; multiple signals would be + # done LIFO, see https://stackoverflow.com/questions/48434964 + for captured_signal in reversed(self._captured_signals): + signal.raise_signal(captured_signal) + + def handle_exit(self, sig: int, frame: FrameType | None) -> None: + self._captured_signals.append(sig) + if self.should_exit and sig == signal.SIGINT: + self.force_exit = True # pragma: full coverage + else: + self.should_exit = True diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py b/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cfceb6b94ce36dad006b1f81b816d45ce70ab3d6 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/supervisors/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from uvicorn.supervisors.basereload import BaseReload +from uvicorn.supervisors.multiprocess import Multiprocess + +if TYPE_CHECKING: + ChangeReload: type[BaseReload] +else: + try: + from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload + except ImportError: # pragma: no cover + from uvicorn.supervisors.statreload import StatReload as ChangeReload + +__all__ = ["Multiprocess", "ChangeReload"] diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a235abb8f815a4a0cda81bd777ceebfaf37dbd7 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/basereload.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/basereload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84eae1d6af6955737cdfe6e5374566809c2b0544 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/basereload.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/multiprocess.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/multiprocess.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc89aaaae4fdb3468b7652d781d74f68308dec24 Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/multiprocess.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/statreload.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/statreload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0489ba28c23db7cb066cc847ccafdecf051829ab Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/statreload.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/watchfilesreload.cpython-312.pyc b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/watchfilesreload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5ea724fc461e61e8b24c52a4ec42543019e49fc Binary files /dev/null and b/lib/python3.12/site-packages/uvicorn/supervisors/__pycache__/watchfilesreload.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py b/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py new file mode 100644 index 0000000000000000000000000000000000000000..eed999230228235f9bd4e532feef05d3108e5930 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/supervisors/basereload.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import logging +import os +import signal +import sys +import threading +from collections.abc import Iterator +from pathlib import Path +from socket import socket +from types import FrameType +from typing import Callable + +import click + +from uvicorn._subprocess import get_subprocess +from uvicorn.config import Config + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill `. +) + +logger = logging.getLogger("uvicorn.error") + + +class BaseReload: + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + self.config = config + self.target = target + self.sockets = sockets + self.should_exit = threading.Event() + self.pid = os.getpid() + self.is_restarting = False + self.reloader_name: str | None = None + + def signal_handler(self, sig: int, frame: FrameType | None) -> None: # pragma: full coverage + """ + A signal handler that is registered with the parent process. + """ + if sys.platform == "win32" and self.is_restarting: + self.is_restarting = False + else: + self.should_exit.set() + + def run(self) -> None: + self.startup() + for changes in self: + if changes: + logger.warning( + "%s detected changes in %s. Reloading...", + self.reloader_name, + ", ".join(map(_display_path, changes)), + ) + self.restart() + + self.shutdown() + + def pause(self) -> None: + if self.should_exit.wait(self.config.reload_delay): + raise StopIteration() + + def __iter__(self) -> Iterator[list[Path] | None]: + return self + + def __next__(self) -> list[Path] | None: + return self.should_restart() + + def startup(self) -> None: + message = f"Started reloader process [{self.pid}] using {self.reloader_name}" + color_message = "Started reloader process [{}] using {}".format( + click.style(str(self.pid), fg="cyan", bold=True), + click.style(str(self.reloader_name), fg="cyan", bold=True), + ) + logger.info(message, extra={"color_message": color_message}) + + for sig in HANDLED_SIGNALS: + signal.signal(sig, self.signal_handler) + + self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) + self.process.start() + + def restart(self) -> None: + if sys.platform == "win32": # pragma: py-not-win32 + self.is_restarting = True + assert self.process.pid is not None + os.kill(self.process.pid, signal.CTRL_C_EVENT) + + # This is a workaround to ensure the Ctrl+C event is processed + sys.stdout.write(" ") # This has to be a non-empty string + sys.stdout.flush() + else: # pragma: py-win32 + self.process.terminate() + self.process.join() + + self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) + self.process.start() + + def shutdown(self) -> None: + if sys.platform == "win32": + self.should_exit.set() # pragma: py-not-win32 + else: + self.process.terminate() # pragma: py-win32 + self.process.join() + + for sock in self.sockets: + sock.close() + + message = f"Stopping reloader process [{str(self.pid)}]" + color_message = "Stopping reloader process [{}]".format(click.style(str(self.pid), fg="cyan", bold=True)) + logger.info(message, extra={"color_message": color_message}) + + def should_restart(self) -> list[Path] | None: + raise NotImplementedError("Reload strategies should override should_restart()") + + +def _display_path(path: Path) -> str: + try: + return f"'{path.relative_to(Path.cwd())}'" + except ValueError: + return f"'{path}'" diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py b/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..e198fe780bf5d24f7eba828a28a279d98133361c --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/supervisors/multiprocess.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import logging +import os +import signal +import threading +from multiprocessing import Pipe +from socket import socket +from typing import Any, Callable + +import click + +from uvicorn._subprocess import get_subprocess +from uvicorn.config import Config + +SIGNALS = { + getattr(signal, f"SIG{x}"): x + for x in "INT TERM BREAK HUP QUIT TTIN TTOU USR1 USR2 WINCH".split() + if hasattr(signal, f"SIG{x}") +} + +logger = logging.getLogger("uvicorn.error") + + +class Process: + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + self.real_target = target + + self.parent_conn, self.child_conn = Pipe() + self.process = get_subprocess(config, self.target, sockets) + + def ping(self, timeout: float = 5) -> bool: + self.parent_conn.send(b"ping") + if self.parent_conn.poll(timeout): + self.parent_conn.recv() + return True + return False + + def pong(self) -> None: + self.child_conn.recv() + self.child_conn.send(b"pong") + + def always_pong(self) -> None: + while True: + self.pong() + + def target(self, sockets: list[socket] | None = None) -> Any: # pragma: no cover + if os.name == "nt": # pragma: py-not-win32 + # Windows doesn't support SIGTERM, so we use SIGBREAK instead. + # And then we raise SIGTERM when SIGBREAK is received. + # https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/signal?view=msvc-170 + signal.signal( + signal.SIGBREAK, # type: ignore[attr-defined] + lambda sig, frame: signal.raise_signal(signal.SIGTERM), + ) + + threading.Thread(target=self.always_pong, daemon=True).start() + return self.real_target(sockets) + + def is_alive(self, timeout: float = 5) -> bool: + if not self.process.is_alive(): + return False # pragma: full coverage + + return self.ping(timeout) + + def start(self) -> None: + self.process.start() + + def terminate(self) -> None: + if self.process.exitcode is None: # Process is still running + assert self.process.pid is not None + if os.name == "nt": # pragma: py-not-win32 + # Windows doesn't support SIGTERM. + # So send SIGBREAK, and then in process raise SIGTERM. + os.kill(self.process.pid, signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined] + else: + os.kill(self.process.pid, signal.SIGTERM) + logger.info(f"Terminated child process [{self.process.pid}]") + + self.parent_conn.close() + self.child_conn.close() + + def kill(self) -> None: + # In Windows, the method will call `TerminateProcess` to kill the process. + # In Unix, the method will send SIGKILL to the process. + self.process.kill() + + def join(self) -> None: + logger.info(f"Waiting for child process [{self.process.pid}]") + self.process.join() + + @property + def pid(self) -> int | None: + return self.process.pid + + +class Multiprocess: + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + self.config = config + self.target = target + self.sockets = sockets + + self.processes_num = config.workers + self.processes: list[Process] = [] + + self.should_exit = threading.Event() + + self.signal_queue: list[int] = [] + for sig in SIGNALS: + signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig)) + + def init_processes(self) -> None: + for _ in range(self.processes_num): + process = Process(self.config, self.target, self.sockets) + process.start() + self.processes.append(process) + + def terminate_all(self) -> None: + for process in self.processes: + process.terminate() + + def join_all(self) -> None: + for process in self.processes: + process.join() + + def restart_all(self) -> None: + for idx, process in enumerate(self.processes): + process.terminate() + process.join() + new_process = Process(self.config, self.target, self.sockets) + new_process.start() + self.processes[idx] = new_process + + def run(self) -> None: + message = f"Started parent process [{os.getpid()}]" + color_message = "Started parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True)) + logger.info(message, extra={"color_message": color_message}) + + self.init_processes() + + while not self.should_exit.wait(0.5): + self.handle_signals() + self.keep_subprocess_alive() + + self.terminate_all() + self.join_all() + + message = f"Stopping parent process [{os.getpid()}]" + color_message = "Stopping parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True)) + logger.info(message, extra={"color_message": color_message}) + + def keep_subprocess_alive(self) -> None: + if self.should_exit.is_set(): + return # parent process is exiting, no need to keep subprocess alive + + for idx, process in enumerate(self.processes): + if process.is_alive(): + continue + + process.kill() # process is hung, kill it + process.join() + + if self.should_exit.is_set(): + return # pragma: full coverage + + logger.info(f"Child process [{process.pid}] died") + process = Process(self.config, self.target, self.sockets) + process.start() + self.processes[idx] = process + + def handle_signals(self) -> None: + for sig in tuple(self.signal_queue): + self.signal_queue.remove(sig) + sig_name = SIGNALS[sig] + sig_handler = getattr(self, f"handle_{sig_name.lower()}", None) + if sig_handler is not None: + sig_handler() + else: # pragma: no cover + logger.debug(f"Received signal {sig_name}, but no handler is defined for it.") + + def handle_int(self) -> None: + logger.info("Received SIGINT, exiting.") + self.should_exit.set() + + def handle_term(self) -> None: + logger.info("Received SIGTERM, exiting.") + self.should_exit.set() + + def handle_break(self) -> None: # pragma: py-not-win32 + logger.info("Received SIGBREAK, exiting.") + self.should_exit.set() + + def handle_hup(self) -> None: # pragma: py-win32 + logger.info("Received SIGHUP, restarting processes.") + self.restart_all() + + def handle_ttin(self) -> None: # pragma: py-win32 + logger.info("Received SIGTTIN, increasing the number of processes.") + self.processes_num += 1 + process = Process(self.config, self.target, self.sockets) + process.start() + self.processes.append(process) + + def handle_ttou(self) -> None: # pragma: py-win32 + logger.info("Received SIGTTOU, decreasing number of processes.") + if self.processes_num <= 1: + logger.info("Already reached one process, cannot decrease the number of processes anymore.") + return + self.processes_num -= 1 + process = self.processes.pop() + process.terminate() + process.join() diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py b/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py new file mode 100644 index 0000000000000000000000000000000000000000..d9cbd525b252cd843cb4020ba92425659653b32b --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/supervisors/statreload.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import logging +from collections.abc import Iterator +from pathlib import Path +from socket import socket +from typing import Callable + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + +logger = logging.getLogger("uvicorn.error") + + +class StatReload(BaseReload): + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + super().__init__(config, target, sockets) + self.reloader_name = "StatReload" + self.mtimes: dict[Path, float] = {} + + if config.reload_excludes or config.reload_includes: + logger.warning("--reload-include and --reload-exclude have no effect unless watchfiles is installed.") + + def should_restart(self) -> list[Path] | None: + self.pause() + + for file in self.iter_py_files(): + try: + mtime = file.stat().st_mtime + except OSError: # pragma: nocover + continue + + old_time = self.mtimes.get(file) + if old_time is None: + self.mtimes[file] = mtime + continue + elif mtime > old_time: + return [file] + return None + + def restart(self) -> None: + self.mtimes = {} + return super().restart() + + def iter_py_files(self) -> Iterator[Path]: + for reload_dir in self.config.reload_dirs: + for path in list(reload_dir.rglob("*.py")): + yield path.resolve() diff --git a/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py b/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py new file mode 100644 index 0000000000000000000000000000000000000000..0d3b9b77e611d2d861fdaaa2940a78b03aee8a91 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/supervisors/watchfilesreload.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from pathlib import Path +from socket import socket +from typing import Callable + +from watchfiles import watch + +from uvicorn.config import Config +from uvicorn.supervisors.basereload import BaseReload + + +class FileFilter: + def __init__(self, config: Config): + default_includes = ["*.py"] + self.includes = [default for default in default_includes if default not in config.reload_excludes] + self.includes.extend(config.reload_includes) + self.includes = list(set(self.includes)) + + default_excludes = [".*", ".py[cod]", ".sw.*", "~*"] + self.excludes = [default for default in default_excludes if default not in config.reload_includes] + self.exclude_dirs = [] + for e in config.reload_excludes: + p = Path(e) + try: + is_dir = p.is_dir() + except OSError: # pragma: no cover + # gets raised on Windows for values like "*.py" + is_dir = False + + if is_dir: + self.exclude_dirs.append(p) + else: + self.excludes.append(e) # pragma: full coverage + self.excludes = list(set(self.excludes)) + + def __call__(self, path: Path) -> bool: + for include_pattern in self.includes: + if path.match(include_pattern): + if str(path).endswith(include_pattern): + return True # pragma: full coverage + + for exclude_dir in self.exclude_dirs: + if exclude_dir in path.parents: + return False + + for exclude_pattern in self.excludes: + if path.match(exclude_pattern): + return False # pragma: full coverage + + return True + return False + + +class WatchFilesReload(BaseReload): + def __init__( + self, + config: Config, + target: Callable[[list[socket] | None], None], + sockets: list[socket], + ) -> None: + super().__init__(config, target, sockets) + self.reloader_name = "WatchFiles" + self.reload_dirs = [] + for directory in config.reload_dirs: + if Path.cwd() not in directory.parents: + self.reload_dirs.append(directory) + if Path.cwd() not in self.reload_dirs: + self.reload_dirs.append(Path.cwd()) + + self.watch_filter = FileFilter(config) + self.watcher = watch( + *self.reload_dirs, + watch_filter=None, + stop_event=self.should_exit, + # using yield_on_timeout here mostly to make sure tests don't + # hang forever, won't affect the class's behavior + yield_on_timeout=True, + ) + + def should_restart(self) -> list[Path] | None: + self.pause() + + changes = next(self.watcher) + if changes: + unique_paths = {Path(c[1]) for c in changes} + return [p for p in unique_paths if self.watch_filter(p)] + return None diff --git a/lib/python3.12/site-packages/uvicorn/workers.py b/lib/python3.12/site-packages/uvicorn/workers.py new file mode 100644 index 0000000000000000000000000000000000000000..06fdf295eeb6ae0a513f145077bf57c3bde3a9a3 --- /dev/null +++ b/lib/python3.12/site-packages/uvicorn/workers.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import asyncio +import logging +import signal +import sys +import warnings +from typing import Any + +from gunicorn.arbiter import Arbiter +from gunicorn.workers.base import Worker + +from uvicorn.config import Config +from uvicorn.server import Server + +warnings.warn( + "The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.\n" + "For more details, see https://github.com/Kludex/uvicorn-worker.", + DeprecationWarning, +) + + +class UvicornWorker(Worker): + """ + A worker class for Gunicorn that interfaces with an ASGI consumer callable, + rather than a WSGI callable. + """ + + CONFIG_KWARGS: dict[str, Any] = {"loop": "auto", "http": "auto"} + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + logger = logging.getLogger("uvicorn.error") + logger.handlers = self.log.error_log.handlers + logger.setLevel(self.log.error_log.level) + logger.propagate = False + + logger = logging.getLogger("uvicorn.access") + logger.handlers = self.log.access_log.handlers + logger.setLevel(self.log.access_log.level) + logger.propagate = False + + config_kwargs: dict = { + "app": None, + "log_config": None, + "timeout_keep_alive": self.cfg.keepalive, + "timeout_notify": self.timeout, + "callback_notify": self.callback_notify, + "limit_max_requests": self.max_requests, + "forwarded_allow_ips": self.cfg.forwarded_allow_ips, + } + + if self.cfg.is_ssl: + ssl_kwargs = { + "ssl_keyfile": self.cfg.ssl_options.get("keyfile"), + "ssl_certfile": self.cfg.ssl_options.get("certfile"), + "ssl_keyfile_password": self.cfg.ssl_options.get("password"), + "ssl_version": self.cfg.ssl_options.get("ssl_version"), + "ssl_cert_reqs": self.cfg.ssl_options.get("cert_reqs"), + "ssl_ca_certs": self.cfg.ssl_options.get("ca_certs"), + "ssl_ciphers": self.cfg.ssl_options.get("ciphers"), + } + config_kwargs.update(ssl_kwargs) + + if self.cfg.settings["backlog"].value: + config_kwargs["backlog"] = self.cfg.settings["backlog"].value + + config_kwargs.update(self.CONFIG_KWARGS) + + self.config = Config(**config_kwargs) + + def init_process(self) -> None: + self.config.setup_event_loop() + super().init_process() + + def init_signals(self) -> None: + # Reset signals so Gunicorn doesn't swallow subprocess return codes + # other signals are set up by Server.install_signal_handlers() + # See: https://github.com/encode/uvicorn/issues/894 + for s in self.SIGNALS: + signal.signal(s, signal.SIG_DFL) + + signal.signal(signal.SIGUSR1, self.handle_usr1) + # Don't let SIGUSR1 disturb active requests by interrupting system calls + signal.siginterrupt(signal.SIGUSR1, False) + + def _install_sigquit_handler(self) -> None: + """Install a SIGQUIT handler on workers. + + - https://github.com/encode/uvicorn/issues/1116 + - https://github.com/benoitc/gunicorn/issues/2604 + """ + + loop = asyncio.get_running_loop() + loop.add_signal_handler(signal.SIGQUIT, self.handle_exit, signal.SIGQUIT, None) + + async def _serve(self) -> None: + self.config.app = self.wsgi + server = Server(config=self.config) + self._install_sigquit_handler() + await server.serve(sockets=self.sockets) + if not server.started: + sys.exit(Arbiter.WORKER_BOOT_ERROR) + + def run(self) -> None: + return asyncio.run(self._serve()) + + async def callback_notify(self) -> None: + self.notify() + + +class UvicornH11Worker(UvicornWorker): + CONFIG_KWARGS = {"loop": "asyncio", "http": "h11"}