diff --git a/.flake8 b/.flake8 index d5712859..1d7308c8 100644 --- a/.flake8 +++ b/.flake8 @@ -4,8 +4,9 @@ per-file-ignores = # F401: Module imported but unused # TC001: Move import into type checking block __init__.py:F401, TC001 + pendulum/date.py:E800 # F811: Redefinition of unused name from line n - pendulum/tz/timezone.py:F811 + pendulum/tz/timezone.py:F811, E800 min_python_version = 3.7.0 ban-relative-imports = true # flake8-use-fstring: https://github.com/MichaelKim0407/flake8-use-fstring#--percent-greedy-and---format-greedy diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 341859e8..7659ec43 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,8 +64,15 @@ jobs: if: steps.cache.outputs.cache-hit == 'true' && matrix.os != 'MacOS' run: timeout 10s poetry run pip --version || rm -rf .venv - - name: Install dependencies - run: poetry install --only main --only test -vvv + - name: Install runtime, testing, and typing dependencies + run: poetry install --only main --only test --only typing -vvv + + - name: Run type checking + run: poetry run mypy + + - name: Uninstall typing dependencies + # This ensures pendulum runs without typing_extensions installed + run: poetry install --only main --only test --sync --no-root -vvv - name: Test Pure Python run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca4f1607..2ee2fd44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,14 +58,3 @@ repos: exclude: ^build\.py$ args: - --py37-plus - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 - hooks: - - id: mypy - pass_filenames: false - exclude: ^build\.py$ - additional_dependencies: - - pytest>=7.1.2 - - types-backports - - types-python-dateutil diff --git a/pendulum/__init__.py b/pendulum/__init__.py index 44912442..72f19eaf 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -4,6 +4,7 @@ from typing import Union from typing import cast +from typing import overload from pendulum.__version__ import __version__ from pendulum.constants import DAYS_PER_WEEK @@ -38,10 +39,10 @@ from pendulum.testing.traveller import Traveller from pendulum.time import Time from pendulum.tz import UTC +from pendulum.tz import fixed_timezone from pendulum.tz import local_timezone from pendulum.tz import set_local_timezone from pendulum.tz import test_local_timezone -from pendulum.tz import timezone from pendulum.tz import timezones from pendulum.tz.timezone import FixedTimezone from pendulum.tz.timezone import Timezone @@ -54,6 +55,34 @@ _formatter = Formatter() +@overload +def timezone(name: int) -> FixedTimezone: + ... + + +@overload +def timezone(name: str) -> Timezone: + ... + + +@overload +def timezone(name: str | int) -> Timezone | FixedTimezone: + ... + + +def timezone(name: str | int) -> Timezone | FixedTimezone: + """ + Return a Timezone instance given its name. + """ + if isinstance(name, int): + return fixed_timezone(name) + + if name.lower() == "utc": + return UTC + + return Timezone(name) + + def _safe_timezone( obj: str | float | _datetime.tzinfo | Timezone | FixedTimezone | None, dt: _datetime.datetime | None = None, @@ -73,7 +102,7 @@ def _safe_timezone( elif isinstance(obj, _datetime.tzinfo): # zoneinfo if hasattr(obj, "key"): - obj = obj.key # type: ignore + obj = obj.key # pytz elif hasattr(obj, "localize"): obj = obj.zone # type: ignore diff --git a/pendulum/_extensions/_helpers.pyi b/pendulum/_extensions/_helpers.pyi index 99a53978..f7763557 100644 --- a/pendulum/_extensions/_helpers.pyi +++ b/pendulum/_extensions/_helpers.pyi @@ -1,8 +1,8 @@ from __future__ import annotations -from collections import namedtuple from datetime import date from datetime import datetime +from typing import NamedTuple def days_in_year(year: int) -> int: ... def is_leap(year: int) -> bool: ... @@ -11,12 +11,7 @@ def local_time( unix_time: int, utc_offset: int, microseconds: int ) -> tuple[int, int, int, int, int, int, int]: ... -class PreciseDiff( - namedtuple( - "PreciseDiff", - "years months days " "hours minutes seconds microseconds " "total_days", - ) -): +class PreciseDiff(NamedTuple): years: int months: int days: int diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py index e9839507..e0b16a33 100644 --- a/pendulum/_extensions/helpers.py +++ b/pendulum/_extensions/helpers.py @@ -3,7 +3,7 @@ import datetime import math -from collections import namedtuple +from typing import NamedTuple from typing import cast from pendulum.constants import DAY_OF_WEEK_TABLE @@ -25,12 +25,7 @@ from pendulum.utils._compat import zoneinfo -class PreciseDiff( - namedtuple( - "PreciseDiff", - "years months days " "hours minutes seconds microseconds " "total_days", - ) -): +class PreciseDiff(NamedTuple): years: int months: int days: int diff --git a/pendulum/date.py b/pendulum/date.py index f2aec479..0d8382a4 100644 --- a/pendulum/date.py +++ b/pendulum/date.py @@ -1,3 +1,5 @@ +# The following is only needed because of Python 3.7 +# mypy: no-warn-unused-ignores from __future__ import annotations import calendar @@ -6,6 +8,7 @@ from datetime import date from datetime import datetime from datetime import timedelta +from typing import TYPE_CHECKING from typing import NoReturn from typing import cast from typing import overload @@ -27,6 +30,10 @@ from pendulum.interval import Interval from pendulum.mixins.default import FormattableMixin +if TYPE_CHECKING: + from typing_extensions import Self + from typing_extensions import SupportsIndex + class Date(FormattableMixin, date): # Names of days of the week @@ -46,7 +53,7 @@ class Date(FormattableMixin, date): def set( self, year: int | None = None, month: int | None = None, day: int | None = None - ) -> Date: + ) -> Self: return self.replace(year=year, month=month, day=day) @property @@ -110,7 +117,7 @@ def __repr__(self) -> str: # COMPARISONS - def closest(self, dt1: date, dt2: date) -> Date: + def closest(self, dt1: date, dt2: date) -> Self: """ Get the closest date from the instance. """ @@ -122,7 +129,7 @@ def closest(self, dt1: date, dt2: date) -> Date: return dt2 - def farthest(self, dt1: date, dt2: date) -> Date: + def farthest(self, dt1: date, dt2: date) -> Self: """ Get the farthest date from the instance. """ @@ -188,7 +195,7 @@ def is_anniversary(self, dt: date | None = None) -> bool: def add( self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 - ) -> Date: + ) -> Self: """ Add duration to the instance. @@ -209,7 +216,7 @@ def add( def subtract( self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 - ) -> Date: + ) -> Self: """ Remove duration from the instance. @@ -220,7 +227,7 @@ def subtract( """ return self.add(years=-years, months=-months, weeks=-weeks, days=-days) - def _add_timedelta(self, delta: timedelta) -> Date: + def _add_timedelta(self, delta: timedelta) -> Self: """ Add timedelta duration to the instance. @@ -236,7 +243,7 @@ def _add_timedelta(self, delta: timedelta) -> Date: return self.add(days=delta.days) - def _subtract_timedelta(self, delta: timedelta) -> Date: + def _subtract_timedelta(self, delta: timedelta) -> Self: """ Remove timedelta duration from the instance. @@ -252,25 +259,25 @@ def _subtract_timedelta(self, delta: timedelta) -> Date: return self.subtract(days=delta.days) - def __add__(self, other: timedelta) -> Date: + def __add__(self, other: timedelta) -> Self: if not isinstance(other, timedelta): return NotImplemented return self._add_timedelta(other) - @overload - def __sub__(self, delta: timedelta) -> Date: + @overload # type: ignore[override] # this is only needed because of Python 3.7 + def __sub__(self, __delta: timedelta) -> Self: ... @overload - def __sub__(self, dt: datetime) -> NoReturn: + def __sub__(self, __dt: datetime) -> NoReturn: ... @overload - def __sub__(self, dt: Date) -> Interval: + def __sub__(self, __dt: Self) -> Interval: ... - def __sub__(self, other: timedelta | date) -> Date | Interval: + def __sub__(self, other: timedelta | date) -> Self | Interval: if isinstance(other, timedelta): return self._subtract_timedelta(other) @@ -335,7 +342,7 @@ def diff_for_humans( # MODIFIERS - def start_of(self, unit: str) -> Date: + def start_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -352,9 +359,9 @@ def start_of(self, unit: str) -> Date: if unit not in self._MODIFIERS_VALID_UNITS: raise ValueError(f'Invalid unit "{unit}" for start_of()') - return cast(Date, getattr(self, f"_start_of_{unit}")()) + return cast("Self", getattr(self, f"_start_of_{unit}")()) - def end_of(self, unit: str) -> Date: + def end_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -370,45 +377,45 @@ def end_of(self, unit: str) -> Date: if unit not in self._MODIFIERS_VALID_UNITS: raise ValueError(f'Invalid unit "{unit}" for end_of()') - return cast(Date, getattr(self, f"_end_of_{unit}")()) + return cast("Self", getattr(self, f"_end_of_{unit}")()) - def _start_of_day(self) -> Date: + def _start_of_day(self) -> Self: """ Compatibility method. """ return self - def _end_of_day(self) -> Date: + def _end_of_day(self) -> Self: """ Compatibility method """ return self - def _start_of_month(self) -> Date: + def _start_of_month(self) -> Self: """ Reset the date to the first day of the month. """ return self.set(self.year, self.month, 1) - def _end_of_month(self) -> Date: + def _end_of_month(self) -> Self: """ Reset the date to the last day of the month. """ return self.set(self.year, self.month, self.days_in_month) - def _start_of_year(self) -> Date: + def _start_of_year(self) -> Self: """ Reset the date to the first day of the year. """ return self.set(self.year, 1, 1) - def _end_of_year(self) -> Date: + def _end_of_year(self) -> Self: """ Reset the date to the last day of the year. """ return self.set(self.year, 12, 31) - def _start_of_decade(self) -> Date: + def _start_of_decade(self) -> Self: """ Reset the date to the first day of the decade. """ @@ -416,7 +423,7 @@ def _start_of_decade(self) -> Date: return self.set(year, 1, 1) - def _end_of_decade(self) -> Date: + def _end_of_decade(self) -> Self: """ Reset the date to the last day of the decade. """ @@ -424,7 +431,7 @@ def _end_of_decade(self) -> Date: return self.set(year, 12, 31) - def _start_of_century(self) -> Date: + def _start_of_century(self) -> Self: """ Reset the date to the first day of the century. """ @@ -432,7 +439,7 @@ def _start_of_century(self) -> Date: return self.set(year, 1, 1) - def _end_of_century(self) -> Date: + def _end_of_century(self) -> Self: """ Reset the date to the last day of the century. """ @@ -440,7 +447,7 @@ def _end_of_century(self) -> Date: return self.set(year, 12, 31) - def _start_of_week(self) -> Date: + def _start_of_week(self) -> Self: """ Reset the date to the first day of the week. """ @@ -451,7 +458,7 @@ def _start_of_week(self) -> Date: return dt.start_of("day") - def _end_of_week(self) -> Date: + def _end_of_week(self) -> Self: """ Reset the date to the last day of the week. """ @@ -462,7 +469,7 @@ def _end_of_week(self) -> Date: return dt.end_of("day") - def next(self, day_of_week: int | None = None) -> Date: + def next(self, day_of_week: int | None = None) -> Self: """ Modify to the next occurrence of a given day of the week. If no day_of_week is provided, modify to the next occurrence @@ -483,7 +490,7 @@ def next(self, day_of_week: int | None = None) -> Date: return dt - def previous(self, day_of_week: int | None = None) -> Date: + def previous(self, day_of_week: int | None = None) -> Self: """ Modify to the previous occurrence of a given day of the week. If no day_of_week is provided, modify to the previous occurrence @@ -504,7 +511,7 @@ def previous(self, day_of_week: int | None = None) -> Date: return dt - def first_of(self, unit: str, day_of_week: int | None = None) -> Date: + def first_of(self, unit: str, day_of_week: int | None = None) -> Self: """ Returns an instance set to the first occurrence of a given day of the week in the current unit. @@ -519,9 +526,9 @@ def first_of(self, unit: str, day_of_week: int | None = None) -> Date: if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - return cast(Date, getattr(self, f"_first_of_{unit}")(day_of_week)) + return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week)) - def last_of(self, unit: str, day_of_week: int | None = None) -> Date: + def last_of(self, unit: str, day_of_week: int | None = None) -> Self: """ Returns an instance set to the last occurrence of a given day of the week in the current unit. @@ -536,9 +543,9 @@ def last_of(self, unit: str, day_of_week: int | None = None) -> Date: if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - return cast(Date, getattr(self, f"_last_of_{unit}")(day_of_week)) + return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week)) - def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date: + def nth_of(self, unit: str, nth: int, day_of_week: int) -> Self: """ Returns a new instance set to the given occurrence of a given day of the week in the current unit. @@ -555,7 +562,7 @@ def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date: if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - dt = cast(Date, getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) + dt = cast("Self", getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) if not dt: raise PendulumException( f"Unable to find occurence {nth}" @@ -564,7 +571,7 @@ def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date: return dt - def _first_of_month(self, day_of_week: int) -> Date: + def _first_of_month(self, day_of_week: int) -> Self: """ Modify to the first occurrence of a given day of the week in the current month. If no day_of_week is provided, @@ -589,7 +596,7 @@ def _first_of_month(self, day_of_week: int) -> Date: return dt.set(day=day_of_month) - def _last_of_month(self, day_of_week: int | None = None) -> Date: + def _last_of_month(self, day_of_week: int | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current month. If no day_of_week is provided, @@ -614,7 +621,7 @@ def _last_of_month(self, day_of_week: int | None = None) -> Date: return dt.set(day=day_of_month) - def _nth_of_month(self, nth: int, day_of_week: int) -> Date | None: + def _nth_of_month(self, nth: int, day_of_week: int) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current month. If the calculated occurrence is outside, @@ -635,7 +642,7 @@ def _nth_of_month(self, nth: int, day_of_week: int) -> Date | None: return None - def _first_of_quarter(self, day_of_week: int | None = None) -> Date: + def _first_of_quarter(self, day_of_week: int | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current quarter. If no day_of_week is provided, @@ -646,7 +653,7 @@ def _first_of_quarter(self, day_of_week: int | None = None) -> Date: "month", day_of_week ) - def _last_of_quarter(self, day_of_week: int | None = None) -> Date: + def _last_of_quarter(self, day_of_week: int | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current quarter. If no day_of_week is provided, @@ -655,7 +662,7 @@ def _last_of_quarter(self, day_of_week: int | None = None) -> Date: """ return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week) - def _nth_of_quarter(self, nth: int, day_of_week: int) -> Date | None: + def _nth_of_quarter(self, nth: int, day_of_week: int) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current quarter. If the calculated occurrence is outside, @@ -678,7 +685,7 @@ def _nth_of_quarter(self, nth: int, day_of_week: int) -> Date | None: return self.set(self.year, dt.month, dt.day) - def _first_of_year(self, day_of_week: int | None = None) -> Date: + def _first_of_year(self, day_of_week: int | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current year. If no day_of_week is provided, @@ -687,7 +694,7 @@ def _first_of_year(self, day_of_week: int | None = None) -> Date: """ return self.set(month=1).first_of("month", day_of_week) - def _last_of_year(self, day_of_week: int | None = None) -> Date: + def _last_of_year(self, day_of_week: int | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current year. If no day_of_week is provided, @@ -696,7 +703,7 @@ def _last_of_year(self, day_of_week: int | None = None) -> Date: """ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) - def _nth_of_year(self, nth: int, day_of_week: int) -> Date | None: + def _nth_of_year(self, nth: int, day_of_week: int) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current year. If the calculated occurrence is outside, @@ -717,7 +724,7 @@ def _nth_of_year(self, nth: int, day_of_week: int) -> Date | None: return self.set(self.year, dt.month, dt.day) - def average(self, dt: date | None = None) -> Date: + def average(self, dt: date | None = None) -> Self: """ Modify the current instance to the average of a given instance (default now) and the current instance. @@ -730,29 +737,29 @@ def average(self, dt: date | None = None) -> Date: # Native methods override @classmethod - def today(cls) -> Date: + def today(cls) -> Self: dt = date.today() return cls(dt.year, dt.month, dt.day) @classmethod - def fromtimestamp(cls, t: float) -> Date: + def fromtimestamp(cls, t: float) -> Self: dt = super().fromtimestamp(t) return cls(dt.year, dt.month, dt.day) @classmethod - def fromordinal(cls, n: int) -> Date: + def fromordinal(cls, n: int) -> Self: dt = super().fromordinal(n) return cls(dt.year, dt.month, dt.day) def replace( self, - year: int | None = None, - month: int | None = None, - day: int | None = None, - ) -> Date: + year: SupportsIndex | None = None, + month: SupportsIndex | None = None, + day: SupportsIndex | None = None, + ) -> Self: year = year if year is not None else self.year month = month if month is not None else self.month day = day if day is not None else self.day diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 52ad3cc7..d265aece 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -2,10 +2,12 @@ import calendar import datetime +import sys from typing import TYPE_CHECKING from typing import Any from typing import Callable +from typing import ClassVar from typing import Optional from typing import cast from typing import overload @@ -38,14 +40,17 @@ from pendulum.tz import local_timezone from pendulum.tz.timezone import FixedTimezone from pendulum.tz.timezone import Timezone -from pendulum.utils._compat import PY38 if TYPE_CHECKING: - from typing import Literal + from typing_extensions import Literal + from typing_extensions import Self + from typing_extensions import SupportsIndex class DateTime(datetime.datetime, Date): - EPOCH: DateTime + EPOCH: ClassVar[DateTime] + min: ClassVar[DateTime] + max: ClassVar[DateTime] # Formats @@ -80,17 +85,17 @@ class DateTime(datetime.datetime, Date): @classmethod def create( cls, - year: int, - month: int, - day: int, - hour: int = 0, - minute: int = 0, - second: int = 0, - microsecond: int = 0, + year: SupportsIndex, + month: SupportsIndex, + day: SupportsIndex, + hour: SupportsIndex = 0, + minute: SupportsIndex = 0, + second: SupportsIndex = 0, + microsecond: SupportsIndex = 0, tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC, fold: int = 1, raise_on_unknown_times: bool = False, - ) -> DateTime: + ) -> Self: """ Creates a new DateTime instance from a specific date and time. """ @@ -118,18 +123,18 @@ def create( @overload @classmethod - def now(cls, tz: datetime.tzinfo | None = None) -> DateTime: + def now(cls, tz: datetime.tzinfo | None = None) -> Self: ... @overload @classmethod - def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> DateTime: + def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> Self: ... @classmethod def now( cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None - ) -> DateTime: + ) -> Self: """ Get a DateTime instance for the current date and time. """ @@ -155,14 +160,14 @@ def now( ) @classmethod - def utcnow(cls) -> DateTime: + def utcnow(cls) -> Self: """ Get a DateTime instance for the current date and time in UTC. """ return cls.now(UTC) @classmethod - def today(cls) -> DateTime: + def today(cls) -> Self: return cls.now() @classmethod @@ -181,7 +186,7 @@ def set( second: int | None = None, microsecond: int | None = None, tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None, - ) -> DateTime: + ) -> Self: if year is None: year = self.year if month is None: @@ -199,7 +204,7 @@ def set( if tz is None: tz = self.tz - return DateTime.create( + return self.__class__.create( year, month, day, hour, minute, second, microsecond, tz=tz ) @@ -286,7 +291,7 @@ def date(self) -> Date: def time(self) -> Time: return Time(self.hour, self.minute, self.second, self.microsecond) - def naive(self) -> DateTime: + def naive(self) -> Self: """ Return the DateTime without timezone information. """ @@ -300,7 +305,7 @@ def naive(self) -> DateTime: self.microsecond, ) - def on(self, year: int, month: int, day: int) -> DateTime: + def on(self, year: int, month: int, day: int) -> Self: """ Returns a new instance with the current date set to a different date. """ @@ -308,7 +313,7 @@ def on(self, year: int, month: int, day: int) -> DateTime: def at( self, hour: int, minute: int = 0, second: int = 0, microsecond: int = 0 - ) -> DateTime: + ) -> Self: """ Returns a new instance with the current time to a different time. """ @@ -316,7 +321,7 @@ def at( hour=hour, minute=minute, second=second, microsecond=microsecond ) - def in_timezone(self, tz: str | Timezone | FixedTimezone) -> DateTime: + def in_timezone(self, tz: str | Timezone | FixedTimezone) -> Self: """ Set the instance's timezone from a string or object. """ @@ -326,9 +331,9 @@ def in_timezone(self, tz: str | Timezone | FixedTimezone) -> DateTime: if not self.timezone: dt = dt.replace(fold=1) - return cast(DateTime, tz.convert(dt)) + return tz.convert(dt) - def in_tz(self, tz: str | Timezone | FixedTimezone) -> DateTime: + def in_tz(self, tz: str | Timezone | FixedTimezone) -> Self: """ Set the instance's timezone from a string or object. """ @@ -540,7 +545,7 @@ def add( minutes: int = 0, seconds: float = 0, microseconds: int = 0, - ) -> DateTime: + ) -> Self: """ Add a duration to the instance. @@ -577,7 +582,7 @@ def add( ) if units_of_variable_length or self.tz is None: - return DateTime.create( + return self.__class__.create( dt.year, dt.month, dt.day, @@ -623,7 +628,7 @@ def subtract( minutes: int = 0, seconds: float = 0, microseconds: int = 0, - ) -> DateTime: + ) -> Self: """ Remove duration from the instance. """ @@ -641,7 +646,7 @@ def subtract( # Adding a final underscore to the method name # to avoid errors for PyPy which already defines # a _add_timedelta method - def _add_timedelta_(self, delta: datetime.timedelta) -> DateTime: + def _add_timedelta_(self, delta: datetime.timedelta) -> Self: """ Add timedelta duration to the instance. """ @@ -663,7 +668,7 @@ def _add_timedelta_(self, delta: datetime.timedelta) -> DateTime: return self.add(seconds=delta.total_seconds()) - def _subtract_timedelta(self, delta: datetime.timedelta) -> DateTime: + def _subtract_timedelta(self, delta: datetime.timedelta) -> Self: """ Remove timedelta duration from the instance. """ @@ -722,7 +727,7 @@ def diff_for_humans( # type: ignore[override] return pendulum.format_diff(diff, is_now, absolute, locale) # Modifiers - def start_of(self, unit: str) -> DateTime: + def start_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -740,9 +745,9 @@ def start_of(self, unit: str) -> DateTime: if unit not in self._MODIFIERS_VALID_UNITS: raise ValueError(f'Invalid unit "{unit}" for start_of()') - return cast(DateTime, getattr(self, f"_start_of_{unit}")()) + return cast("Self", getattr(self, f"_start_of_{unit}")()) - def end_of(self, unit: str) -> DateTime: + def end_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -760,83 +765,83 @@ def end_of(self, unit: str) -> DateTime: if unit not in self._MODIFIERS_VALID_UNITS: raise ValueError(f'Invalid unit "{unit}" for end_of()') - return cast(DateTime, getattr(self, f"_end_of_{unit}")()) + return cast("Self", getattr(self, f"_end_of_{unit}")()) - def _start_of_second(self) -> DateTime: + def _start_of_second(self) -> Self: """ Reset microseconds to 0. """ return self.set(microsecond=0) - def _end_of_second(self) -> DateTime: + def _end_of_second(self) -> Self: """ Set microseconds to 999999. """ return self.set(microsecond=999999) - def _start_of_minute(self) -> DateTime: + def _start_of_minute(self) -> Self: """ Reset seconds and microseconds to 0. """ return self.set(second=0, microsecond=0) - def _end_of_minute(self) -> DateTime: + def _end_of_minute(self) -> Self: """ Set seconds to 59 and microseconds to 999999. """ return self.set(second=59, microsecond=999999) - def _start_of_hour(self) -> DateTime: + def _start_of_hour(self) -> Self: """ Reset minutes, seconds and microseconds to 0. """ return self.set(minute=0, second=0, microsecond=0) - def _end_of_hour(self) -> DateTime: + def _end_of_hour(self) -> Self: """ Set minutes and seconds to 59 and microseconds to 999999. """ return self.set(minute=59, second=59, microsecond=999999) - def _start_of_day(self) -> DateTime: + def _start_of_day(self) -> Self: """ Reset the time to 00:00:00. """ return self.at(0, 0, 0, 0) - def _end_of_day(self) -> DateTime: + def _end_of_day(self) -> Self: """ Reset the time to 23:59:59.999999. """ return self.at(23, 59, 59, 999999) - def _start_of_month(self) -> DateTime: + def _start_of_month(self) -> Self: """ Reset the date to the first day of the month and the time to 00:00:00. """ return self.set(self.year, self.month, 1, 0, 0, 0, 0) - def _end_of_month(self) -> DateTime: + def _end_of_month(self) -> Self: """ Reset the date to the last day of the month and the time to 23:59:59.999999. """ return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999) - def _start_of_year(self) -> DateTime: + def _start_of_year(self) -> Self: """ Reset the date to the first day of the year and the time to 00:00:00. """ return self.set(self.year, 1, 1, 0, 0, 0, 0) - def _end_of_year(self) -> DateTime: + def _end_of_year(self) -> Self: """ Reset the date to the last day of the year and the time to 23:59:59.999999. """ return self.set(self.year, 12, 31, 23, 59, 59, 999999) - def _start_of_decade(self) -> DateTime: + def _start_of_decade(self) -> Self: """ Reset the date to the first day of the decade and the time to 00:00:00. @@ -844,7 +849,7 @@ def _start_of_decade(self) -> DateTime: year = self.year - self.year % YEARS_PER_DECADE return self.set(year, 1, 1, 0, 0, 0, 0) - def _end_of_decade(self) -> DateTime: + def _end_of_decade(self) -> Self: """ Reset the date to the last day of the decade and the time to 23:59:59.999999. @@ -853,7 +858,7 @@ def _end_of_decade(self) -> DateTime: return self.set(year, 12, 31, 23, 59, 59, 999999) - def _start_of_century(self) -> DateTime: + def _start_of_century(self) -> Self: """ Reset the date to the first day of the century and the time to 00:00:00. @@ -862,7 +867,7 @@ def _start_of_century(self) -> DateTime: return self.set(year, 1, 1, 0, 0, 0, 0) - def _end_of_century(self) -> DateTime: + def _end_of_century(self) -> Self: """ Reset the date to the last day of the century and the time to 23:59:59.999999. @@ -871,7 +876,7 @@ def _end_of_century(self) -> DateTime: return self.set(year, 12, 31, 23, 59, 59, 999999) - def _start_of_week(self) -> DateTime: + def _start_of_week(self) -> Self: """ Reset the date to the first day of the week and the time to 00:00:00. @@ -883,7 +888,7 @@ def _start_of_week(self) -> DateTime: return dt.start_of("day") - def _end_of_week(self) -> DateTime: + def _end_of_week(self) -> Self: """ Reset the date to the last day of the week and the time to 23:59:59. @@ -895,7 +900,7 @@ def _end_of_week(self) -> DateTime: return dt.end_of("day") - def next(self, day_of_week: int | None = None, keep_time: bool = False) -> DateTime: + def next(self, day_of_week: int | None = None, keep_time: bool = False) -> Self: """ Modify to the next occurrence of a given day of the week. If no day_of_week is provided, modify to the next occurrence @@ -919,9 +924,7 @@ def next(self, day_of_week: int | None = None, keep_time: bool = False) -> DateT return dt - def previous( - self, day_of_week: int | None = None, keep_time: bool = False - ) -> DateTime: + def previous(self, day_of_week: int | None = None, keep_time: bool = False) -> Self: """ Modify to the previous occurrence of a given day of the week. If no day_of_week is provided, modify to the previous occurrence @@ -945,7 +948,7 @@ def previous( return dt - def first_of(self, unit: str, day_of_week: int | None = None) -> DateTime: + def first_of(self, unit: str, day_of_week: int | None = None) -> Self: """ Returns an instance set to the first occurrence of a given day of the week in the current unit. @@ -957,9 +960,9 @@ def first_of(self, unit: str, day_of_week: int | None = None) -> DateTime: if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - return cast(DateTime, getattr(self, f"_first_of_{unit}")(day_of_week)) + return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week)) - def last_of(self, unit: str, day_of_week: int | None = None) -> DateTime: + def last_of(self, unit: str, day_of_week: int | None = None) -> Self: """ Returns an instance set to the last occurrence of a given day of the week in the current unit. @@ -971,9 +974,9 @@ def last_of(self, unit: str, day_of_week: int | None = None) -> DateTime: if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - return cast(DateTime, getattr(self, f"_last_of_{unit}")(day_of_week)) + return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week)) - def nth_of(self, unit: str, nth: int, day_of_week: int) -> DateTime: + def nth_of(self, unit: str, nth: int, day_of_week: int) -> Self: """ Returns a new instance set to the given occurrence of a given day of the week in the current unit. @@ -986,9 +989,7 @@ def nth_of(self, unit: str, nth: int, day_of_week: int) -> DateTime: if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - dt = cast( - Optional[DateTime], getattr(self, f"_nth_of_{unit}")(nth, day_of_week) - ) + dt = cast(Optional["Self"], getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) if not dt: raise PendulumException( f"Unable to find occurence {nth}" @@ -997,7 +998,7 @@ def nth_of(self, unit: str, nth: int, day_of_week: int) -> DateTime: return dt - def _first_of_month(self, day_of_week: int | None = None) -> DateTime: + def _first_of_month(self, day_of_week: int | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current month. If no day_of_week is provided, @@ -1020,7 +1021,7 @@ def _first_of_month(self, day_of_week: int | None = None) -> DateTime: return dt.set(day=day_of_month) - def _last_of_month(self, day_of_week: int | None = None) -> DateTime: + def _last_of_month(self, day_of_week: int | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current month. If no day_of_week is provided, @@ -1043,9 +1044,7 @@ def _last_of_month(self, day_of_week: int | None = None) -> DateTime: return dt.set(day=day_of_month) - def _nth_of_month( - self, nth: int, day_of_week: int | None = None - ) -> DateTime | None: + def _nth_of_month(self, nth: int, day_of_week: int | None = None) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current month. If the calculated occurrence is outside, @@ -1066,7 +1065,7 @@ def _nth_of_month( return None - def _first_of_quarter(self, day_of_week: int | None = None) -> DateTime: + def _first_of_quarter(self, day_of_week: int | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current quarter. If no day_of_week is provided, @@ -1077,7 +1076,7 @@ def _first_of_quarter(self, day_of_week: int | None = None) -> DateTime: "month", day_of_week ) - def _last_of_quarter(self, day_of_week: int | None = None) -> DateTime: + def _last_of_quarter(self, day_of_week: int | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current quarter. If no day_of_week is provided, @@ -1086,9 +1085,7 @@ def _last_of_quarter(self, day_of_week: int | None = None) -> DateTime: """ return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week) - def _nth_of_quarter( - self, nth: int, day_of_week: int | None = None - ) -> DateTime | None: + def _nth_of_quarter(self, nth: int, day_of_week: int | None = None) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current quarter. If the calculated occurrence is outside, @@ -1111,7 +1108,7 @@ def _nth_of_quarter( return self.on(self.year, dt.month, dt.day).start_of("day") - def _first_of_year(self, day_of_week: int | None = None) -> DateTime: + def _first_of_year(self, day_of_week: int | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current year. If no day_of_week is provided, @@ -1120,7 +1117,7 @@ def _first_of_year(self, day_of_week: int | None = None) -> DateTime: """ return self.set(month=1).first_of("month", day_of_week) - def _last_of_year(self, day_of_week: int | None = None) -> DateTime: + def _last_of_year(self, day_of_week: int | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current year. If no day_of_week is provided, @@ -1129,7 +1126,7 @@ def _last_of_year(self, day_of_week: int | None = None) -> DateTime: """ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) - def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime | None: + def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current year. If the calculated occurrence is outside, @@ -1152,7 +1149,7 @@ def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime | N def average( # type: ignore[override] self, dt: datetime.datetime | None = None - ) -> DateTime: + ) -> Self: """ Modify the current instance to the average of a given instance (default now) and the current instance. @@ -1166,16 +1163,14 @@ def average( # type: ignore[override] ) @overload # type: ignore[override] - def __sub__(self, other: datetime.timedelta) -> DateTime: + def __sub__(self, other: datetime.timedelta) -> Self: ... @overload def __sub__(self, other: DateTime) -> Interval: ... - def __sub__( - self, other: datetime.datetime | datetime.timedelta - ) -> DateTime | Interval: + def __sub__(self, other: datetime.datetime | datetime.timedelta) -> Self | Interval: if isinstance(other, datetime.timedelta): return self._subtract_timedelta(other) @@ -1218,11 +1213,11 @@ def __rsub__(self, other: datetime.datetime) -> Interval: return self.diff(other, False) - def __add__(self, other: datetime.timedelta) -> DateTime: + def __add__(self, other: datetime.timedelta) -> Self: if not isinstance(other, datetime.timedelta): return NotImplemented - if PY38: + if sys.version_info >= (3, 8): # This is a workaround for Python 3.8+ # since calling astimezone() will call this method # instead of the base datetime class one. @@ -1230,11 +1225,11 @@ def __add__(self, other: datetime.timedelta) -> DateTime: caller = inspect.stack()[1][3] if caller == "astimezone": - return cast(DateTime, super().__add__(other)) + return super().__add__(other) return self._add_timedelta_(other) - def __radd__(self, other: datetime.timedelta) -> DateTime: + def __radd__(self, other: datetime.timedelta) -> Self: return self.__add__(other) # Native methods override @@ -1264,7 +1259,7 @@ def combine( ) -> DateTime: return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo) - def astimezone(self, tz: datetime.tzinfo | None = None) -> DateTime: + def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: dt = super().astimezone(tz) return self.__class__( @@ -1281,16 +1276,16 @@ def astimezone(self, tz: datetime.tzinfo | None = None) -> DateTime: def replace( self, - year: int | None = None, - month: int | None = None, - day: int | None = None, - hour: int | None = None, - minute: int | None = None, - second: int | None = None, - microsecond: int | None = None, + year: SupportsIndex | None = None, + month: SupportsIndex | None = None, + day: SupportsIndex | None = None, + hour: SupportsIndex | None = None, + minute: SupportsIndex | None = None, + second: SupportsIndex | None = None, + microsecond: SupportsIndex | None = None, tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, fold: int | None = None, - ) -> DateTime: + ) -> Self: if year is None: year = self.year if month is None: @@ -1313,7 +1308,7 @@ def replace( if tzinfo is not None: tzinfo = pendulum._safe_timezone(tzinfo) - return DateTime.create( + return self.__class__.create( year, month, day, @@ -1329,7 +1324,7 @@ def __getnewargs__(self) -> tuple[DateTime]: return (self,) def _getstate( - self, protocol: int = 3 + self, protocol: SupportsIndex = 3 ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]: return ( self.year, @@ -1349,8 +1344,8 @@ def __reduce__( ]: return self.__reduce_ex__(2) - def __reduce_ex__( # type: ignore[override] - self, protocol: int + def __reduce_ex__( + self, protocol: SupportsIndex ) -> tuple[ type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] ]: @@ -1374,8 +1369,6 @@ def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int: return 0 if dt == other else 1 if dt > other else -1 -DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) # type: ignore[misc] -DateTime.max: DateTime = DateTime( # type: ignore[misc] - 9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC -) -DateTime.EPOCH: DateTime = DateTime(1970, 1, 1, tzinfo=UTC) # type: ignore[misc] +DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) +DateTime.max = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC) +DateTime.EPOCH = DateTime(1970, 1, 1, tzinfo=UTC) diff --git a/pendulum/duration.py b/pendulum/duration.py index a3a68b1a..2e9daf2d 100644 --- a/pendulum/duration.py +++ b/pendulum/duration.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING from typing import cast from typing import overload @@ -12,6 +13,9 @@ from pendulum.constants import US_PER_SECOND from pendulum.utils._compat import PYPY +if TYPE_CHECKING: + from typing_extensions import Self + def _divide_and_round(a: float, b: float) -> int: """divide a by b and round result to the nearest integer @@ -74,7 +78,7 @@ def __new__( weeks: float = 0, years: float = 0, months: float = 0, - ) -> Duration: + ) -> Self: if not isinstance(years, int) or not isinstance(months, int): raise ValueError("Float year and months are not supported") @@ -313,7 +317,7 @@ def __repr__(self) -> str: return rep.replace(", )", ")") - def __add__(self, other: timedelta) -> Duration: + def __add__(self, other: timedelta) -> Self: if isinstance(other, timedelta): return self.__class__(seconds=self.total_seconds() + other.total_seconds()) @@ -321,13 +325,13 @@ def __add__(self, other: timedelta) -> Duration: __radd__ = __add__ - def __sub__(self, other: timedelta) -> Duration: + def __sub__(self, other: timedelta) -> Self: if isinstance(other, timedelta): return self.__class__(seconds=self.total_seconds() - other.total_seconds()) return NotImplemented - def __neg__(self) -> Duration: + def __neg__(self) -> Self: return self.__class__( years=-self._years, months=-self._months, @@ -340,7 +344,7 @@ def __neg__(self) -> Duration: def _to_microseconds(self) -> int: return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds - def __mul__(self, other: int | float) -> Duration: + def __mul__(self, other: int | float) -> Self: if isinstance(other, int): return self.__class__( years=self._years * other, @@ -363,7 +367,7 @@ def __floordiv__(self, other: timedelta) -> int: ... @overload - def __floordiv__(self, other: int) -> Duration: + def __floordiv__(self, other: int) -> Self: ... def __floordiv__(self, other: int | timedelta) -> int | Duration: @@ -388,10 +392,10 @@ def __truediv__(self, other: timedelta) -> float: ... @overload - def __truediv__(self, other: float) -> Duration: + def __truediv__(self, other: float) -> Self: ... - def __truediv__(self, other: int | float | timedelta) -> Duration | float: + def __truediv__(self, other: int | float | timedelta) -> Self | float: if not isinstance(other, (int, float, timedelta)): return NotImplemented @@ -421,7 +425,7 @@ def __truediv__(self, other: int | float | timedelta) -> Duration | float: __div__ = __floordiv__ - def __mod__(self, other: timedelta) -> Duration: + def __mod__(self, other: timedelta) -> Self: if isinstance(other, timedelta): r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] diff --git a/pendulum/helpers.py b/pendulum/helpers.py index 13b7f221..72c527db 100644 --- a/pendulum/helpers.py +++ b/pendulum/helpers.py @@ -40,12 +40,12 @@ from pendulum._extensions._helpers import timestamp from pendulum._extensions._helpers import week_day except ImportError: - from pendulum._extensions.helpers import PreciseDiff # type: ignore[misc] + from pendulum._extensions.helpers import PreciseDiff # type: ignore[assignment] from pendulum._extensions.helpers import days_in_year from pendulum._extensions.helpers import is_leap from pendulum._extensions.helpers import is_long_year from pendulum._extensions.helpers import local_time - from pendulum._extensions.helpers import precise_diff # type: ignore[misc] + from pendulum._extensions.helpers import precise_diff # type: ignore[assignment] from pendulum._extensions.helpers import timestamp from pendulum._extensions.helpers import week_day @@ -54,7 +54,7 @@ @overload def add_duration( - dt: datetime, + dt: _DT, years: int = 0, months: int = 0, weeks: int = 0, @@ -63,18 +63,18 @@ def add_duration( minutes: int = 0, seconds: float = 0, microseconds: int = 0, -) -> datetime: +) -> _DT: ... @overload def add_duration( - dt: date, + dt: _D, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, -) -> date: +) -> _D: pass diff --git a/pendulum/interval.py b/pendulum/interval.py index f20042bf..ed2cfe63 100644 --- a/pendulum/interval.py +++ b/pendulum/interval.py @@ -18,7 +18,8 @@ from pendulum.helpers import precise_diff if TYPE_CHECKING: - from typing import SupportsIndex + from typing_extensions import Self + from typing_extensions import SupportsIndex from pendulum.helpers import PreciseDiff from pendulum.locales.locale import Locale # noqa @@ -35,7 +36,7 @@ def __new__( start: pendulum.DateTime | datetime, end: pendulum.DateTime | datetime, absolute: bool = False, - ) -> Interval: + ) -> Self: ... @overload @@ -44,7 +45,7 @@ def __new__( start: pendulum.Date | date, end: pendulum.Date | date, absolute: bool = False, - ) -> Interval: + ) -> Self: ... def __new__( @@ -52,7 +53,7 @@ def __new__( start: pendulum.DateTime | pendulum.Date | datetime | date, end: pendulum.DateTime | pendulum.Date | datetime | date, absolute: bool = False, - ) -> Interval: + ) -> Self: if ( isinstance(start, datetime) and not isinstance(end, datetime) @@ -125,7 +126,7 @@ def __new__( delta: timedelta = _end - _start # type: ignore[operator] - return cast(Interval, super().__new__(cls, seconds=delta.total_seconds())) + return super().__new__(cls, seconds=delta.total_seconds()) def __init__( self, @@ -316,9 +317,9 @@ def range( i += amount - def as_interval(self) -> Duration: + def as_duration(self) -> Duration: """ - Return the Period as a Duration. + Return the Interval as a Duration. """ return Duration(seconds=self.total_seconds()) @@ -330,23 +331,23 @@ def __contains__( ) -> bool: return self.start <= item <= self.end - def __add__(self, other: timedelta) -> Duration: - return self.as_interval().__add__(other) + def __add__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__add__(other) - __radd__ = __add__ + __radd__ = __add__ # type: ignore[assignment] - def __sub__(self, other: timedelta) -> Duration: - return self.as_interval().__sub__(other) + def __sub__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__sub__(other) - def __neg__(self) -> Interval: + def __neg__(self) -> Self: return self.__class__(self.end, self.start, self._absolute) - def __mul__(self, other: int | float) -> Duration: - return self.as_interval().__mul__(other) + def __mul__(self, other: int | float) -> Duration: # type: ignore[override] + return self.as_duration().__mul__(other) - __rmul__ = __mul__ + __rmul__ = __mul__ # type: ignore[assignment] - @overload + @overload # type: ignore[override] def __floordiv__(self, other: timedelta) -> int: ... @@ -355,11 +356,11 @@ def __floordiv__(self, other: int) -> Duration: ... def __floordiv__(self, other: int | timedelta) -> int | Duration: - return self.as_interval().__floordiv__(other) + return self.as_duration().__floordiv__(other) __div__ = __floordiv__ # type: ignore[assignment] - @overload + @overload # type: ignore[override] def __truediv__(self, other: timedelta) -> float: ... @@ -368,15 +369,15 @@ def __truediv__(self, other: float) -> Duration: ... def __truediv__(self, other: float | timedelta) -> Duration | float: - return self.as_interval().__truediv__(other) + return self.as_duration().__truediv__(other) - def __mod__(self, other: timedelta) -> Duration: - return self.as_interval().__mod__(other) + def __mod__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__mod__(other) def __divmod__(self, other: timedelta) -> tuple[int, Duration]: - return self.as_interval().__divmod__(other) + return self.as_duration().__divmod__(other) - def __abs__(self) -> Interval: + def __abs__(self) -> Self: return self.__class__(self.start, self.end, absolute=True) def __repr__(self) -> str: @@ -413,7 +414,7 @@ def _getstate( def __reduce__( self, ) -> tuple[ - type[Interval], + type[Self], tuple[ pendulum.DateTime | pendulum.Date | datetime | date, pendulum.DateTime | pendulum.Date | datetime | date, @@ -425,7 +426,7 @@ def __reduce__( def __reduce_ex__( self, protocol: SupportsIndex ) -> tuple[ - type[Interval], + type[Self], tuple[ pendulum.DateTime | pendulum.Date | datetime | date, pendulum.DateTime | pendulum.Date | datetime | date, @@ -445,4 +446,4 @@ def __eq__(self, other: object) -> bool: other._absolute, ) else: - return self.as_interval() == other + return self.as_duration() == other diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py index 637509a1..dba73bdf 100644 --- a/pendulum/locales/locale.py +++ b/pendulum/locales/locale.py @@ -4,14 +4,10 @@ from pathlib import Path import re -import sys from typing import Any, cast from typing import Dict -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources +from pendulum.utils._compat import resources class Locale: diff --git a/pendulum/parser.py b/pendulum/parser.py index 77900e20..1b050621 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -109,7 +109,7 @@ def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | I ), ) - if CDuration and isinstance(parsed, CDuration): + if CDuration and isinstance(parsed, CDuration): # type: ignore[truthy-function] return pendulum.duration( years=parsed.years, months=parsed.months, diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py index 81c876f5..ca465926 100644 --- a/pendulum/parsing/__init__.py +++ b/pendulum/parsing/__init__.py @@ -26,8 +26,8 @@ from pendulum.parsing._iso8601 import Duration from pendulum.parsing._iso8601 import parse_iso8601 except ImportError: - from pendulum.duration import Duration # type: ignore[misc] - from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[misc] + from pendulum.duration import Duration # type: ignore[assignment] + from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[assignment] COMMON = re.compile( # Date (optional) # noqa: E800 diff --git a/pendulum/parsing/_iso8601.pyi b/pendulum/parsing/_iso8601.pyi index b9ce5d4e..761fe6b3 100644 --- a/pendulum/parsing/_iso8601.pyi +++ b/pendulum/parsing/_iso8601.pyi @@ -5,7 +5,6 @@ from datetime import datetime from datetime import time class Duration: - years: int = 0 months: int = 0 weeks: int = 0 diff --git a/pendulum/testing/traveller.py b/pendulum/testing/traveller.py index 3c1d885f..8e96c5e0 100644 --- a/pendulum/testing/traveller.py +++ b/pendulum/testing/traveller.py @@ -9,15 +9,17 @@ if TYPE_CHECKING: from types import TracebackType + from typing_extensions import Self + class BaseTraveller: def __init__(self, datetime_class: type[DateTime] = DateTime) -> None: self._datetime_class: type[DateTime] = datetime_class - def freeze(self: BaseTraveller) -> BaseTraveller: + def freeze(self) -> Self: raise NotImplementedError() - def travel_back(self: BaseTraveller) -> BaseTraveller: + def travel_back(self) -> Self: raise NotImplementedError() def travel( @@ -30,12 +32,23 @@ def travel( minutes: int = 0, seconds: int = 0, microseconds: int = 0, - ) -> BaseTraveller: + ) -> Self: raise NotImplementedError() - def travel_to(self, dt: DateTime) -> BaseTraveller: + def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self: raise NotImplementedError() + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType, + ) -> None: + ... + if not PYPY: import time_machine @@ -48,7 +61,7 @@ def __init__(self, datetime_class: type[DateTime] = DateTime) -> None: self._traveller: time_machine.travel | None = None self._coordinates: time_machine.Coordinates | None = None - def freeze(self) -> Traveller: + def freeze(self) -> Self: if self._started: cast(time_machine.Coordinates, self._coordinates).move_to( self._datetime_class.now(), tick=False @@ -58,7 +71,7 @@ def freeze(self) -> Traveller: return self - def travel_back(self) -> Traveller: + def travel_back(self) -> Self: if not self._started: return self @@ -81,7 +94,7 @@ def travel( microseconds: int = 0, *, freeze: bool = False, - ) -> Traveller: + ) -> Self: self._start(freeze=freeze) cast(time_machine.Coordinates, self._coordinates).move_to( @@ -99,7 +112,7 @@ def travel( return self - def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Traveller: + def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self: self._start(freeze=freeze) cast(time_machine.Coordinates, self._coordinates).move_to(dt) @@ -119,7 +132,7 @@ def _start(self, freeze: bool = False) -> None: self._started = True - def __enter__(self) -> Traveller: + def __enter__(self) -> Self: self._start() return self diff --git a/pendulum/time.py b/pendulum/time.py index f979e254..6c0c3048 100644 --- a/pendulum/time.py +++ b/pendulum/time.py @@ -19,7 +19,9 @@ from pendulum.mixins.default import FormattableMixin if TYPE_CHECKING: - from typing import Literal + from typing_extensions import Literal + from typing_extensions import Self + from typing_extensions import SupportsIndex class Time(FormattableMixin, time): @@ -44,7 +46,7 @@ def __repr__(self) -> str: # Comparisons - def closest(self, dt1: Time | time, dt2: Time | time) -> Time: + def closest(self, dt1: Time | time, dt2: Time | time) -> Self: """ Get the closest time from the instance. """ @@ -56,7 +58,7 @@ def closest(self, dt1: Time | time, dt2: Time | time) -> Time: return dt2 - def farthest(self, dt1: Time | time, dt2: Time | time) -> Time: + def farthest(self, dt1: Time | time, dt2: Time | time) -> Self: """ Get the farthest time from the instance. """ @@ -250,13 +252,13 @@ def diff_for_humans( def replace( self, - hour: int | None = None, - minute: int | None = None, - second: int | None = None, - microsecond: int | None = None, + hour: SupportsIndex | None = None, + minute: SupportsIndex | None = None, + second: SupportsIndex | None = None, + microsecond: SupportsIndex | None = None, tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, fold: int = 0, - ) -> Time: + ) -> Self: if tzinfo is True: tzinfo = self.tzinfo @@ -281,7 +283,7 @@ def __getnewargs__(self) -> tuple[Time]: return (self,) def _get_state( - self, protocol: int = 3 + self, protocol: SupportsIndex = 3 ) -> tuple[int, int, int, int, datetime.tzinfo | None]: tz = self.tzinfo @@ -292,8 +294,8 @@ def __reduce__( ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]: return self.__reduce_ex__(2) - def __reduce_ex__( # type: ignore[override] - self, protocol: int + def __reduce_ex__( + self, protocol: SupportsIndex ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]: return self.__class__, self._get_state(protocol) diff --git a/pendulum/tz/__init__.py b/pendulum/tz/__init__.py index 45c9855d..0ca1b328 100644 --- a/pendulum/tz/__init__.py +++ b/pendulum/tz/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations -import sys +from pathlib import Path +from typing import cast from pendulum.tz.local_timezone import get_local_timezone from pendulum.tz.local_timezone import set_local_timezone @@ -8,11 +9,7 @@ from pendulum.tz.timezone import UTC from pendulum.tz.timezone import FixedTimezone from pendulum.tz.timezone import Timezone - -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources +from pendulum.utils._compat import resources PRE_TRANSITION = "pre" POST_TRANSITION = "post" @@ -27,25 +24,12 @@ def timezones() -> tuple[str, ...]: global _timezones if _timezones is None: - with resources.files("tzdata").joinpath("zones").open() as f: + with cast(Path, resources.files("tzdata").joinpath("zones")).open() as f: _timezones = tuple(tz.strip() for tz in f.readlines()) return _timezones -def timezone(name: str | int) -> Timezone | FixedTimezone: - """ - Return a Timezone instance given its name. - """ - if isinstance(name, int): - return fixed_timezone(name) - - if name.lower() == "utc": - return UTC - - return Timezone(name) - - def fixed_timezone(offset: int) -> FixedTimezone: """ Return a Timezone instance given its offset in seconds. @@ -73,7 +57,6 @@ def local_timezone() -> Timezone | FixedTimezone: "set_local_timezone", "get_local_timezone", "test_local_timezone", - "timezone", "fixed_timezone", "local_timezone", "timezones", diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py index 28b07ba7..0965768e 100644 --- a/pendulum/tz/local_timezone.py +++ b/pendulum/tz/local_timezone.py @@ -14,10 +14,7 @@ from pendulum.tz.timezone import Timezone if sys.platform == "win32": - try: - import _winreg as winreg - except (ImportError, AttributeError): - import winreg + import winreg _mock_local_timezone = None _local_timezone = None @@ -142,7 +139,7 @@ def _get_windows_timezone() -> Timezone: else: def _get_windows_timezone() -> Timezone: - ... + raise NotImplementedError def _get_darwin_timezone() -> Timezone: diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py index e7e84af9..9befc004 100644 --- a/pendulum/tz/timezone.py +++ b/pendulum/tz/timezone.py @@ -1,9 +1,12 @@ +# mypy: no-warn-redundant-casts from __future__ import annotations -import datetime as datetime_ +import datetime as _datetime from abc import ABC from abc import abstractmethod +from typing import TYPE_CHECKING +from typing import TypeVar from typing import cast from pendulum.tz.exceptions import AmbiguousTime @@ -11,11 +14,17 @@ from pendulum.tz.exceptions import NonExistingTime from pendulum.utils._compat import zoneinfo +if TYPE_CHECKING: + from typing_extensions import Self + POST_TRANSITION = "post" PRE_TRANSITION = "pre" TRANSITION_ERROR = "error" +_DT = TypeVar("_DT", bound=_datetime.datetime) + + class PendulumTimezone(ABC): @property @abstractmethod @@ -23,9 +32,7 @@ def name(self) -> str: raise NotImplementedError @abstractmethod - def convert( - self, dt: datetime_.datetime, raise_on_unknown_times: bool = False - ) -> datetime_.datetime: + def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT: raise NotImplementedError @abstractmethod @@ -38,7 +45,7 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - ) -> datetime_.datetime: + ) -> _datetime.datetime: raise NotImplementedError @@ -52,7 +59,7 @@ class Timezone(zoneinfo.ZoneInfo, PendulumTimezone): >>> tz = Timezone('Europe/Paris') """ - def __new__(cls, key: str) -> Timezone: + def __new__(cls, key: str) -> Self: try: return super().__new__(cls, key) # type: ignore[call-arg] except zoneinfo.ZoneInfoNotFoundError: @@ -62,9 +69,7 @@ def __new__(cls, key: str) -> Timezone: def name(self) -> str: return self.key - def convert( - self, dt: datetime_.datetime, raise_on_unknown_times: bool = False - ) -> datetime_.datetime: + def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT: """ Converts a datetime in the current timezone. @@ -85,6 +90,7 @@ def convert( >>> in_new_york.isoformat() '2013-03-30T21:30:00-04:00' """ + if dt.tzinfo is None: # Technically, utcoffset() can return None, but none of the zone information # in tzdata sets _tti_before to None. This can be checked with the following @@ -98,11 +104,11 @@ def convert( # >>> print(tzname) offset_before = cast( - datetime_.timedelta, + _datetime.timedelta, (self.utcoffset(dt.replace(fold=0)) if dt.fold else self.utcoffset(dt)), ) offset_after = cast( - datetime_.timedelta, + _datetime.timedelta, (self.utcoffset(dt) if dt.fold else self.utcoffset(dt.replace(fold=1))), ) @@ -111,10 +117,14 @@ def convert( if raise_on_unknown_times: raise NonExistingTime(dt) - dt += ( - (offset_after - offset_before) - if dt.fold - else (offset_before - offset_after) + dt = cast( + _DT, + dt + + ( + (offset_after - offset_before) + if dt.fold + else (offset_before - offset_after) + ), ) elif offset_before > offset_after and raise_on_unknown_times: # Repeated time @@ -122,7 +132,7 @@ def convert( return dt.replace(tzinfo=self) - return dt.astimezone(self) + return cast(_DT, dt.astimezone(self)) def datetime( self, @@ -133,12 +143,12 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - ) -> datetime_.datetime: + ) -> _datetime.datetime: """ Return a normalized datetime for the current timezone. """ return self.convert( - datetime_.datetime( + _datetime.datetime( year, month, day, hour, minute, second, microsecond, fold=1 ) ) @@ -147,7 +157,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}('{self.name}')" -class FixedTimezone(datetime_.tzinfo, PendulumTimezone): +class FixedTimezone(_datetime.tzinfo, PendulumTimezone): def __init__(self, offset: int, name: str | None = None) -> None: sign = "-" if offset < 0 else "+" @@ -159,15 +169,13 @@ def __init__(self, offset: int, name: str | None = None) -> None: self._name = name self._offset = offset - self._utcoffset = datetime_.timedelta(seconds=offset) + self._utcoffset = _datetime.timedelta(seconds=offset) @property def name(self) -> str: return self._name - def convert( - self, dt: datetime_.datetime, raise_on_unknown_times: bool = False - ) -> datetime_.datetime: + def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT: if dt.tzinfo is None: return dt.__class__( dt.year, @@ -181,7 +189,7 @@ def convert( fold=0, ) - return dt.astimezone(self) + return cast(_DT, dt.astimezone(self)) def datetime( self, @@ -192,9 +200,9 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - ) -> datetime_.datetime: + ) -> _datetime.datetime: return self.convert( - datetime_.datetime( + _datetime.datetime( year, month, day, hour, minute, second, microsecond, fold=1 ) ) @@ -203,17 +211,17 @@ def datetime( def offset(self) -> int: return self._offset - def utcoffset(self, dt: datetime_.datetime | None) -> datetime_.timedelta: + def utcoffset(self, dt: _datetime.datetime | None) -> _datetime.timedelta: return self._utcoffset - def dst(self, dt: datetime_.datetime | None) -> datetime_.timedelta: - return datetime_.timedelta() + def dst(self, dt: _datetime.datetime | None) -> _datetime.timedelta: + return _datetime.timedelta() - def fromutc(self, dt: datetime_.datetime) -> datetime_.datetime: + def fromutc(self, dt: _datetime.datetime) -> _datetime.datetime: # Use the stdlib datetime's add method to avoid infinite recursion - return (datetime_.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) + return (_datetime.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) - def tzname(self, dt: datetime_.datetime | None) -> str | None: + def tzname(self, dt: _datetime.datetime | None) -> str | None: return self._name def __getinitargs__(self) -> tuple[int, str]: diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py index 81b0a308..5ac51bdd 100644 --- a/pendulum/utils/_compat.py +++ b/pendulum/utils/_compat.py @@ -2,12 +2,13 @@ import sys +from pendulum.utils import _zoneinfo as zoneinfo + PYPY = hasattr(sys, "pypy_version_info") -PY38 = sys.version_info[:2] >= (3, 8) if sys.version_info < (3, 9): - from backports import zoneinfo + import importlib_resources as resources else: - import zoneinfo + from importlib import resources -__all__ = ["zoneinfo"] +__all__ = ["resources", "zoneinfo"] diff --git a/pendulum/utils/_zoneinfo.py b/pendulum/utils/_zoneinfo.py new file mode 100644 index 00000000..8f87b795 --- /dev/null +++ b/pendulum/utils/_zoneinfo.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import sys + +from typing import TYPE_CHECKING + +if sys.version_info < (3, 9): + # Works around https://github.com/pganssle/zoneinfo/issues/125 + from backports.zoneinfo import TZPATH + from backports.zoneinfo import InvalidTZPathWarning + from backports.zoneinfo import ZoneInfoNotFoundError + from backports.zoneinfo import available_timezones + from backports.zoneinfo import reset_tzpath + + if TYPE_CHECKING: + from collections.abc import Iterable + from datetime import datetime + from datetime import timedelta + from datetime import tzinfo + from typing import Any + from typing import Protocol + + from typing_extensions import Self + + class _IOBytes(Protocol): + def read(self, __size: int) -> bytes: + ... + + def seek(self, __size: int, __whence: int = ...) -> Any: + ... + + class ZoneInfo(tzinfo): + @property + def key(self) -> str: + ... + + def __init__(self, key: str) -> None: + ... + + @classmethod + def no_cache(cls, key: str) -> Self: + ... + + @classmethod + def from_file(cls, __fobj: _IOBytes, key: str | None = ...) -> Self: + ... + + @classmethod + def clear_cache(cls, *, only_keys: Iterable[str] | None = ...) -> None: + ... + + def tzname(self, __dt: datetime | None) -> str | None: + ... + + def utcoffset(self, __dt: datetime | None) -> timedelta | None: + ... + + def dst(self, __dt: datetime | None) -> timedelta | None: + ... + + else: + from backports.zoneinfo import ZoneInfo + +else: + from zoneinfo import TZPATH + from zoneinfo import InvalidTZPathWarning + from zoneinfo import ZoneInfo + from zoneinfo import ZoneInfoNotFoundError + from zoneinfo import available_timezones + from zoneinfo import reset_tzpath + +__all__ = [ + "ZoneInfo", + "reset_tzpath", + "available_timezones", + "TZPATH", + "ZoneInfoNotFoundError", + "InvalidTZPathWarning", +] diff --git a/poetry.lock b/poetry.lock index 22d75ed1..96da94cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -15,7 +14,6 @@ files = [ name = "attrs" version = "22.1.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -33,7 +31,6 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy name = "babel" version = "2.10.3" description = "Internationalization utilities" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -48,7 +45,6 @@ pytz = ">=2015.7" name = "backports-zoneinfo" version = "0.2.1" description = "Backport of the standard library zoneinfo module" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -77,7 +73,6 @@ tzdata = ["tzdata"] name = "black" version = "22.6.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -125,7 +120,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "dev" optional = false python-versions = "*" files = [ @@ -202,7 +196,6 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -214,7 +207,6 @@ files = [ name = "cleo" version = "2.0.1" description = "Cleo allows you to create beautiful and testable command-line interfaces." -category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -230,7 +222,6 @@ rapidfuzz = ">=2.2.0,<3.0.0" name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -246,7 +237,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -258,7 +248,6 @@ files = [ name = "crashtest" version = "0.4.1" description = "Manage Python errors with ease" -category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -270,7 +259,6 @@ files = [ name = "distlib" version = "0.3.5" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -282,7 +270,6 @@ files = [ name = "filelock" version = "3.7.1" description = "A platform independent file lock." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -298,7 +285,6 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." -category = "dev" optional = false python-versions = "*" files = [ @@ -316,7 +302,6 @@ dev = ["flake8", "markdown", "twine", "wheel"] name = "identify" version = "2.5.3" description = "File identification library for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -331,7 +316,6 @@ license = ["ukkonen"] name = "importlib-metadata" version = "4.12.0" description = "Read metadata from Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -352,7 +336,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "importlib-resources" version = "5.9.0" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -371,7 +354,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" files = [ @@ -383,7 +365,6 @@ files = [ name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.6.1,<4.0" files = [ @@ -401,7 +382,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -419,7 +399,6 @@ i18n = ["Babel (>=2.7)"] name = "markdown" version = "3.4.1" description = "Python implementation of Markdown." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -437,7 +416,6 @@ testing = ["coverage", "pyyaml"] name = "markdown-include" version = "0.5.1" description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator." -category = "dev" optional = false python-versions = "*" files = [ @@ -451,7 +429,6 @@ markdown = "*" name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -501,7 +478,6 @@ files = [ name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -513,7 +489,6 @@ files = [ name = "meson" version = "0.63.2" description = "A high performance build system" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -530,7 +505,6 @@ typing = ["mypy", "typing-extensions"] name = "mkdocs" version = "1.3.0" description = "Project documentation with Markdown." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -553,23 +527,68 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] +[[package]] +name = "mypy" +version = "1.3.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"}, + {file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"}, + {file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"}, + {file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"}, + {file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"}, + {file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"}, + {file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"}, + {file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"}, + {file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"}, + {file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"}, + {file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"}, + {file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"}, + {file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"}, + {file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"}, + {file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"}, + {file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"}, + {file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"}, + {file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"}, + {file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"}, + {file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"}, + {file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"}, + {file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"}, + {file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"}, + {file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"}, + {file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"}, + {file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "ninja" version = "1.10.2.3" description = "Ninja is a small build system with a focus on speed" -category = "dev" optional = false python-versions = "*" files = [ @@ -596,7 +615,6 @@ test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "pytest (>= name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -611,7 +629,6 @@ setuptools = "*" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -626,7 +643,6 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "pathspec" version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -638,7 +654,6 @@ files = [ name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -654,7 +669,6 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -673,7 +687,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -694,7 +707,6 @@ virtualenv = ">=20.0.8" name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -706,7 +718,6 @@ files = [ name = "py-cpuinfo" version = "9.0.0" description = "Get CPU info with pure Python" -category = "dev" optional = false python-versions = "*" files = [ @@ -718,7 +729,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -730,7 +740,6 @@ files = [ name = "pygments" version = "2.12.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -742,7 +751,6 @@ files = [ name = "pymdown-extensions" version = "6.3" description = "Extension pack for Python Markdown." -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -757,7 +765,6 @@ Markdown = ">=3.2" name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" optional = false python-versions = ">=3.6.8" files = [ @@ -772,7 +779,6 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.1.2" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -798,7 +804,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-benchmark" version = "4.0.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -819,7 +824,6 @@ histogram = ["pygal", "pygaljs"] name = "pytest-codspeed" version = "1.2.2" description = "Pytest plugin to create CodSpeed benchmarks" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -839,7 +843,6 @@ dev = ["black (>=22.3.0,<22.4.0)", "flake8 (>=5.0.4,<5.1.0)", "hatchling (>=1.11 name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -854,7 +857,6 @@ six = ">=1.5" name = "pytz" version = "2022.1" description = "World timezone definitions, modern and historical" -category = "dev" optional = false python-versions = "*" files = [ @@ -866,7 +868,6 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -916,7 +917,6 @@ files = [ name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -931,7 +931,6 @@ pyyaml = "*" name = "rapidfuzz" version = "2.13.7" description = "rapid fuzzy string matching" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1033,7 +1032,6 @@ full = ["numpy"] name = "setuptools" version = "63.4.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1050,7 +1048,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1062,7 +1059,6 @@ files = [ name = "time-machine" version = "2.7.1" description = "Travel through time in your tests." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1115,7 +1111,6 @@ python-dateutil = "*" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1127,7 +1122,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1139,7 +1133,6 @@ files = [ name = "tox" version = "3.25.1" description = "tox is a generic virtualenv management and test command line tool" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -1166,7 +1159,6 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psu name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1197,34 +1189,31 @@ files = [ ] [[package]] -name = "types-backports" -version = "0.1.3" -description = "Typing stubs for backports" -category = "dev" +name = "types-python-dateutil" +version = "2.8.19" +description = "Typing stubs for python-dateutil" optional = false python-versions = "*" files = [ - {file = "types-backports-0.1.3.tar.gz", hash = "sha256:f4b7206c073df88d6200891e3d27506185fd60cda66fb289737b2fa92c0010cf"}, - {file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"}, + {file = "types-python-dateutil-2.8.19.tar.gz", hash = "sha256:bfd3eb39c7253aea4ba23b10f69b017d30b013662bb4be4ab48b20bbd763f309"}, + {file = "types_python_dateutil-2.8.19-py3-none-any.whl", hash = "sha256:6284df1e4783d8fc6e587f0317a81333856b872a6669a282f8a325342bce7fa8"}, ] [[package]] -name = "types-python-dateutil" -version = "2.8.19" -description = "Typing stubs for python-dateutil" -category = "dev" +name = "types-pytz" +version = "2022.7.1.2" +description = "Typing stubs for pytz" optional = false python-versions = "*" files = [ - {file = "types-python-dateutil-2.8.19.tar.gz", hash = "sha256:bfd3eb39c7253aea4ba23b10f69b017d30b013662bb4be4ab48b20bbd763f309"}, - {file = "types_python_dateutil-2.8.19-py3-none-any.whl", hash = "sha256:6284df1e4783d8fc6e587f0317a81333856b872a6669a282f8a325342bce7fa8"}, + {file = "types-pytz-2022.7.1.2.tar.gz", hash = "sha256:487d3e8e9f4071eec8081746d53fa982bbc05812e719dcbf2ebf3d55a1a4cd28"}, + {file = "types_pytz-2022.7.1.2-py3-none-any.whl", hash = "sha256:40ca448a928d566f7d44ddfde0066e384f7ffbd4da2778e42a4570eaca572446"}, ] [[package]] name = "typing-extensions" version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1236,7 +1225,6 @@ files = [ name = "tzdata" version = "2022.1" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ @@ -1248,7 +1236,6 @@ files = [ name = "virtualenv" version = "20.16.3" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1270,7 +1257,6 @@ testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7 name = "watchdog" version = "2.1.9" description = "Filesystem events monitoring" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1308,7 +1294,6 @@ watchmedo = ["PyYAML (>=3.10)"] name = "zipp" version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1323,4 +1308,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "c8ac8cbd6ca3a6f7be217c160e93b23f7bf89056ca9cd1b66febc70c4c809e10" +content-hash = "39af764fa2788e55fa847cb48f19c948c043b37bb52546ab5535d6953387200f" diff --git a/pyproject.toml b/pyproject.toml index 63cfac55..3aa14314 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,8 +49,11 @@ markdown-include = "^0.5.1" black = { version = "^22.6.0", markers = "implementation_name != 'pypy'" } isort = "^5.10.1" pre-commit = "^2.20.0" -types-backports = "^0.1.3" + +[tool.poetry.group.typing.dependencies] +mypy = "^1.3.0" types-python-dateutil = "^2.8.19" +types-pytz = ">=2022.7.1.2" [tool.poetry.group.dev.dependencies] babel = "^2.10.3" @@ -97,6 +100,9 @@ strict = true files = "pendulum, tests" show_error_codes = true pretty = true +exclude = [ + "^build\\.py$" +] # The following whitelist is used to allow for incremental adoption # of Mypy. Modules should be removed from this whitelist as and when @@ -106,9 +112,6 @@ pretty = true [[tool.mypy.overrides]] module = [ "pendulum.mixins.default", - "tests.conftest", - "tests.test_helpers", - "tests.test_main", "tests.test_parsing", "tests.date.test_add", "tests.date.test_behavior", diff --git a/tests/benchmarks/test_format.py b/tests/benchmarks/test_format.py index 6b3964e1..41e67828 100644 --- a/tests/benchmarks/test_format.py +++ b/tests/benchmarks/test_format.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from pytest_benchmark.fixture import BenchmarkFixture + from pytest_benchmark.fixture import BenchmarkFixture # type: ignore[import] import pendulum diff --git a/tests/conftest.py b/tests/conftest.py index 060e9518..73b66db9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest import pendulum +if TYPE_CHECKING: + from collections.abc import Iterator + @pytest.fixture(autouse=True) -def setup(): +def setup() -> Iterator[None]: pendulum.set_local_timezone(pendulum.timezone("America/Toronto")) yield @@ -18,8 +23,15 @@ def setup(): def assert_datetime( - d, year, month, day, hour=None, minute=None, second=None, microsecond=None -): + d: pendulum.DateTime, + year: int, + month: int, + day: int, + hour: int | None = None, + minute: int | None = None, + second: int | None = None, + microsecond: int | None = None, +) -> None: assert year == d.year assert month == d.month assert day == d.day @@ -37,13 +49,19 @@ def assert_datetime( assert microsecond == d.microsecond -def assert_date(d, year, month, day): +def assert_date(d: pendulum.Date, year: int, month: int, day: int) -> None: assert year == d.year assert month == d.month assert day == d.day -def assert_time(t, hour, minute, second, microsecond=None): +def assert_time( + t: pendulum.Time, + hour: int, + minute: int, + second: int, + microsecond: int | None = None, +) -> None: assert hour == t.hour assert minute == t.minute assert second == t.second @@ -53,15 +71,15 @@ def assert_time(t, hour, minute, second, microsecond=None): def assert_duration( - dur, - years=None, - months=None, - weeks=None, - days=None, - hours=None, - minutes=None, - seconds=None, - microseconds=None, + dur: pendulum.Duration, + years: int | None = None, + months: int | None = None, + weeks: int | None = None, + days: int | None = None, + hours: int | None = None, + minutes: int | None = None, + seconds: int | None = None, + microseconds: int | None = None, ) -> None: expected = {} actual = {} diff --git a/tests/datetime/test_construct.py b/tests/datetime/test_construct.py index 9488c08b..9435413b 100644 --- a/tests/datetime/test_construct.py +++ b/tests/datetime/test_construct.py @@ -12,7 +12,7 @@ import pendulum from pendulum import DateTime -from pendulum.tz import timezone +from pendulum import timezone from pendulum.utils._compat import PYPY from tests.conftest import assert_datetime diff --git a/tests/datetime/test_getters.py b/tests/datetime/test_getters.py index 50746234..9adfc189 100644 --- a/tests/datetime/test_getters.py +++ b/tests/datetime/test_getters.py @@ -7,7 +7,7 @@ import pendulum from pendulum import DateTime -from pendulum.tz import timezone +from pendulum import timezone from tests.conftest import assert_date from tests.conftest import assert_time diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e3daeac5..7756a063 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -8,14 +8,22 @@ import pendulum from pendulum import timezone +from pendulum.helpers import PreciseDiff from pendulum.helpers import days_in_year from pendulum.helpers import precise_diff from pendulum.helpers import week_day def assert_diff( - diff, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0 -): + diff: PreciseDiff, + years: int = 0, + months: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: int = 0, + microseconds: int = 0, +) -> None: assert diff.years == years assert diff.months == months assert diff.days == days @@ -25,7 +33,7 @@ def assert_diff( assert diff.microseconds == microseconds -def test_precise_diff(): +def test_precise_diff() -> None: dt1 = datetime(2003, 3, 1, 0, 0, 0) dt2 = datetime(2003, 1, 31, 23, 59, 59) @@ -76,7 +84,7 @@ def test_precise_diff(): assert_diff(diff, days=6, hours=0) -def test_precise_diff_timezone(): +def test_precise_diff_timezone() -> None: paris = pendulum.timezone("Europe/Paris") toronto = pendulum.timezone("America/Toronto") @@ -92,16 +100,16 @@ def test_precise_diff_timezone(): assert_diff(diff, days=1, hours=5) # pytz - paris = pytz.timezone("Europe/Paris") - toronto = pytz.timezone("America/Toronto") + paris_pytz = pytz.timezone("Europe/Paris") + toronto_pytz = pytz.timezone("America/Toronto") - dt1 = paris.localize(datetime(2013, 3, 31, 1, 30)) - dt2 = paris.localize(datetime(2013, 4, 1, 1, 30)) + dt1 = paris_pytz.localize(datetime(2013, 3, 31, 1, 30)) + dt2 = paris_pytz.localize(datetime(2013, 4, 1, 1, 30)) diff = precise_diff(dt1, dt2) assert_diff(diff, days=1, hours=0) - dt2 = toronto.localize(datetime(2013, 4, 1, 1, 30)) + dt2 = toronto_pytz.localize(datetime(2013, 4, 1, 1, 30)) diff = precise_diff(dt1, dt2) assert_diff(diff, days=1, hours=5) @@ -113,17 +121,17 @@ def test_precise_diff_timezone(): assert_diff(diff, minutes=10) -def test_week_day(): +def test_week_day() -> None: assert week_day(2017, 6, 2) == 5 assert week_day(2017, 1, 1) == 7 -def test_days_in_years(): +def test_days_in_years() -> None: assert days_in_year(2017) == 365 assert days_in_year(2016) == 366 -def test_locale(): +def test_locale() -> None: dt = pendulum.datetime(2000, 11, 10, 12, 34, 56, 123456) pendulum.set_locale("fr") @@ -133,7 +141,7 @@ def test_locale(): assert dt.date().format("MMMM") == "novembre" -def test_set_locale_invalid(): +def test_set_locale_invalid() -> None: with pytest.raises(ValueError): pendulum.set_locale("invalid") @@ -141,13 +149,13 @@ def test_set_locale_invalid(): @pytest.mark.parametrize( "locale", ["DE", "pt-BR", "pt-br", "PT-br", "PT-BR", "pt_br", "PT_BR", "PT_BR"] ) -def test_set_locale_malformed_locale(locale): +def test_set_locale_malformed_locale(locale: str) -> None: pendulum.set_locale(locale) pendulum.set_locale("en") -def test_week_starts_at(): +def test_week_starts_at() -> None: pendulum.week_starts_at(pendulum.SATURDAY) dt = pendulum.now().start_of("week") @@ -155,7 +163,7 @@ def test_week_starts_at(): assert dt.date().day_of_week == pendulum.SATURDAY -def test_week_starts_at_invalid_value(): +def test_week_starts_at_invalid_value() -> None: with pytest.raises(ValueError): pendulum.week_starts_at(-1) @@ -163,7 +171,7 @@ def test_week_starts_at_invalid_value(): pendulum.week_starts_at(11) -def test_week_ends_at(): +def test_week_ends_at() -> None: pendulum.week_ends_at(pendulum.SATURDAY) dt = pendulum.now().end_of("week") @@ -171,7 +179,7 @@ def test_week_ends_at(): assert dt.date().day_of_week == pendulum.SATURDAY -def test_week_ends_at_invalid_value(): +def test_week_ends_at_invalid_value() -> None: with pytest.raises(ValueError): pendulum.week_ends_at(-1) diff --git a/tests/test_main.py b/tests/test_main.py index 1710bf29..7d6c46b0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,7 @@ from pendulum.tz.timezone import Timezone -def test_safe_timezone_with_tzinfo_objects(): +def test_safe_timezone_with_tzinfo_objects() -> None: tz = _safe_timezone(pytz.timezone("Europe/Paris")) assert isinstance(tz, Timezone) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 0e5308c8..37812ee2 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -8,13 +8,14 @@ from tests.conftest import assert_time -def test_parse(): +def test_parse() -> None: text = "2016-10-16T12:34:56.123456+01:30" dt = pendulum.parse(text) assert isinstance(dt, pendulum.DateTime) assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456) + assert dt.tz is not None assert dt.tz.name == "+01:30" assert dt.offset == 5400 @@ -36,16 +37,18 @@ def test_parse(): assert dt.offset == 0 -def test_parse_with_timezone(): +def test_parse_with_timezone() -> None: text = "2016-10-16T12:34:56.123456" dt = pendulum.parse(text, tz="Europe/Paris") + assert isinstance(dt, pendulum.DateTime) assert_datetime(dt, 2016, 10, 16, 12, 34, 56, 123456) + assert dt.tz is not None assert dt.tz.name == "Europe/Paris" assert dt.offset == 7200 -def test_parse_exact(): +def test_parse_exact() -> None: text = "2016-10-16T12:34:56.123456+01:30" dt = pendulum.parse(text, exact=True) @@ -76,7 +79,7 @@ def test_parse_exact(): assert_time(dt, 13, 0, 0) -def test_parse_duration(): +def test_parse_duration() -> None: text = "P2Y3M4DT5H6M7S" duration = pendulum.parse(text) @@ -92,7 +95,7 @@ def test_parse_duration(): assert_duration(duration, 0, 0, 2, 0, 0, 0, 0) -def test_parse_interval(): +def test_parse_interval() -> None: text = "2008-05-11T15:30:00Z/P1Y2M10DT2H30M" period = pendulum.parse(text) @@ -124,7 +127,7 @@ def test_parse_interval(): assert period.end.offset == 0 -def test_parse_now(): +def test_parse_now() -> None: dt = pendulum.parse("now") assert dt.timezone_name == "America/Toronto" @@ -135,7 +138,7 @@ def test_parse_now(): assert pendulum.parse("now") == mock_now -def test_parse_with_utc_timezone(): +def test_parse_with_utc_timezone() -> None: dt = pendulum.parse("2020-02-05T20:05:37.364951Z") assert dt.to_iso8601_string() == "2020-02-05T20:05:37.364951Z" diff --git a/tests/tz/test_helpers.py b/tests/tz/test_helpers.py index edec6fda..9402372c 100644 --- a/tests/tz/test_helpers.py +++ b/tests/tz/test_helpers.py @@ -2,7 +2,7 @@ import pytest -from pendulum.tz import timezone +from pendulum import timezone from pendulum.tz.exceptions import InvalidTimezone from pendulum.tz.timezone import FixedTimezone from pendulum.tz.timezone import Timezone