@@ -642,12 +590,6 @@ DOCUMENTATION :: END
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
}
- $('#nav-tabs-synceditems').on('shown.bs.tab', function() {
- if (typeof(sync_table) === 'undefined') {
- loadSyncTable(user_id);
- }
- });
-
$("#refresh-syncs-list").click(function() {
sync_table.ajax.reload();
});
diff --git a/lib/annotated_types/__init__.py b/lib/annotated_types/__init__.py
new file mode 100644
index 00000000..2f989504
--- /dev/null
+++ b/lib/annotated_types/__init__.py
@@ -0,0 +1,396 @@
+import math
+import sys
+from dataclasses import dataclass
+from datetime import timezone
+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.6.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 BaseMetadata objects.
+
+ `GroupedMetadata` on its own is not metadata and has no meaning.
+ All it the the constraint 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[BaseMetadata]:
+ >>> 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[BaseMetadata]:
+ ...
+
+ 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[BaseMetadata]: # 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 timezone 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, timezone, EllipsisType, None]
+
+
+@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
+ ``IsDigit = 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 propogate or discard the resulting exception.
+ """
+
+ func: Callable[[Any], bool]
+
+
+@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
+IsDigits = Annotated[_StrType, Predicate(str.isdigit)]
+"""
+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/importlib_resources/tests/zipdata01/__init__.py b/lib/annotated_types/py.typed
similarity index 100%
rename from lib/importlib_resources/tests/zipdata01/__init__.py
rename to lib/annotated_types/py.typed
diff --git a/lib/annotated_types/test_cases.py b/lib/annotated_types/test_cases.py
new file mode 100644
index 00000000..f54df700
--- /dev/null
+++ b/lib/annotated_types/test_cases.py
@@ -0,0 +1,147 @@
+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)))],
+ )
+
+ # predicate types
+
+ yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
+ yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
+ yield Case(at.IsDigits[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/arrow/_version.py b/lib/arrow/_version.py
index 10aa336c..67bc602a 100644
--- a/lib/arrow/_version.py
+++ b/lib/arrow/_version.py
@@ -1 +1 @@
-__version__ = "1.2.3"
+__version__ = "1.3.0"
diff --git a/lib/arrow/arrow.py b/lib/arrow/arrow.py
index 1ede107f..8d329efd 100644
--- a/lib/arrow/arrow.py
+++ b/lib/arrow/arrow.py
@@ -168,9 +168,9 @@ class Arrow:
isinstance(tzinfo, dt_tzinfo)
and hasattr(tzinfo, "localize")
and hasattr(tzinfo, "zone")
- and tzinfo.zone # type: ignore[attr-defined]
+ and tzinfo.zone
):
- tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined]
+ tzinfo = parser.TzinfoParser.parse(tzinfo.zone)
elif isinstance(tzinfo, str):
tzinfo = parser.TzinfoParser.parse(tzinfo)
@@ -495,7 +495,7 @@ class Arrow:
yield current
values = [getattr(current, f) for f in cls._ATTRS]
- current = cls(*values, tzinfo=tzinfo).shift( # type: ignore
+ current = cls(*values, tzinfo=tzinfo).shift( # type: ignore[misc]
**{frame_relative: relative_steps}
)
@@ -578,7 +578,7 @@ class Arrow:
for _ in range(3 - len(values)):
values.append(1)
- floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore
+ floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore[misc]
if frame_absolute == "week":
# if week_start is greater than self.isoweekday() go back one week by setting delta = 7
@@ -792,7 +792,6 @@ class Arrow:
return self._datetime.isoformat()
def __format__(self, formatstr: str) -> str:
-
if len(formatstr) > 0:
return self.format(formatstr)
@@ -804,7 +803,6 @@ class Arrow:
# attributes and properties
def __getattr__(self, name: str) -> int:
-
if name == "week":
return self.isocalendar()[1]
@@ -965,7 +963,6 @@ class Arrow:
absolute_kwargs = {}
for key, value in kwargs.items():
-
if key in self._ATTRS:
absolute_kwargs[key] = value
elif key in ["week", "quarter"]:
@@ -1022,7 +1019,6 @@ class Arrow:
additional_attrs = ["weeks", "quarters", "weekday"]
for key, value in kwargs.items():
-
if key in self._ATTRS_PLURAL or key in additional_attrs:
relative_kwargs[key] = value
else:
@@ -1259,11 +1255,10 @@ class Arrow:
)
if trunc(abs(delta)) != 1:
- granularity += "s" # type: ignore
+ granularity += "s" # type: ignore[assignment]
return locale.describe(granularity, delta, only_distance=only_distance)
else:
-
if not granularity:
raise ValueError(
"Empty granularity list provided. "
@@ -1314,7 +1309,7 @@ class Arrow:
def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":
"""Returns a new :class:`Arrow
` object, that represents
- the time difference relative to the attrbiutes of the
+ the time difference relative to the attributes of the
:class:`Arrow ` object.
:param timestring: a ``str`` representing a humanized relative time.
@@ -1367,7 +1362,6 @@ class Arrow:
# Search input string for each time unit within locale
for unit, unit_object in locale_obj.timeframes.items():
-
# Need to check the type of unit_object to create the correct dictionary
if isinstance(unit_object, Mapping):
strings_to_search = unit_object
@@ -1378,7 +1372,6 @@ class Arrow:
# Needs to cycle all through strings as some locales have strings that
# could overlap in a regex match, since input validation isn't being performed.
for time_delta, time_string in strings_to_search.items():
-
# Replace {0} with regex \d representing digits
search_string = str(time_string)
search_string = search_string.format(r"\d+")
@@ -1419,7 +1412,7 @@ class Arrow:
# Assert error if string does not modify any units
if not any([True for k, v in unit_visited.items() if v]):
raise ValueError(
- "Input string not valid. Note: Some locales do not support the week granulairty in Arrow. "
+ "Input string not valid. Note: Some locales do not support the week granularity in Arrow. "
"If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error."
)
@@ -1718,7 +1711,6 @@ class Arrow:
# math
def __add__(self, other: Any) -> "Arrow":
-
if isinstance(other, (timedelta, relativedelta)):
return self.fromdatetime(self._datetime + other, self._datetime.tzinfo)
@@ -1736,7 +1728,6 @@ class Arrow:
pass # pragma: no cover
def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]:
-
if isinstance(other, (timedelta, relativedelta)):
return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)
@@ -1749,7 +1740,6 @@ class Arrow:
return NotImplemented
def __rsub__(self, other: Any) -> timedelta:
-
if isinstance(other, dt_datetime):
return other - self._datetime
@@ -1758,42 +1748,36 @@ class Arrow:
# comparisons
def __eq__(self, other: Any) -> bool:
-
if not isinstance(other, (Arrow, dt_datetime)):
return False
return self._datetime == self._get_datetime(other)
def __ne__(self, other: Any) -> bool:
-
if not isinstance(other, (Arrow, dt_datetime)):
return True
return not self.__eq__(other)
def __gt__(self, other: Any) -> bool:
-
if not isinstance(other, (Arrow, dt_datetime)):
return NotImplemented
return self._datetime > self._get_datetime(other)
def __ge__(self, other: Any) -> bool:
-
if not isinstance(other, (Arrow, dt_datetime)):
return NotImplemented
return self._datetime >= self._get_datetime(other)
def __lt__(self, other: Any) -> bool:
-
if not isinstance(other, (Arrow, dt_datetime)):
return NotImplemented
return self._datetime < self._get_datetime(other)
def __le__(self, other: Any) -> bool:
-
if not isinstance(other, (Arrow, dt_datetime)):
return NotImplemented
@@ -1865,7 +1849,6 @@ class Arrow:
def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]:
"""Sets default end and limit values for range method."""
if end is None:
-
if limit is None:
raise ValueError("One of 'end' or 'limit' is required.")
diff --git a/lib/arrow/factory.py b/lib/arrow/factory.py
index aad4af8b..f35085f1 100644
--- a/lib/arrow/factory.py
+++ b/lib/arrow/factory.py
@@ -267,11 +267,9 @@ class ArrowFactory:
raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.")
elif arg_count == 2:
-
arg_1, arg_2 = args[0], args[1]
if isinstance(arg_1, datetime):
-
# (datetime, tzinfo/str) -> fromdatetime @ tzinfo
if isinstance(arg_2, (dt_tzinfo, str)):
return self.type.fromdatetime(arg_1, tzinfo=arg_2)
@@ -281,7 +279,6 @@ class ArrowFactory:
)
elif isinstance(arg_1, date):
-
# (date, tzinfo/str) -> fromdate @ tzinfo
if isinstance(arg_2, (dt_tzinfo, str)):
return self.type.fromdate(arg_1, tzinfo=arg_2)
diff --git a/lib/arrow/formatter.py b/lib/arrow/formatter.py
index 728bea1a..d45f7153 100644
--- a/lib/arrow/formatter.py
+++ b/lib/arrow/formatter.py
@@ -29,7 +29,6 @@ FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ"
class DateTimeFormatter:
-
# This pattern matches characters enclosed in square brackets are matched as
# an atomic group. For more info on atomic groups and how to they are
# emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578
@@ -41,18 +40,15 @@ class DateTimeFormatter:
locale: locales.Locale
def __init__(self, locale: str = DEFAULT_LOCALE) -> None:
-
self.locale = locales.get_locale(locale)
def format(cls, dt: datetime, fmt: str) -> str:
-
# FIXME: _format_token() is nullable
return cls._FORMAT_RE.sub(
lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt
)
def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]:
-
if token and token.startswith("[") and token.endswith("]"):
return token[1:-1]
diff --git a/lib/arrow/locales.py b/lib/arrow/locales.py
index 3627497f..34b2a098 100644
--- a/lib/arrow/locales.py
+++ b/lib/arrow/locales.py
@@ -129,7 +129,6 @@ class Locale:
_locale_map[locale_name.lower().replace("_", "-")] = cls
def __init__(self) -> None:
-
self._month_name_to_ordinal = None
def describe(
@@ -174,7 +173,7 @@ class Locale:
# Needed to determine the correct relative string to use
timeframe_value = 0
- for _unit_name, unit_value in timeframes:
+ for _, unit_value in timeframes:
if trunc(unit_value) != 0:
timeframe_value = trunc(unit_value)
break
@@ -285,7 +284,6 @@ class Locale:
timeframe: TimeFrameLiteral,
delta: Union[float, int],
) -> str:
-
if timeframe == "now":
return humanized
@@ -425,7 +423,7 @@ class ItalianLocale(Locale):
"hours": "{0} ore",
"day": "un giorno",
"days": "{0} giorni",
- "week": "una settimana,",
+ "week": "una settimana",
"weeks": "{0} settimane",
"month": "un mese",
"months": "{0} mesi",
@@ -867,14 +865,16 @@ class FinnishLocale(Locale):
timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = {
"now": "juuri nyt",
- "second": "sekunti",
- "seconds": {"past": "{0} muutama sekunti", "future": "{0} muutaman sekunnin"},
+ "second": {"past": "sekunti", "future": "sekunnin"},
+ "seconds": {"past": "{0} sekuntia", "future": "{0} sekunnin"},
"minute": {"past": "minuutti", "future": "minuutin"},
"minutes": {"past": "{0} minuuttia", "future": "{0} minuutin"},
"hour": {"past": "tunti", "future": "tunnin"},
"hours": {"past": "{0} tuntia", "future": "{0} tunnin"},
- "day": "päivä",
+ "day": {"past": "päivä", "future": "päivän"},
"days": {"past": "{0} päivää", "future": "{0} päivän"},
+ "week": {"past": "viikko", "future": "viikon"},
+ "weeks": {"past": "{0} viikkoa", "future": "{0} viikon"},
"month": {"past": "kuukausi", "future": "kuukauden"},
"months": {"past": "{0} kuukautta", "future": "{0} kuukauden"},
"year": {"past": "vuosi", "future": "vuoden"},
@@ -1887,7 +1887,7 @@ class GermanBaseLocale(Locale):
future = "in {0}"
and_word = "und"
- timeframes = {
+ timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = {
"now": "gerade eben",
"second": "einer Sekunde",
"seconds": "{0} Sekunden",
@@ -1982,7 +1982,9 @@ class GermanBaseLocale(Locale):
return super().describe(timeframe, delta, only_distance)
# German uses a different case without 'in' or 'ago'
- humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta)))
+ humanized: str = self.timeframes_only_distance[timeframe].format(
+ trunc(abs(delta))
+ )
return humanized
@@ -2547,6 +2549,8 @@ class ArabicLocale(Locale):
"hours": {"2": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"},
"day": "يوم",
"days": {"2": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"},
+ "week": "اسبوع",
+ "weeks": {"2": "اسبوعين", "ten": "{0} أسابيع", "higher": "{0} اسبوع"},
"month": "شهر",
"months": {"2": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"},
"year": "سنة",
@@ -3709,6 +3713,8 @@ class HungarianLocale(Locale):
"hours": {"past": "{0} órával", "future": "{0} óra"},
"day": {"past": "egy nappal", "future": "egy nap"},
"days": {"past": "{0} nappal", "future": "{0} nap"},
+ "week": {"past": "egy héttel", "future": "egy hét"},
+ "weeks": {"past": "{0} héttel", "future": "{0} hét"},
"month": {"past": "egy hónappal", "future": "egy hónap"},
"months": {"past": "{0} hónappal", "future": "{0} hónap"},
"year": {"past": "egy évvel", "future": "egy év"},
@@ -3934,7 +3940,6 @@ class ThaiLocale(Locale):
class LaotianLocale(Locale):
-
names = ["lo", "lo-la"]
past = "{0} ກ່ອນຫນ້ານີ້"
@@ -4119,6 +4124,7 @@ class BengaliLocale(Locale):
return f"{n}র্থ"
if n == 6:
return f"{n}ষ্ঠ"
+ return ""
class RomanshLocale(Locale):
@@ -4137,6 +4143,8 @@ class RomanshLocale(Locale):
"hours": "{0} ura",
"day": "in di",
"days": "{0} dis",
+ "week": "in'emna",
+ "weeks": "{0} emnas",
"month": "in mais",
"months": "{0} mais",
"year": "in onn",
@@ -5399,7 +5407,7 @@ class LuxembourgishLocale(Locale):
future = "an {0}"
and_word = "an"
- timeframes = {
+ timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = {
"now": "just elo",
"second": "enger Sekonn",
"seconds": "{0} Sekonnen",
@@ -5487,7 +5495,9 @@ class LuxembourgishLocale(Locale):
return super().describe(timeframe, delta, only_distance)
# Luxembourgish uses a different case without 'in' or 'ago'
- humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta)))
+ humanized: str = self.timeframes_only_distance[timeframe].format(
+ trunc(abs(delta))
+ )
return humanized
diff --git a/lib/arrow/parser.py b/lib/arrow/parser.py
index e95d78b0..645e3da7 100644
--- a/lib/arrow/parser.py
+++ b/lib/arrow/parser.py
@@ -159,7 +159,6 @@ class DateTimeParser:
_input_re_map: Dict[_FORMAT_TYPE, Pattern[str]]
def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None:
-
self.locale = locales.get_locale(locale)
self._input_re_map = self._BASE_INPUT_RE_MAP.copy()
self._input_re_map.update(
@@ -196,7 +195,6 @@ class DateTimeParser:
def parse_iso(
self, datetime_string: str, normalize_whitespace: bool = False
) -> datetime:
-
if normalize_whitespace:
datetime_string = re.sub(r"\s+", " ", datetime_string.strip())
@@ -236,13 +234,14 @@ class DateTimeParser:
]
if has_time:
-
if has_space_divider:
date_string, time_string = datetime_string.split(" ", 1)
else:
date_string, time_string = datetime_string.split("T", 1)
- time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE)
+ time_parts = re.split(
+ r"[\+\-Z]", time_string, maxsplit=1, flags=re.IGNORECASE
+ )
time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0])
@@ -303,7 +302,6 @@ class DateTimeParser:
fmt: Union[List[str], str],
normalize_whitespace: bool = False,
) -> datetime:
-
if normalize_whitespace:
datetime_string = re.sub(r"\s+", " ", datetime_string)
@@ -341,12 +339,11 @@ class DateTimeParser:
f"Unable to find a match group for the specified token {token!r}."
)
- self._parse_token(token, value, parts) # type: ignore
+ self._parse_token(token, value, parts) # type: ignore[arg-type]
return self._build_datetime(parts)
def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]:
-
# fmt is a string of tokens like 'YYYY-MM-DD'
# we construct a new string by replacing each
# token by its pattern:
@@ -498,7 +495,6 @@ class DateTimeParser:
value: Any,
parts: _Parts,
) -> None:
-
if token == "YYYY":
parts["year"] = int(value)
@@ -508,7 +504,7 @@ class DateTimeParser:
elif token in ["MMMM", "MMM"]:
# FIXME: month_number() is nullable
- parts["month"] = self.locale.month_number(value.lower()) # type: ignore
+ parts["month"] = self.locale.month_number(value.lower()) # type: ignore[typeddict-item]
elif token in ["MM", "M"]:
parts["month"] = int(value)
@@ -588,7 +584,6 @@ class DateTimeParser:
weekdate = parts.get("weekdate")
if weekdate is not None:
-
year, week = int(weekdate[0]), int(weekdate[1])
if weekdate[2] is not None:
@@ -712,7 +707,6 @@ class DateTimeParser:
)
def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime:
-
_datetime: Optional[datetime] = None
for fmt in formats:
@@ -740,12 +734,11 @@ class DateTimeParser:
class TzinfoParser:
_TZINFO_RE: ClassVar[Pattern[str]] = re.compile(
- r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$"
+ r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?"
)
@classmethod
def parse(cls, tzinfo_string: str) -> dt_tzinfo:
-
tzinfo: Optional[dt_tzinfo] = None
if tzinfo_string == "local":
@@ -755,7 +748,6 @@ class TzinfoParser:
tzinfo = tz.tzutc()
else:
-
iso_match = cls._TZINFO_RE.match(tzinfo_string)
if iso_match:
diff --git a/lib/autocommand/autoasync.py b/lib/autocommand/autoasync.py
index 3c8ebdcf..688f7e05 100644
--- a/lib/autocommand/autoasync.py
+++ b/lib/autocommand/autoasync.py
@@ -20,7 +20,7 @@ from functools import wraps
from inspect import signature
-def _launch_forever_coro(coro, args, kwargs, loop):
+async def _run_forever_coro(coro, args, kwargs, loop):
'''
This helper function launches an async main function that was tagged with
forever=True. There are two possibilities:
@@ -48,7 +48,7 @@ def _launch_forever_coro(coro, args, kwargs, loop):
# forever=True feature from autoasync at some point in the future.
thing = coro(*args, **kwargs)
if iscoroutine(thing):
- loop.create_task(thing)
+ await thing
def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
@@ -127,7 +127,9 @@ def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
args, kwargs = bound_args.args, bound_args.kwargs
if forever:
- _launch_forever_coro(coro, args, kwargs, local_loop)
+ local_loop.create_task(_run_forever_coro(
+ coro, args, kwargs, local_loop
+ ))
local_loop.run_forever()
else:
return local_loop.run_until_complete(coro(*args, **kwargs))
diff --git a/lib/backports/functools_lru_cache.py b/lib/backports/functools_lru_cache.py
index 1b83fe99..e372cff3 100644
--- a/lib/backports/functools_lru_cache.py
+++ b/lib/backports/functools_lru_cache.py
@@ -26,6 +26,12 @@ def update_wrapper(
class _HashedSeq(list):
+ """This class guarantees that hash() will be called no more than once
+ per element. This is important because the lru_cache() will hash
+ the key multiple times on a cache miss.
+
+ """
+
__slots__ = 'hashvalue'
def __init__(self, tup, hash=hash):
@@ -41,45 +47,57 @@ def _make_key(
kwds,
typed,
kwd_mark=(object(),),
- fasttypes=set([int, str, frozenset, type(None)]),
- sorted=sorted,
+ fasttypes={int, str},
tuple=tuple,
type=type,
len=len,
):
- 'Make a cache key from optionally typed positional and keyword arguments'
+ """Make a cache key from optionally typed positional and keyword arguments
+
+ The key is constructed in a way that is flat as possible rather than
+ as a nested structure that would take more memory.
+
+ If there is only a single argument and its data type is known to cache
+ its hash value, then that argument is returned without a wrapper. This
+ saves space and improves lookup speed.
+
+ """
+ # All of code below relies on kwds preserving the order input by the user.
+ # Formerly, we sorted() the kwds before looping. The new way is *much*
+ # faster; however, it means that f(x=1, y=2) will now be treated as a
+ # distinct call from f(y=2, x=1) which will be cached separately.
key = args
if kwds:
- sorted_items = sorted(kwds.items())
key += kwd_mark
- for item in sorted_items:
+ for item in kwds.items():
key += item
if typed:
key += tuple(type(v) for v in args)
if kwds:
- key += tuple(type(v) for k, v in sorted_items)
+ key += tuple(type(v) for v in kwds.values())
elif len(key) == 1 and type(key[0]) in fasttypes:
return key[0]
return _HashedSeq(key)
-def lru_cache(maxsize=100, typed=False): # noqa: C901
+def lru_cache(maxsize=128, typed=False):
"""Least-recently-used cache decorator.
If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.
If *typed* is True, arguments of different types will be cached separately.
- For example, f(3.0) and f(3) will be treated as distinct calls with
- distinct results.
+ For example, f(decimal.Decimal("3.0")) and f(3.0) will be treated as
+ distinct calls with distinct results. Some types such as str and int may
+ be cached separately even when typed is false.
Arguments to the cached function must be hashable.
- View the cache statistics named tuple (hits, misses, maxsize, currsize) with
- f.cache_info(). Clear the cache and statistics with f.cache_clear().
+ View the cache statistics named tuple (hits, misses, maxsize, currsize)
+ with f.cache_info(). Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.
- See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
+ See: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
"""
@@ -88,108 +106,138 @@ def lru_cache(maxsize=100, typed=False): # noqa: C901
# The internals of the lru_cache are encapsulated for thread safety and
# to allow the implementation to change (including a possible C version).
+ if isinstance(maxsize, int):
+ # Negative maxsize is treated as 0
+ if maxsize < 0:
+ maxsize = 0
+ elif callable(maxsize) and isinstance(typed, bool):
+ # The user_function was passed in directly via the maxsize argument
+ user_function, maxsize = maxsize, 128
+ wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
+ wrapper.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed}
+ return update_wrapper(wrapper, user_function)
+ elif maxsize is not None:
+ raise TypeError('Expected first argument to be an integer, a callable, or None')
+
def decorating_function(user_function):
- cache = dict()
- stats = [0, 0] # make statistics updateable non-locally
- HITS, MISSES = 0, 1 # names for the stats fields
- make_key = _make_key
- cache_get = cache.get # bound method to lookup key or return None
- _len = len # localize the global len() function
- lock = RLock() # because linkedlist updates aren't threadsafe
- root = [] # root of the circular doubly linked list
- root[:] = [root, root, None, None] # initialize by pointing to self
- nonlocal_root = [root] # make updateable non-locally
- PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
-
- if maxsize == 0:
-
- def wrapper(*args, **kwds):
- # no caching, just do a statistics update after a successful call
- result = user_function(*args, **kwds)
- stats[MISSES] += 1
- return result
-
- elif maxsize is None:
-
- def wrapper(*args, **kwds):
- # simple caching without ordering or size limit
- key = make_key(args, kwds, typed)
- result = cache_get(
- key, root
- ) # root used here as a unique not-found sentinel
- if result is not root:
- stats[HITS] += 1
- return result
- result = user_function(*args, **kwds)
- cache[key] = result
- stats[MISSES] += 1
- return result
-
- else:
-
- def wrapper(*args, **kwds):
- # size limited caching that tracks accesses by recency
- key = make_key(args, kwds, typed) if kwds or typed else args
- with lock:
- link = cache_get(key)
- if link is not None:
- # record recent use of the key by moving it
- # to the front of the list
- (root,) = nonlocal_root
- link_prev, link_next, key, result = link
- link_prev[NEXT] = link_next
- link_next[PREV] = link_prev
- last = root[PREV]
- last[NEXT] = root[PREV] = link
- link[PREV] = last
- link[NEXT] = root
- stats[HITS] += 1
- return result
- result = user_function(*args, **kwds)
- with lock:
- (root,) = nonlocal_root
- if key in cache:
- # getting here means that this same key was added to the
- # cache while the lock was released. since the link
- # update is already done, we need only return the
- # computed result and update the count of misses.
- pass
- elif _len(cache) >= maxsize:
- # use the old root to store the new key and result
- oldroot = root
- oldroot[KEY] = key
- oldroot[RESULT] = result
- # empty the oldest link and make it the new root
- root = nonlocal_root[0] = oldroot[NEXT]
- oldkey = root[KEY]
- root[KEY] = root[RESULT] = None
- # now update the cache dictionary for the new links
- del cache[oldkey]
- cache[key] = oldroot
- else:
- # put result in a new link at the front of the list
- last = root[PREV]
- link = [last, root, key, result]
- last[NEXT] = root[PREV] = cache[key] = link
- stats[MISSES] += 1
- return result
-
- def cache_info():
- """Report cache statistics"""
- with lock:
- return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
-
- def cache_clear():
- """Clear the cache and cache statistics"""
- with lock:
- cache.clear()
- root = nonlocal_root[0]
- root[:] = [root, root, None, None]
- stats[:] = [0, 0]
-
- wrapper.__wrapped__ = user_function
- wrapper.cache_info = cache_info
- wrapper.cache_clear = cache_clear
+ wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
+ wrapper.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
return decorating_function
+
+
+def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
+ # Constants shared by all lru cache instances:
+ sentinel = object() # unique object used to signal cache misses
+ make_key = _make_key # build a key from the function arguments
+ PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
+
+ cache = {}
+ hits = misses = 0
+ full = False
+ cache_get = cache.get # bound method to lookup a key or return None
+ cache_len = cache.__len__ # get cache size without calling len()
+ lock = RLock() # because linkedlist updates aren't threadsafe
+ root = [] # root of the circular doubly linked list
+ root[:] = [root, root, None, None] # initialize by pointing to self
+
+ if maxsize == 0:
+
+ def wrapper(*args, **kwds):
+ # No caching -- just a statistics update
+ nonlocal misses
+ misses += 1
+ result = user_function(*args, **kwds)
+ return result
+
+ elif maxsize is None:
+
+ def wrapper(*args, **kwds):
+ # Simple caching without ordering or size limit
+ nonlocal hits, misses
+ key = make_key(args, kwds, typed)
+ result = cache_get(key, sentinel)
+ if result is not sentinel:
+ hits += 1
+ return result
+ misses += 1
+ result = user_function(*args, **kwds)
+ cache[key] = result
+ return result
+
+ else:
+
+ def wrapper(*args, **kwds):
+ # Size limited caching that tracks accesses by recency
+ nonlocal root, hits, misses, full
+ key = make_key(args, kwds, typed)
+ with lock:
+ link = cache_get(key)
+ if link is not None:
+ # Move the link to the front of the circular queue
+ link_prev, link_next, _key, result = link
+ link_prev[NEXT] = link_next
+ link_next[PREV] = link_prev
+ last = root[PREV]
+ last[NEXT] = root[PREV] = link
+ link[PREV] = last
+ link[NEXT] = root
+ hits += 1
+ return result
+ misses += 1
+ result = user_function(*args, **kwds)
+ with lock:
+ if key in cache:
+ # Getting here means that this same key was added to the
+ # cache while the lock was released. Since the link
+ # update is already done, we need only return the
+ # computed result and update the count of misses.
+ pass
+ elif full:
+ # Use the old root to store the new key and result.
+ oldroot = root
+ oldroot[KEY] = key
+ oldroot[RESULT] = result
+ # Empty the oldest link and make it the new root.
+ # Keep a reference to the old key and old result to
+ # prevent their ref counts from going to zero during the
+ # update. That will prevent potentially arbitrary object
+ # clean-up code (i.e. __del__) from running while we're
+ # still adjusting the links.
+ root = oldroot[NEXT]
+ oldkey = root[KEY]
+ root[KEY] = root[RESULT] = None
+ # Now update the cache dictionary.
+ del cache[oldkey]
+ # Save the potentially reentrant cache[key] assignment
+ # for last, after the root and links have been put in
+ # a consistent state.
+ cache[key] = oldroot
+ else:
+ # Put result in a new link at the front of the queue.
+ last = root[PREV]
+ link = [last, root, key, result]
+ last[NEXT] = root[PREV] = cache[key] = link
+ # Use the cache_len bound method instead of the len() function
+ # which could potentially be wrapped in an lru_cache itself.
+ full = cache_len() >= maxsize
+ return result
+
+ def cache_info():
+ """Report cache statistics"""
+ with lock:
+ return _CacheInfo(hits, misses, maxsize, cache_len())
+
+ def cache_clear():
+ """Clear the cache and cache statistics"""
+ nonlocal hits, misses, full
+ with lock:
+ cache.clear()
+ root[:] = [root, root, None, None]
+ hits = misses = 0
+ full = False
+
+ wrapper.cache_info = cache_info
+ wrapper.cache_clear = cache_clear
+ return wrapper
diff --git a/lib/bleach/__init__.py b/lib/bleach/__init__.py
index 4e87eb80..12e93b4d 100644
--- a/lib/bleach/__init__.py
+++ b/lib/bleach/__init__.py
@@ -11,9 +11,9 @@ from bleach.sanitizer import (
# yyyymmdd
-__releasedate__ = "20230123"
+__releasedate__ = "20231006"
# x.y.z or x.y.z.dev0 -- semver
-__version__ = "6.0.0"
+__version__ = "6.1.0"
__all__ = ["clean", "linkify"]
diff --git a/lib/bleach/html5lib_shim.py b/lib/bleach/html5lib_shim.py
index aa5189b1..ca1cc8c8 100644
--- a/lib/bleach/html5lib_shim.py
+++ b/lib/bleach/html5lib_shim.py
@@ -395,10 +395,17 @@ class BleachHTMLTokenizer(HTMLTokenizer):
# followed by a series of characters. It's treated as a tag
# name that abruptly ends, but we should treat that like
# character data
- yield {
- "type": TAG_TOKEN_TYPE_CHARACTERS,
- "data": "<" + self.currentToken["name"],
- }
+ yield {"type": TAG_TOKEN_TYPE_CHARACTERS, "data": self.stream.get_tag()}
+ elif last_error_token["data"] in (
+ "eof-in-attribute-name",
+ "eof-in-attribute-value-no-quotes",
+ ):
+ # Handle the case where the text being parsed ends with <
+ # followed by a series of characters and then space and then
+ # more characters. It's treated as a tag name followed by an
+ # attribute that abruptly ends, but we should treat that like
+ # character data.
+ yield {"type": TAG_TOKEN_TYPE_CHARACTERS, "data": self.stream.get_tag()}
else:
yield last_error_token
diff --git a/lib/bleach/linkifier.py b/lib/bleach/linkifier.py
index 679d7ead..8fcefb2c 100644
--- a/lib/bleach/linkifier.py
+++ b/lib/bleach/linkifier.py
@@ -45,8 +45,8 @@ def build_url_re(tlds=TLDS, protocols=html5lib_shim.allowed_protocols):
r"""\(* # Match any opening parentheses.
\b(?"]*)?
- # /path/zz (excluding "unsafe" chars from RFC 1738,
+ (?:[/?][^\s\{{\}}\|\\\^`<>"]*)?
+ # /path/zz (excluding "unsafe" chars from RFC 3986,
# except for # and ~, which happen in practice)
""".format(
"|".join(sorted(protocols)), "|".join(sorted(tlds))
@@ -591,7 +591,7 @@ class LinkifyFilter(html5lib_shim.Filter):
in_a = False
token_buffer = []
else:
- token_buffer.append(token)
+ token_buffer.extend(list(self.extract_entities(token)))
continue
if token["type"] in ["StartTag", "EmptyTag"]:
diff --git a/lib/bs4/__init__.py b/lib/bs4/__init__.py
index 3d2ab09a..d8ad5e1d 100644
--- a/lib/bs4/__init__.py
+++ b/lib/bs4/__init__.py
@@ -15,8 +15,8 @@ documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/
"""
__author__ = "Leonard Richardson (leonardr@segfault.org)"
-__version__ = "4.12.2"
-__copyright__ = "Copyright (c) 2004-2023 Leonard Richardson"
+__version__ = "4.12.3"
+__copyright__ = "Copyright (c) 2004-2024 Leonard Richardson"
# Use of this source code is governed by the MIT license.
__license__ = "MIT"
diff --git a/lib/bs4/builder/__init__.py b/lib/bs4/builder/__init__.py
index 2e397458..ffb31fc2 100644
--- a/lib/bs4/builder/__init__.py
+++ b/lib/bs4/builder/__init__.py
@@ -514,15 +514,19 @@ class DetectsXMLParsedAsHTML(object):
XML_PREFIX_B = b'
foo
bar
baz
diff --git a/lib/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase b/lib/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase
new file mode 100644
index 00000000..4828f8a4
--- /dev/null
+++ b/lib/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase
@@ -0,0 +1 @@
+ tet>