Source code for whenever._common

from __future__ import annotations

from collections.abc import Callable

# Unused imports are necessary for sphinx autodoc due to
# scoping issues introduced by add_alternate_constructors().
from datetime import (  # noqa: F401
    date as _date,
    datetime as _datetime,
    time as _time,  # noqa: F401
    timedelta as _timedelta,
    timezone as _timezone,
)
from functools import lru_cache
from typing import TYPE_CHECKING, Any, TypeVar, no_type_check
from warnings import warn

UTC = _timezone.utc
DUMMY_LEAP_YEAR = 4
Nanos = int  # 0-999_999_999

WARNING_HANDLING_DOCS_MSG = (
    "For project-wide warning configuration, see "
    "https://whenever.readthedocs.io/en/latest/guide/warnings.html"
)

OFFSET_SHIFT_STALE_MSG = (
    "Shifting an OffsetDateTime keeps its fixed UTC offset. If the operation "
    "crosses a DST or other timezone transition, that offset may become stale—"
    "no longer matching the region's actual offset "
    "(e.g. adding 1 day to 2024-03-09 12:00-07:00 gives 2024-03-10 12:00-07:00, "
    "but if this offset represents Denver, Colorado (America/Denver), "
    "the actual offset changed to -06:00 on that date). "
    "Convert to ZonedDateTime first (using .assume_tz()) for timezone-aware arithmetic. "
    "If the fixed offset is intentional, pass `stale_offset_ok=True`. "
    + WARNING_HANDLING_DOCS_MSG
)

PLAIN_SHIFT_UNAWARE_MSG = (
    "Shifting a PlainDateTime by exact time units does not account for timezone transitions "
    "that may occur in the interval "
    "(e.g. adding 2 hours to 2023-03-26 01:30 in Amsterdam crosses the spring-forward "
    "transition, so only 1 real hour has passed). "
    "Use .assume_tz('<tz>') + delta if you know the timezone. "
    "If timezone transitions are intentionally irrelevant here, pass "
    "`naive_arithmetic_ok=True`. " + WARNING_HANDLING_DOCS_MSG
)

# A self-set variable to detect if we're being run by sphinx autodoc
try:
    from sphinx import (  # type: ignore[attr-defined, import-not-found, unused-ignore]
        SPHINX_RUNNING as SPHINX_RUNNING,
    )
except ImportError:
    SPHINX_RUNNING = False

# A sentinel value that looks nice in autodoc.
# Used in cases where `None` would be a valid value, or where we want to
# avoid allowing `None` to be passed in by users.
UNSET: Any = type(
    "UNSET", (), {"__repr__": lambda _: "...", "__bool__": lambda _: False}
)()


# We cache fixed-offset tzinfo objects to avoid creating multiple identical ones.
# It's very common to only have whole-hour offsets, so this helps a lot.
@lru_cache
def mk_fixed_tzinfo(secs: int, /) -> _timezone:
    return _timezone(_timedelta(seconds=secs))


def check_utc_bounds(dt: _datetime) -> _datetime:
    try:
        dt.astimezone(UTC)
    except (OverflowError, ValueError):
        raise ValueError("Instant out of range")
    return dt


[docs] class WheneverWarning(UserWarning): """Base class for all warnings emitted by the ``whenever`` library. This can be used with Python's standard warning filters to suppress or escalate all warnings emitted by ``whenever``: .. code-block:: python import warnings, whenever warnings.filterwarnings("error", category=whenever.WheneverWarning) """
# A custom warnings class to prevent silent deprecation warnings in user code. # See https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries
[docs] class WheneverDeprecationWarning(WheneverWarning): """Raised when a deprecated feature of the ``whenever`` library is used. This is a custom warning class (not a subclass of :class:`DeprecationWarning`) so that deprecation warnings from this library are visible by default—unlike standard ``DeprecationWarning``, which Python silences in production code. """
_T = TypeVar("_T") # Basic behavior common to all classes class _Base: __slots__ = () # Immutable classes don't need to be copied @no_type_check def __copy__(self): return self @no_type_check def __deepcopy__(self, _): return self @no_type_check @classmethod def __get_pydantic_core_schema__(cls, *_, **kwargs): from ._utils import pydantic_schema return pydantic_schema(cls) @classmethod def parse_iso(cls: type[_T], s: str, /) -> _T: raise NotImplementedError # pragma: no cover if TYPE_CHECKING: from typing import final as final # re-export to suppress linting errors else: def final(cls): def init_subclass_not_allowed(cls, **kwargs): # pragma: no cover raise TypeError("Subclassing not allowed") cls.__init_subclass__ = init_subclass_not_allowed return cls _Tcall = TypeVar("_Tcall", bound=Callable[..., None]) # I'd love for this to be a decorator, but every attempt I made resulted # in mypy getting too confused. I've tried a lot. def add_alternate_constructors( init_default: _Tcall, py_type: type | None = None, deprecation_msg: str | None = None, ) -> _Tcall: """Add alternate constructors to a class's __init__ method.""" def __init__(self: Any, *args: Any, **kwargs: Any) -> None: match args: case [str() as iso_string] if not kwargs: if deprecation_msg: warn( deprecation_msg, WheneverDeprecationWarning, stacklevel=2, ) self._init_from_iso(iso_string) case [obj] if ( py_type is not None and not kwargs and isinstance(obj, py_type) ): self._init_from_py(obj) case _: init_default(self, *args, **kwargs) return __init__ # type: ignore[return-value]