From 8732968d380d02cda7bc3c96ab6cee8ee1b68893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 28 Jun 2022 23:25:52 +0200 Subject: [PATCH] Improve typing --- .pre-commit-config.yaml | 6 +- pendulum/__init__.py | 118 +++++-- pendulum/_extensions/_helpers.pyi | 31 ++ pendulum/_extensions/helpers.py | 74 ++-- pendulum/constants.py | 1 - pendulum/date.py | 353 +++++++------------- pendulum/datetime.py | 257 ++++++++------ pendulum/duration.py | 179 +++++----- pendulum/formatting/__init__.py | 1 - pendulum/formatting/difference_formatter.py | 28 +- pendulum/formatting/formatter.py | 114 +++---- pendulum/helpers.py | 82 +++-- pendulum/locales/locale.py | 34 +- pendulum/mixins/default.py | 21 +- pendulum/parser.py | 26 +- pendulum/parsing/__init__.py | 46 +-- pendulum/parsing/_iso8601.pyi | 22 ++ pendulum/parsing/iso8601.py | 295 ++++++++-------- pendulum/period.py | 235 ++++++++----- pendulum/time.py | 134 ++++---- pendulum/tz/__init__.py | 29 +- pendulum/tz/data/windows.py | 1 - pendulum/tz/exceptions.py | 13 +- pendulum/tz/local_timezone.py | 164 ++++----- pendulum/tz/timezone.py | 61 ++-- pendulum/utils/_compat.py | 5 +- poetry.lock | 141 ++++++-- pyproject.toml | 41 +-- tests/date/test_diff.py | 12 +- tests/datetime/test_behavior.py | 5 +- tests/localization/test_cs.py | 1 - tests/localization/test_da.py | 1 - tests/localization/test_de.py | 1 - tests/localization/test_es.py | 1 - tests/localization/test_fa.py | 1 - tests/localization/test_fo.py | 1 - tests/localization/test_fr.py | 1 - tests/localization/test_he.py | 1 - tests/localization/test_id.py | 1 - tests/localization/test_it.py | 1 - tests/localization/test_ja.py | 1 - tests/localization/test_ko.py | 1 - tests/localization/test_lt.py | 1 - tests/localization/test_nb.py | 1 - tests/localization/test_nl.py | 1 - tests/localization/test_nn.py | 1 - tests/localization/test_pl.py | 1 - tests/localization/test_ru.py | 1 - tests/localization/test_sk.py | 1 - tests/localization/test_sv.py | 1 - tests/parsing/test_parse_iso8601.py | 1 - tests/tz/test_timezone.py | 4 +- 52 files changed, 1413 insertions(+), 1141 deletions(-) create mode 100644 pendulum/_extensions/_helpers.pyi create mode 100644 pendulum/parsing/_iso8601.pyi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dfb621b7..eb809830 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v2.1.1 hooks: - id: pycln - args: [--all] + args: [ --all ] - repo: https://github.com/psf/black rev: 22.6.0 @@ -26,7 +26,7 @@ repos: rev: 5.10.1 hooks: - id: isort - args: [--add-import, from __future__ import annotations] + args: [ --add-import, from __future__ import annotations, --lines-after-imports, "-1" ] - repo: https://github.com/pycqa/flake8 rev: 5.0.4 @@ -67,3 +67,5 @@ repos: 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 bbe284b9..33ff1f0a 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -2,8 +2,8 @@ import datetime as _datetime -from typing import Optional from typing import Union +from typing import cast from pendulum.__version__ import __version__ from pendulum.constants import DAYS_PER_WEEK @@ -40,9 +40,6 @@ from pendulum.parser import parse from pendulum.period import Period from pendulum.time import Time -from pendulum.tz import POST_TRANSITION -from pendulum.tz import PRE_TRANSITION -from pendulum.tz import TRANSITION_ERROR from pendulum.tz import UTC from pendulum.tz import local_timezone from pendulum.tz import set_local_timezone @@ -52,7 +49,6 @@ from pendulum.tz.timezone import FixedTimezone from pendulum.tz.timezone import Timezone - _TEST_NOW: DateTime | None = None _LOCALE = "en" _WEEK_STARTS_AT = MONDAY @@ -61,7 +57,10 @@ _formatter = Formatter() -def _safe_timezone(obj: str | float | _datetime.tzinfo | Timezone | None) -> Timezone: +def _safe_timezone( + obj: str | float | _datetime.tzinfo | Timezone | FixedTimezone | None, + dt: _datetime.datetime | None = None, +) -> Timezone | FixedTimezone: """ Creates a timezone instance from a string, Timezone, TimezoneInfo or integer offset. @@ -75,19 +74,24 @@ def _safe_timezone(obj: str | float | _datetime.tzinfo | Timezone | None) -> Tim if isinstance(obj, (int, float)): obj = int(obj * 60 * 60) elif isinstance(obj, _datetime.tzinfo): + # zoneinfo + if hasattr(obj, "key"): + obj = obj.key # type: ignore # pytz - if hasattr(obj, "localize"): - obj = obj.zone + elif hasattr(obj, "localize"): + obj = obj.zone # type: ignore elif obj.tzname(None) == "UTC": return UTC else: - offset = obj.utcoffset(None) + offset = obj.utcoffset(dt) if offset is None: offset = _datetime.timedelta(0) obj = int(offset.total_seconds()) + obj = cast(Union[str, int], obj) + return timezone(obj) @@ -100,8 +104,8 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - tz: str | float | Timezone | None = UTC, - fold: int | None = 1, + tz: str | float | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + fold: int = 1, raise_on_unknown_times: bool = False, ) -> DateTime: """ @@ -146,7 +150,7 @@ def naive( minute: int = 0, second: int = 0, microsecond: int = 0, - fold: int | None = 1, + fold: int = 1, ) -> DateTime: """ Return a naive DateTime. @@ -168,7 +172,10 @@ def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> T return Time(hour, minute, second, microsecond) -def instance(dt: _datetime.datetime, tz: str | Timezone | None = UTC) -> DateTime: +def instance( + dt: _datetime.datetime, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> DateTime: """ Create a DateTime instance from a datetime one. """ @@ -180,19 +187,18 @@ def instance(dt: _datetime.datetime, tz: str | Timezone | None = UTC) -> DateTim tz = dt.tzinfo or tz - # Checking for pytz/tzinfo - if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, Timezone): - # pytz - if hasattr(tz, "localize") and tz.zone: - tz = tz.zone - else: - # We have no sure way to figure out - # the timezone name, we fallback - # on a fixed offset - tz = tz.utcoffset(dt).total_seconds() / 3600 + if tz is not None: + tz = _safe_timezone(tz, dt=dt) return datetime( - dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=cast(Union[str, int, Timezone, FixedTimezone, None], tz), ) @@ -228,12 +234,13 @@ def from_format( string: str, fmt: str, tz: str | Timezone = UTC, - locale: str | None = None, # noqa + locale: str | None = None, ) -> DateTime: """ Creates a DateTime instance from a specific format. """ parts = _formatter.parse(string, fmt, now(), locale=locale) + if parts["tz"] is None: parts["tz"] = tz @@ -288,3 +295,64 @@ def period(start: DateTime, end: DateTime, absolute: bool = False) -> Period: Create a Period instance. """ return Period(start, end, absolute=absolute) + + +__all__ = [ + "__version__", + "DAYS_PER_WEEK", + "FRIDAY", + "HOURS_PER_DAY", + "MINUTES_PER_HOUR", + "MONDAY", + "MONTHS_PER_YEAR", + "SATURDAY", + "SECONDS_PER_DAY", + "SECONDS_PER_HOUR", + "SECONDS_PER_MINUTE", + "SUNDAY", + "THURSDAY", + "TUESDAY", + "WEDNESDAY", + "WEEKS_PER_YEAR", + "YEARS_PER_CENTURY", + "YEARS_PER_DECADE", + "Date", + "DateTime", + "Duration", + "Formatter", + "date", + "datetime", + "duration", + "format_diff", + "from_format", + "from_timestamp", + "get_locale", + "get_test_now", + "has_test_now", + "instance", + "local", + "locale", + "naive", + "now", + "period", + "set_locale", + "set_test_now", + "test", + "week_ends_at", + "week_starts_at", + "parse", + "Period", + "Time", + "UTC", + "local_timezone", + "set_local_timezone", + "test_local_timezone", + "time", + "timezone", + "timezones", + "today", + "tomorrow", + "FixedTimezone", + "Timezone", + "yesterday", +] diff --git a/pendulum/_extensions/_helpers.pyi b/pendulum/_extensions/_helpers.pyi new file mode 100644 index 00000000..99a53978 --- /dev/null +++ b/pendulum/_extensions/_helpers.pyi @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections import namedtuple +from datetime import date +from datetime import datetime + +def days_in_year(year: int) -> int: ... +def is_leap(year: int) -> bool: ... +def is_long_year(year: int) -> bool: ... +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", + ) +): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + +def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... +def timestamp(dt: datetime) -> int: ... +def week_day(year: int, month: int, day: int) -> int: ... diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py index 992ea06b..01066a30 100644 --- a/pendulum/_extensions/helpers.py +++ b/pendulum/_extensions/helpers.py @@ -4,6 +4,7 @@ import math from collections import namedtuple +from typing import cast from pendulum.constants import DAY_OF_WEEK_TABLE from pendulum.constants import DAYS_PER_L_YEAR @@ -20,6 +21,8 @@ from pendulum.constants import SECS_PER_YEAR from pendulum.constants import TM_DECEMBER from pendulum.constants import TM_JANUARY +from pendulum.tz.timezone import Timezone +from pendulum.utils._compat import zoneinfo class PreciseDiff( @@ -28,7 +31,16 @@ class PreciseDiff( "years months days " "hours minutes seconds microseconds " "total_days", ) ): - def __repr__(self): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + + def __repr__(self) -> str: return ( f"{self.years} years " f"{self.months} months " @@ -45,7 +57,7 @@ def is_leap(year: int) -> bool: def is_long_year(year: int) -> bool: - def p(y): + def p(y: int) -> int: return y + y // 4 - y // 100 + y // 400 return p(year) % 7 == 4 or p(year - 1) % 7 == 3 @@ -103,14 +115,8 @@ def local_time( unix_time: int, utc_offset: int, microseconds: int ) -> tuple[int, int, int, int, int, int, int]: """ - Returns a UNIX time as a broken down time + Returns a UNIX time as a broken-down time for a particular transition type. - - :type unix_time: int - :type utc_offset: int - :type microseconds: int - - :rtype: tuple """ year = EPOCH_YEAR seconds = int(math.floor(unix_time)) @@ -173,7 +179,7 @@ def local_time( minute = seconds // SECS_PER_MIN second = seconds % SECS_PER_MIN - return (year, month, day, hour, minute, second, microseconds) + return year, month, day, hour, minute, second, microseconds def precise_diff( @@ -183,20 +189,19 @@ def precise_diff( Calculate a precise difference between two datetimes. :param d1: The first datetime - :type d1: datetime.datetime or datetime.date - :param d2: The second datetime - :type d2: datetime.datetime or datetime.date - - :rtype: PreciseDiff """ sign = 1 if d1 == d2: return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0) - tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None - tzinfo2 = d2.tzinfo if isinstance(d2, datetime.datetime) else None + tzinfo1: datetime.tzinfo | None = ( + d1.tzinfo if isinstance(d1, datetime.datetime) else None + ) + tzinfo2: datetime.tzinfo | None = ( + d2.tzinfo if isinstance(d2, datetime.datetime) else None + ) if ( tzinfo1 is None @@ -227,22 +232,8 @@ def precise_diff( # Trying to figure out the timezone names # If we can't find them, we assume different timezones if tzinfo1 and tzinfo2: - if hasattr(tzinfo1, "key"): - # zoneinfo timezone - tz1 = tzinfo1.key - elif hasattr(tzinfo1, "name"): - # Pendulum timezone - tz1 = tzinfo1.name - elif hasattr(tzinfo1, "zone"): - # pytz timezone - tz1 = tzinfo1.zone - - if hasattr(tzinfo2, "key"): - tz2 = tzinfo2.key - elif hasattr(tzinfo2, "name"): - tz2 = tzinfo2.name - elif hasattr(tzinfo2, "zone"): - tz2 = tzinfo2.zone + tz1 = _get_tzinfo_name(tzinfo1) + tz2 = _get_tzinfo_name(tzinfo2) in_same_tz = tz1 == tz2 and tz1 is not None @@ -354,3 +345,20 @@ def _day_number(year: int, month: int, day: int) -> int: + (month * 306 + 5) // 10 + (day - 1) ) + + +def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None: + if tzinfo is None: + return None + + if hasattr(tzinfo, "key"): + # zoneinfo timezone + return cast(str, cast(zoneinfo.ZoneInfo, tzinfo).key) + elif hasattr(tzinfo, "name"): + # Pendulum timezone + return cast(Timezone, tzinfo).name + elif hasattr(tzinfo, "zone"): + # pytz timezone + return tzinfo.zone # type: ignore + + return None diff --git a/pendulum/constants.py b/pendulum/constants.py index c7b3876a..a3d2a18e 100644 --- a/pendulum/constants.py +++ b/pendulum/constants.py @@ -1,7 +1,6 @@ # The day constants from __future__ import annotations - SUNDAY = 0 MONDAY = 1 TUESDAY = 2 diff --git a/pendulum/date.py b/pendulum/date.py index 925f7a87..f1303494 100644 --- a/pendulum/date.py +++ b/pendulum/date.py @@ -4,7 +4,11 @@ import math from datetime import date +from datetime import datetime from datetime import timedelta +from typing import NoReturn +from typing import cast +from typing import overload import pendulum @@ -20,12 +24,13 @@ from pendulum.constants import YEARS_PER_DECADE from pendulum.exceptions import PendulumException from pendulum.helpers import add_duration +from pendulum.helpers import get_test_now +from pendulum.helpers import has_test_now from pendulum.mixins.default import FormattableMixin from pendulum.period import Period class Date(FormattableMixin, date): - # Names of days of the week _days = { SUNDAY: "Sunday", @@ -41,54 +46,52 @@ class Date(FormattableMixin, date): # Getters/Setters - def set(self, year=None, month=None, day=None): + def set( + self, year: int | None = None, month: int | None = None, day: int | None = None + ) -> Date: return self.replace(year=year, month=month, day=day) @property - def day_of_week(self): + def day_of_week(self) -> int: """ Returns the day of the week (0-6). - - :rtype: int """ return self.isoweekday() % 7 @property - def day_of_year(self): + def day_of_year(self) -> int: """ Returns the day of the year (1-366). - - :rtype: int """ k = 1 if self.is_leap_year() else 2 return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30 @property - def week_of_year(self): + def week_of_year(self) -> int: return self.isocalendar()[1] @property - def days_in_month(self): + def days_in_month(self) -> int: return calendar.monthrange(self.year, self.month)[1] @property - def week_of_month(self): + def week_of_month(self) -> int: first_day_of_month = self.replace(day=1) return self.week_of_year - first_day_of_month.week_of_year + 1 @property - def age(self): + def age(self) -> int: return self.diff(abs=False).in_years() @property - def quarter(self): + def quarter(self) -> int: return int(math.ceil(self.month / 3)) # String Formatting - def to_date_string(self): + def to_date_string(self) -> str: """ Format the instance as date. @@ -96,7 +99,7 @@ def to_date_string(self): """ return self.strftime("%Y-%m-%d") - def to_formatted_date_string(self): + def to_formatted_date_string(self) -> str: """ Format the instance as a readable date. @@ -104,19 +107,14 @@ def to_formatted_date_string(self): """ return self.strftime("%b %d, %Y") - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})" # COMPARISONS - def closest(self, dt1, dt2): + def closest(self, dt1: date, dt2: date) -> Date: """ Get the closest date from the instance. - - :type dt1: Date or date - :type dt2: Date or date - - :rtype: Date """ dt1 = self.__class__(dt1.year, dt1.month, dt1.day) dt2 = self.__class__(dt2.year, dt2.month, dt2.day) @@ -126,14 +124,9 @@ def closest(self, dt1, dt2): return dt2 - def farthest(self, dt1, dt2): + def farthest(self, dt1: date, dt2: date) -> Date: """ Get the farthest date from the instance. - - :type dt1: Date or date - :type dt2: Date or date - - :rtype: Date """ dt1 = self.__class__(dt1.year, dt1.month, dt1.day) dt2 = self.__class__(dt2.year, dt2.month, dt2.day) @@ -143,57 +136,43 @@ def farthest(self, dt1, dt2): return dt2 - def is_future(self): + def is_future(self) -> bool: """ Determines if the instance is in the future, ie. greater than now. - - :rtype: bool """ return self > self.today() - def is_past(self): + def is_past(self) -> bool: """ Determines if the instance is in the past, ie. less than now. - - :rtype: bool """ return self < self.today() - def is_leap_year(self): + def is_leap_year(self) -> bool: """ Determines if the instance is a leap year. - - :rtype: bool """ return calendar.isleap(self.year) - def is_long_year(self): + def is_long_year(self) -> bool: """ Determines if the instance is a long year See link ``_ - - :rtype: bool """ return Date(self.year, 12, 28).isocalendar()[1] == 53 - def is_same_day(self, dt): + def is_same_day(self, dt: date) -> bool: """ Checks if the passed in date is the same day as the instance current day. - - :type dt: Date or date - - :rtype: bool """ return self == dt - def is_anniversary(self, dt=None): + def is_anniversary(self, dt: date | None = None) -> bool: """ - Check if its the anniversary. + Check if it's the anniversary. Compares the date/month values of the two dates. - - :rtype: bool """ if dt is None: dt = Date.today() @@ -207,25 +186,18 @@ def is_anniversary(self, dt=None): # the old name can be completely replaced with the new in one of the future versions is_birthday = is_anniversary - # ADDITIONS AND SUBSTRACTIONS + # ADDITIONS AND SUBTRACTIONS - def add(self, years=0, months=0, weeks=0, days=0): + def add( + self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 + ) -> Date: """ Add duration to the instance. :param years: The number of years - :type years: int - :param months: The number of months - :type months: int - :param weeks: The number of weeks - :type weeks: int - :param days: The number of days - :type days: int - - :rtype: Date """ dt = add_duration( date(self.year, self.month, self.day), @@ -237,34 +209,24 @@ def add(self, years=0, months=0, weeks=0, days=0): return self.__class__(dt.year, dt.month, dt.day) - def subtract(self, years=0, months=0, weeks=0, days=0): + def subtract( + self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 + ) -> Date: """ Remove duration from the instance. :param years: The number of years - :type years: int - :param months: The number of months - :type months: int - :param weeks: The number of weeks - :type weeks: int - :param days: The number of days - :type days: int - - :rtype: Date """ return self.add(years=-years, months=-months, weeks=-weeks, days=-days) - def _add_timedelta(self, delta): + def _add_timedelta(self, delta: timedelta) -> Date: """ Add timedelta duration to the instance. :param delta: The timedelta instance - :type delta: pendulum.Duration or datetime.timedelta - - :rtype: Date """ if isinstance(delta, pendulum.Duration): return self.add( @@ -276,14 +238,11 @@ def _add_timedelta(self, delta): return self.add(days=delta.days) - def _subtract_timedelta(self, delta): + def _subtract_timedelta(self, delta: timedelta) -> Date: """ Remove timedelta duration from the instance. :param delta: The timedelta instance - :type delta: pendulum.Duration or datetime.timedelta - - :rtype: Date """ if isinstance(delta, pendulum.Duration): return self.subtract( @@ -295,13 +254,25 @@ def _subtract_timedelta(self, delta): return self.subtract(days=delta.days) - def __add__(self, other): + def __add__(self, other: timedelta) -> Date: if not isinstance(other, timedelta): return NotImplemented return self._add_timedelta(other) - def __sub__(self, other): + @overload + def __sub__(self, delta: timedelta) -> Date: + ... + + @overload + def __sub__(self, dt: datetime) -> NoReturn: + ... + + @overload + def __sub__(self, dt: Date) -> Period: + ... + + def __sub__(self, other: timedelta | date) -> Date | Period: if isinstance(other, timedelta): return self._subtract_timedelta(other) @@ -314,23 +285,24 @@ def __sub__(self, other): # DIFFERENCES - def diff(self, dt=None, abs=True): + def diff(self, dt: date | None = None, abs: bool = True) -> Period: """ Returns the difference between two Date objects as a Period. - :type dt: Date or None - + :param dt: The date to compare to (defaults to today) :param abs: Whether to return an absolute interval or not - :type abs: bool - - :rtype: Period """ if dt is None: dt = self.today() return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs) - def diff_for_humans(self, other=None, absolute=False, locale=None): + def diff_for_humans( + self, + other: date | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: """ Get the difference in a human readable format in the current locale. @@ -350,15 +322,9 @@ def diff_for_humans(self, other=None, absolute=False, locale=None): 1 day after 5 months after - :type other: Date - + :param other: The date to compare to (defaults to today) :param absolute: removes time difference modifiers ago, after, etc - :type absolute: bool - :param locale: The locale to use for localization - :type locale: str - - :rtype: str """ is_now = other is None @@ -371,7 +337,7 @@ def diff_for_humans(self, other=None, absolute=False, locale=None): # MODIFIERS - def start_of(self, unit): + def start_of(self, unit: str) -> Date: """ Returns a copy of the instance with the time reset with the following rules: @@ -384,16 +350,13 @@ def start_of(self, unit): * century: date to first day of century and time to 00:00:00 :param unit: The unit to reset to - :type unit: str - - :rtype: Date """ if unit not in self._MODIFIERS_VALID_UNITS: raise ValueError(f'Invalid unit "{unit}" for start_of()') - return getattr(self, f"_start_of_{unit}")() + return cast(Date, getattr(self, f"_start_of_{unit}")()) - def end_of(self, unit): + def end_of(self, unit: str) -> Date: """ Returns a copy of the instance with the time reset with the following rules: @@ -405,108 +368,83 @@ def end_of(self, unit): * century: date to last day of century :param unit: The unit to reset to - :type unit: str - - :rtype: Date """ if unit not in self._MODIFIERS_VALID_UNITS: raise ValueError(f'Invalid unit "{unit}" for end_of()') - return getattr(self, f"_end_of_{unit}")() + return cast(Date, getattr(self, f"_end_of_{unit}")()) - def _start_of_day(self): + def _start_of_day(self) -> Date: """ Compatibility method. - - :rtype: Date """ return self - def _end_of_day(self): + def _end_of_day(self) -> Date: """ Compatibility method - - :rtype: Date """ return self - def _start_of_month(self): + def _start_of_month(self) -> Date: """ Reset the date to the first day of the month. - - :rtype: Date """ return self.set(self.year, self.month, 1) - def _end_of_month(self): + def _end_of_month(self) -> Date: """ Reset the date to the last day of the month. - - :rtype: Date """ return self.set(self.year, self.month, self.days_in_month) - def _start_of_year(self): + def _start_of_year(self) -> Date: """ Reset the date to the first day of the year. - - :rtype: Date """ return self.set(self.year, 1, 1) - def _end_of_year(self): + def _end_of_year(self) -> Date: """ Reset the date to the last day of the year. - - :rtype: Date """ return self.set(self.year, 12, 31) - def _start_of_decade(self): + def _start_of_decade(self) -> Date: """ Reset the date to the first day of the decade. - - :rtype: Date """ year = self.year - self.year % YEARS_PER_DECADE return self.set(year, 1, 1) - def _end_of_decade(self): + def _end_of_decade(self) -> Date: """ Reset the date to the last day of the decade. - - :rtype: Date """ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 return self.set(year, 12, 31) - def _start_of_century(self): + def _start_of_century(self) -> Date: """ Reset the date to the first day of the century. - - :rtype: Date """ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 return self.set(year, 1, 1) - def _end_of_century(self): + def _end_of_century(self) -> Date: """ Reset the date to the last day of the century. - - :rtype: Date """ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY return self.set(year, 12, 31) - def _start_of_week(self): + def _start_of_week(self) -> Date: """ Reset the date to the first day of the week. - - :rtype: Date """ dt = self @@ -515,11 +453,9 @@ def _start_of_week(self): return dt.start_of("day") - def _end_of_week(self): + def _end_of_week(self) -> Date: """ Reset the date to the last day of the week. - - :rtype: Date """ dt = self @@ -528,7 +464,7 @@ def _end_of_week(self): return dt.end_of("day") - def next(self, day_of_week=None): + def next(self, day_of_week: int | None = None) -> Date: """ Modify to the next occurrence of a given day of the week. If no day_of_week is provided, modify to the next occurrence @@ -536,9 +472,6 @@ def next(self, day_of_week=None): to indicate the desired day_of_week, ex. pendulum.MONDAY. :param day_of_week: The next day of week to reset to. - :type day_of_week: int or None - - :rtype: Date """ if day_of_week is None: day_of_week = self.day_of_week @@ -552,7 +485,7 @@ def next(self, day_of_week=None): return dt - def previous(self, day_of_week=None): + def previous(self, day_of_week: int | None = None) -> Date: """ Modify to the previous occurrence of a given day of the week. If no day_of_week is provided, modify to the previous occurrence @@ -560,9 +493,6 @@ def previous(self, day_of_week=None): to indicate the desired day_of_week, ex. pendulum.MONDAY. :param day_of_week: The previous day of week to reset to. - :type day_of_week: int or None - - :rtype: Date """ if day_of_week is None: day_of_week = self.day_of_week @@ -576,7 +506,7 @@ def previous(self, day_of_week=None): return dt - def first_of(self, unit, day_of_week=None): + def first_of(self, unit: str, day_of_week: int | None = None) -> Date: """ Returns an instance set to the first occurrence of a given day of the week in the current unit. @@ -586,18 +516,14 @@ def first_of(self, unit, day_of_week=None): Supported units are month, quarter and year. :param unit: The unit to use - :type unit: str - - :type day_of_week: int or None - - :rtype: Date + :param day_of_week: The day of week to reset to. """ if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - return getattr(self, f"_first_of_{unit}")(day_of_week) + return cast(Date, getattr(self, f"_first_of_{unit}")(day_of_week)) - def last_of(self, unit, day_of_week=None): + def last_of(self, unit: str, day_of_week: int | None = None) -> Date: """ Returns an instance set to the last occurrence of a given day of the week in the current unit. @@ -607,18 +533,14 @@ def last_of(self, unit, day_of_week=None): Supported units are month, quarter and year. :param unit: The unit to use - :type unit: str - - :type day_of_week: int or None - - :rtype: Date + :param day_of_week: The day of week to reset to. """ if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - return getattr(self, f"_last_of_{unit}")(day_of_week) + return cast(Date, getattr(self, f"_last_of_{unit}")(day_of_week)) - def nth_of(self, unit, nth, day_of_week): + def nth_of(self, unit: str, nth: int, day_of_week: int) -> Date: """ Returns a new instance set to the given occurrence of a given day of the week in the current unit. @@ -629,19 +551,14 @@ def nth_of(self, unit, nth, day_of_week): Supported units are month, quarter and year. :param unit: The unit to use - :type unit: str - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date + :param nth: The occurrence to use + :param day_of_week: The day of week to set to. """ if unit not in ["month", "quarter", "year"]: raise ValueError(f'Invalid unit "{unit}" for first_of()') - dt = getattr(self, f"_nth_of_{unit}")(nth, day_of_week) - if dt is False: + dt = cast(Date, getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) + if not dt: raise PendulumException( f"Unable to find occurence {nth}" f" of {self._days[day_of_week]} in {unit}" @@ -649,16 +566,14 @@ def nth_of(self, unit, nth, day_of_week): return dt - def _first_of_month(self, day_of_week): + def _first_of_month(self, day_of_week: int) -> Date: """ Modify to the first occurrence of a given day of the week in the current month. If no day_of_week is provided, modify to the first day of the month. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - :type day_of_week: int - - :rtype: Date + :param day_of_week: The day of week to set to. """ dt = self @@ -676,16 +591,14 @@ def _first_of_month(self, day_of_week): return dt.set(day=day_of_month) - def _last_of_month(self, day_of_week=None): + def _last_of_month(self, day_of_week: int | None = None) -> Date: """ Modify to the last occurrence of a given day of the week in the current month. If no day_of_week is provided, modify to the last day of the month. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - :type day_of_week: int or None - - :rtype: Date + :param day_of_week: The day of week to set to. """ dt = self @@ -703,19 +616,13 @@ def _last_of_month(self, day_of_week=None): return dt.set(day=day_of_month) - def _nth_of_month(self, nth, day_of_week): + def _nth_of_month(self, nth: int, day_of_week: int) -> Date | None: """ Modify to the given occurrence of a given day of the week in the current month. If the calculated occurrence is outside, the scope of the current month, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date """ if nth == 1: return self.first_of("month", day_of_week) @@ -728,49 +635,35 @@ def _nth_of_month(self, nth, day_of_week): if dt.format("YYYY-MM") == check: return self.set(day=dt.day) - return False + return None - def _first_of_quarter(self, day_of_week=None): + def _first_of_quarter(self, day_of_week: int | None = None) -> Date: """ Modify to the first occurrence of a given day of the week in the current quarter. If no day_of_week is provided, modify to the first day of the quarter. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(self.year, self.quarter * 3 - 2, 1).first_of( "month", day_of_week ) - def _last_of_quarter(self, day_of_week=None): + def _last_of_quarter(self, day_of_week: int | None = None) -> Date: """ Modify to the last occurrence of a given day of the week in the current quarter. If no day_of_week is provided, modify to the last day of the quarter. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week) - def _nth_of_quarter(self, nth, day_of_week): + def _nth_of_quarter(self, nth: int, day_of_week: int) -> Date | None: """ Modify to the given occurrence of a given day of the week in the current quarter. If the calculated occurrence is outside, the scope of the current quarter, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date """ if nth == 1: return self.first_of("quarter", day_of_week) @@ -783,49 +676,35 @@ def _nth_of_quarter(self, nth, day_of_week): dt = dt.next(day_of_week) if last_month < dt.month or year != dt.year: - return False + return None return self.set(self.year, dt.month, dt.day) - def _first_of_year(self, day_of_week=None): + def _first_of_year(self, day_of_week: int | None = None) -> Date: """ Modify to the first occurrence of a given day of the week in the current year. If no day_of_week is provided, modify to the first day of the year. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(month=1).first_of("month", day_of_week) - def _last_of_year(self, day_of_week=None): + def _last_of_year(self, day_of_week: int | None = None) -> Date: """ Modify to the last occurrence of a given day of the week in the current year. If no day_of_week is provided, modify to the last day of the year. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) - def _nth_of_year(self, nth, day_of_week): + def _nth_of_year(self, nth: int, day_of_week: int) -> Date | None: """ Modify to the given occurrence of a given day of the week in the current year. If the calculated occurrence is outside, the scope of the current year, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date """ if nth == 1: return self.first_of("year", day_of_week) @@ -836,18 +715,14 @@ def _nth_of_year(self, nth, day_of_week): dt = dt.next(day_of_week) if year != dt.year: - return False + return None return self.set(self.year, dt.month, dt.day) - def average(self, dt=None): + def average(self, dt: date | None = None) -> Date: """ Modify the current instance to the average of a given instance (default now) and the current instance. - - :type dt: Date or date - - :rtype: Date """ if dt is None: dt = Date.today() @@ -857,22 +732,32 @@ def average(self, dt=None): # Native methods override @classmethod - def today(cls): - return pendulum.today().date() + def today(cls) -> Date: + if has_test_now(): + return cast(pendulum.DateTime, get_test_now()).date() + + dt = date.today() + + return cls(dt.year, dt.month, dt.day) @classmethod - def fromtimestamp(cls, t): + def fromtimestamp(cls, t: float) -> Date: dt = super().fromtimestamp(t) return cls(dt.year, dt.month, dt.day) @classmethod - def fromordinal(cls, n): + def fromordinal(cls, n: int) -> Date: dt = super().fromordinal(n) return cls(dt.year, dt.month, dt.day) - def replace(self, year=None, month=None, day=None): + def replace( + self, + year: int | None = None, + month: int | None = None, + day: int | None = None, + ) -> Date: 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 03fb28d8..1c6872d2 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -3,7 +3,12 @@ import calendar import datetime +from typing import TYPE_CHECKING +from typing import Any from typing import Callable +from typing import Optional +from typing import cast +from typing import overload import pendulum @@ -37,14 +42,16 @@ from pendulum.tz.timezone import Timezone from pendulum.utils._compat import PY38 +if TYPE_CHECKING: + from typing import Literal -class DateTime(datetime.datetime, Date): - EPOCH: DateTime | None = None +class DateTime(datetime.datetime, Date): + EPOCH: DateTime # Formats - _FORMATS: dict[str, str | Callable] = { + _FORMATS: dict[str, str | Callable[[datetime.datetime], str]] = { "atom": ATOM, "cookie": COOKIE, "iso8601": lambda dt: dt.isoformat(), @@ -58,8 +65,6 @@ class DateTime(datetime.datetime, Date): "w3c": W3C, } - _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC) - _MODIFIERS_VALID_UNITS: list[str] = [ "second", "minute", @@ -72,6 +77,8 @@ class DateTime(datetime.datetime, Date): "century", ] + _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC) + @classmethod def create( cls, @@ -82,8 +89,8 @@ def create( minute: int = 0, second: int = 0, microsecond: int = 0, - tz: str | float | Timezone | None = UTC, - fold: int | None = 1, + tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC, + fold: int = 1, raise_on_unknown_times: bool = False, ) -> DateTime: """ @@ -111,13 +118,25 @@ def create( fold=dt.fold, ) + @overload @classmethod - def now(cls, tz: str | Timezone | None = None) -> DateTime: + def now(cls, tz: datetime.tzinfo | None = None) -> DateTime: + ... + + @overload + @classmethod + def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> DateTime: + ... + + @classmethod + def now( + cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None + ) -> DateTime: """ Get a DateTime instance for the current date and time. """ if has_test_now(): - test_instance = get_test_now() + test_instance: DateTime = cast(DateTime, get_test_now()) _tz = pendulum._safe_timezone(tz) if tz is not None and _tz != test_instance.timezone: @@ -151,11 +170,11 @@ def utcnow(cls) -> DateTime: """ Get a DateTime instance for the current date and time in UTC. """ - return pendulum.now(UTC) + return cls.now(UTC) @classmethod def today(cls) -> DateTime: - return pendulum.now() + return cls.now() @classmethod def strptime(cls, time: str, fmt: str) -> DateTime: @@ -165,15 +184,15 @@ def strptime(cls, time: str, fmt: str) -> DateTime: def set( self, - year=None, - month=None, - day=None, - hour=None, - minute=None, - second=None, - microsecond=None, - tz=None, - ): + 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, + tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None, + ) -> DateTime: if year is None: year = self.year if month is None: @@ -220,22 +239,27 @@ def int_timestamp(self) -> int: return delta.days * SECONDS_PER_DAY + delta.seconds @property - def offset(self) -> int: + def offset(self) -> int | None: return self.get_offset() @property - def offset_hours(self) -> int: - return self.get_offset() / SECONDS_PER_MINUTE / MINUTES_PER_HOUR + def offset_hours(self) -> float | None: + offset = self.get_offset() + + if offset is None: + return None + + return offset / SECONDS_PER_MINUTE / MINUTES_PER_HOUR @property - def timezone(self) -> Timezone | None: + def timezone(self) -> Timezone | FixedTimezone | None: if not isinstance(self.tzinfo, (Timezone, FixedTimezone)): - return + return None return self.tzinfo @property - def tz(self) -> Timezone | None: + def tz(self) -> Timezone | FixedTimezone | None: return self.timezone @property @@ -243,7 +267,7 @@ def timezone_name(self) -> str | None: tz = self.timezone if tz is None: - return + return None return tz.name @@ -260,8 +284,12 @@ def is_utc(self) -> bool: def is_dst(self) -> bool: return self.dst() != datetime.timedelta() - def get_offset(self) -> int: - return int(self.utcoffset().total_seconds()) + def get_offset(self) -> int | None: + utcoffset = self.utcoffset() + if utcoffset is None: + return None + + return int(utcoffset.total_seconds()) def date(self) -> Date: return Date(self.year, self.month, self.day) @@ -299,7 +327,7 @@ def at( hour=hour, minute=minute, second=second, microsecond=microsecond ) - def in_timezone(self, tz: str | Timezone) -> DateTime: + def in_timezone(self, tz: str | Timezone | FixedTimezone) -> DateTime: """ Set the instance's timezone from a string or object. """ @@ -309,9 +337,9 @@ def in_timezone(self, tz: str | Timezone) -> DateTime: if not self.timezone: dt = dt.replace(fold=1) - return tz.convert(dt) + return cast(DateTime, tz.convert(dt)) - def in_tz(self, tz: str | Timezone) -> DateTime: + def in_tz(self, tz: str | Timezone | FixedTimezone) -> DateTime: """ Set the instance's timezone from a string or object. """ @@ -415,11 +443,11 @@ def _to_string(self, fmt: str, locale: str | None = None) -> str: if fmt not in self._FORMATS: raise ValueError(f"Format [{fmt}] is not supported") - fmt = self._FORMATS[fmt] - if callable(fmt): - return fmt(self) + fmt_value = self._FORMATS[fmt] + if callable(fmt_value): + return fmt_value(self) - return self.format(fmt, locale=locale) + return self.format(fmt_value, locale=locale) def __str__(self) -> str: return self.isoformat("T") @@ -449,32 +477,21 @@ def __repr__(self) -> str: ) # Comparisons - def closest( - self, dt1: datetime.datetime, dt2: datetime.datetime, *dts: datetime.datetime - ) -> DateTime: + def closest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override] """ Get the farthest date from the instance. """ - dt1 = pendulum.instance(dt1) - dt2 = pendulum.instance(dt2) - dts = [dt1, dt2] + [pendulum.instance(x) for x in dts] - dts = [(abs(self - dt), dt) for dt in dts] + pdts = [pendulum.instance(x) for x in dts] - return min(dts)[1] + return min((abs(self - dt), dt) for dt in pdts)[1] - def farthest( - self, dt1: datetime.datetime, dt2: datetime.datetime, *dts: datetime.datetime - ) -> DateTime: + def farthest(self, *dts: datetime.datetime) -> DateTime: # type: ignore[override] """ Get the farthest date from the instance. """ - dt1 = pendulum.instance(dt1) - dt2 = pendulum.instance(dt2) + pdts = [pendulum.instance(x) for x in dts] - dts = [dt1, dt2] + [pendulum.instance(x) for x in dts] - dts = [(abs(self - dt), dt) for dt in dts] - - return max(dts)[1] + return max((abs(self - dt), dt) for dt in pdts)[1] def is_future(self) -> bool: """ @@ -499,7 +516,7 @@ def is_long_year(self) -> bool: == 53 ) - def is_same_day(self, dt: datetime.datetime) -> bool: + def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override] """ Checks if the passed in date is the same day as the instance current day. @@ -508,7 +525,9 @@ def is_same_day(self, dt: datetime.datetime) -> bool: return self.to_date_string() == dt.to_date_string() - def is_anniversary(self, dt: datetime.datetime | None = None) -> bool: + def is_anniversary( # type: ignore[override] + self, dt: datetime.datetime | None = None + ) -> bool: """ Check if its the anniversary. Compares the date/month values of the two dates. @@ -530,7 +549,7 @@ def add( days: int = 0, hours: int = 0, minutes: int = 0, - seconds: int = 0, + seconds: float = 0, microseconds: int = 0, ) -> DateTime: """ @@ -568,7 +587,7 @@ def add( microseconds=microseconds, ) - if units_of_variable_length or self.tzinfo is None: + if units_of_variable_length or self.tz is None: return DateTime.create( dt.year, dt.month, @@ -613,7 +632,7 @@ def subtract( days: int = 0, hours: int = 0, minutes: int = 0, - seconds: int = 0, + seconds: float = 0, microseconds: int = 0, ) -> DateTime: """ @@ -668,7 +687,9 @@ def _subtract_timedelta(self, delta: datetime.timedelta) -> DateTime: # DIFFERENCES - def diff(self, dt: DateTime | None = None, abs: bool = True) -> Period: + def diff( # type: ignore[override] + self, dt: datetime.datetime | None = None, abs: bool = True + ) -> Period: """ Returns the difference between two DateTime objects represented as a Period. """ @@ -677,7 +698,7 @@ def diff(self, dt: DateTime | None = None, abs: bool = True) -> Period: return Period(self, dt, absolute=abs) - def diff_for_humans( + def diff_for_humans( # type: ignore[override] self, other: DateTime | None = None, absolute: bool = False, @@ -730,7 +751,7 @@ 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 getattr(self, f"_start_of_{unit}")() + return cast(DateTime, getattr(self, f"_start_of_{unit}")()) def end_of(self, unit: str) -> DateTime: """ @@ -750,7 +771,7 @@ 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 getattr(self, f"_end_of_{unit}")() + return cast(DateTime, getattr(self, f"_end_of_{unit}")()) def _start_of_second(self) -> DateTime: """ @@ -947,7 +968,7 @@ 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 getattr(self, f"_first_of_{unit}")(day_of_week) + return cast(DateTime, getattr(self, f"_first_of_{unit}")(day_of_week)) def last_of(self, unit: str, day_of_week: int | None = None) -> DateTime: """ @@ -961,7 +982,7 @@ 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 getattr(self, f"_last_of_{unit}")(day_of_week) + return cast(DateTime, getattr(self, f"_last_of_{unit}")(day_of_week)) def nth_of(self, unit: str, nth: int, day_of_week: int) -> DateTime: """ @@ -976,8 +997,10 @@ 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 = getattr(self, f"_nth_of_{unit}")(nth, day_of_week) - if dt is False: + dt = cast( + Optional[DateTime], getattr(self, f"_nth_of_{unit}")(nth, day_of_week) + ) + if not dt: raise PendulumException( f"Unable to find occurence {nth}" f" of {self._days[day_of_week]} in {unit}" @@ -1031,7 +1054,9 @@ 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: + def _nth_of_month( + self, nth: int, day_of_week: int | None = None + ) -> DateTime | None: """ Modify to the given occurrence of a given day of the week in the current month. If the calculated occurrence is outside, @@ -1050,7 +1075,7 @@ def _nth_of_month(self, nth: int, day_of_week: int | None = None) -> DateTime: if dt.format("%Y-%M") == check: return self.set(day=dt.day).start_of("day") - return False + return None def _first_of_quarter(self, day_of_week: int | None = None) -> DateTime: """ @@ -1072,19 +1097,15 @@ 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: + def _nth_of_quarter( + self, nth: int, day_of_week: int | None = None + ) -> DateTime | None: """ Modify to the given occurrence of a given day of the week in the current quarter. If the calculated occurrence is outside, the scope of the current quarter, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: DateTime """ if nth == 1: return self.first_of("quarter", day_of_week) @@ -1097,7 +1118,7 @@ def _nth_of_quarter(self, nth: int, day_of_week: int | None = None) -> DateTime: dt = dt.next(day_of_week) if last_month < dt.month or year != dt.year: - return False + return None return self.on(self.year, dt.month, dt.day).start_of("day") @@ -1119,7 +1140,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: + def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime | None: """ Modify to the given occurrence of a given day of the week in the current year. If the calculated occurrence is outside, @@ -1136,18 +1157,16 @@ def _nth_of_year(self, nth: int, day_of_week: int | None = None) -> DateTime: dt = dt.next(day_of_week) if year != dt.year: - return False + return None return self.on(self.year, dt.month, dt.day).start_of("day") - def average(self, dt: datetime.datetime | None = None) -> DateTime: + def average( # type: ignore[override] + self, dt: datetime.datetime | None = None + ) -> DateTime: """ Modify the current instance to the average of a given instance (default now) and the current instance. - - :type dt: DateTime or datetime - - :rtype: DateTime """ if dt is None: dt = self.now(self.tz) @@ -1157,6 +1176,14 @@ def average(self, dt: datetime.datetime | None = None) -> DateTime: microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2 ) + @overload # type: ignore[override] + def __sub__(self, other: datetime.timedelta) -> DateTime: + ... + + @overload + def __sub__(self, other: DateTime) -> Period: + ... + def __sub__( self, other: datetime.datetime | datetime.timedelta ) -> DateTime | Period: @@ -1214,7 +1241,7 @@ def __add__(self, other: datetime.timedelta) -> DateTime: caller = inspect.stack()[1][3] if caller == "astimezone": - return super().__add__(other) + return cast(DateTime, super().__add__(other)) return self._add_timedelta_(other) @@ -1225,19 +1252,28 @@ def __radd__(self, other: datetime.timedelta) -> DateTime: @classmethod def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> DateTime: - return pendulum.instance(datetime.datetime.fromtimestamp(t, tz=tz), tz=tz) + tzinfo = pendulum._safe_timezone(tz) + + return pendulum.instance( + datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo + ) @classmethod def utcfromtimestamp(cls, t: float) -> DateTime: return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None) @classmethod - def fromordinal(cls, n) -> DateTime: + def fromordinal(cls, n: int) -> DateTime: return pendulum.instance(datetime.datetime.fromordinal(n), tz=None) @classmethod - def combine(cls, date: datetime.date, time: datetime.time) -> DateTime: - return pendulum.instance(datetime.datetime.combine(date, time), tz=None) + def combine( + cls, + date: datetime.date, + time: datetime.time, + tzinfo: datetime.tzinfo | None = None, + ) -> DateTime: + return pendulum.instance(datetime.datetime.combine(date, time), tz=tzinfo) def astimezone(self, tz: datetime.tzinfo | None = None) -> DateTime: dt = super().astimezone(tz) @@ -1263,9 +1299,9 @@ def replace( minute: int | None = None, second: int | None = None, microsecond: int | None = None, - tzinfo: bool | datetime.tzinfo | None = True, + tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, fold: int | None = None, - ): + ) -> DateTime: if year is None: year = self.year if month is None: @@ -1285,14 +1321,27 @@ def replace( if fold is None: fold = self.fold + if tzinfo is not None: + tzinfo = pendulum._safe_timezone(tzinfo) + return DateTime.create( - year, month, day, hour, minute, second, microsecond, tz=tzinfo, fold=fold + year, + month, + day, + hour, + minute, + second, + microsecond, + tz=tzinfo, + fold=fold, ) - def __getnewargs__(self) -> tuple: + def __getnewargs__(self) -> tuple[DateTime]: return (self,) - def _getstate(self, protocol: int = 3) -> tuple: + def _getstate( + self, protocol: int = 3 + ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]: return ( self.year, self.month, @@ -1304,13 +1353,21 @@ def _getstate(self, protocol: int = 3) -> tuple: self.tzinfo, ) - def __reduce__(self) -> tuple: + def __reduce__( + self, + ) -> tuple[ + type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] + ]: return self.__reduce_ex__(2) - def __reduce_ex__(self, protocol: int) -> tuple: + def __reduce_ex__( # type: ignore[override] + self, protocol: int + ) -> tuple[ + type[DateTime], tuple[int, int, int, int, int, int, int, datetime.tzinfo | None] + ]: return self.__class__, self._getstate(protocol) - def _cmp(self, other: datetime.datetime, **kwargs) -> int: + def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int: # Fix for pypy which compares using this method # which would lead to infinite recursion if we didn't override dt = datetime.datetime( @@ -1328,6 +1385,8 @@ def _cmp(self, other: datetime.datetime, **kwargs) -> int: return 0 if dt == other else 1 if dt > other else -1 -DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) -DateTime.max: DateTime = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC) -DateTime.EPOCH: DateTime = DateTime(1970, 1, 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] diff --git a/pendulum/duration.py b/pendulum/duration.py index 88414ff3..a3a68b1a 100644 --- a/pendulum/duration.py +++ b/pendulum/duration.py @@ -1,6 +1,8 @@ from __future__ import annotations from datetime import timedelta +from typing import cast +from typing import overload import pendulum @@ -11,7 +13,7 @@ from pendulum.utils._compat import PYPY -def _divide_and_round(a, b): +def _divide_and_round(a: float, b: float) -> int: """divide a by b and round result to the nearest integer When the ratio is exactly half-way between two integers, @@ -43,6 +45,15 @@ class Duration(timedelta): Provides several improvements over the base class. """ + _total: float = 0 + _years: int = 0 + _months: int = 0 + _weeks: int = 0 + _days: int = 0 + _remaining_days: int = 0 + _seconds: int = 0 + _microseconds: int = 0 + _y = None _m = None _w = None @@ -54,16 +65,16 @@ class Duration(timedelta): def __new__( cls, - days=0, - seconds=0, - microseconds=0, - milliseconds=0, - minutes=0, - hours=0, - weeks=0, - years=0, - months=0, - ): + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, + ) -> Duration: if not isinstance(years, int) or not isinstance(months, int): raise ValueError("Float year and months are not supported") @@ -98,21 +109,21 @@ def __new__( return self - def total_minutes(self): + def total_minutes(self) -> float: return self.total_seconds() / SECONDS_PER_MINUTE - def total_hours(self): + def total_hours(self) -> float: return self.total_seconds() / SECONDS_PER_HOUR - def total_days(self): + def total_days(self) -> float: return self.total_seconds() / SECONDS_PER_DAY - def total_weeks(self): + def total_weeks(self) -> float: return self.total_days() / 7 if PYPY: - def total_seconds(self): + def total_seconds(self) -> float: days = 0 if hasattr(self, "_years"): @@ -132,29 +143,29 @@ def total_seconds(self): ) / US_PER_SECOND @property - def years(self): + def years(self) -> int: return self._years @property - def months(self): + def months(self) -> int: return self._months @property - def weeks(self): + def weeks(self) -> int: return self._weeks if PYPY: @property - def days(self): + def days(self) -> int: return self._years * 365 + self._months * 30 + self._days @property - def remaining_days(self): + def remaining_days(self) -> int: return self._remaining_days @property - def hours(self): + def hours(self) -> int: if self._h is None: seconds = self._seconds self._h = 0 @@ -164,7 +175,7 @@ def hours(self): return self._h @property - def minutes(self): + def minutes(self) -> int: if self._i is None: seconds = self._seconds self._i = 0 @@ -174,11 +185,11 @@ def minutes(self): return self._i @property - def seconds(self): + def seconds(self) -> int: return self._seconds @property - def remaining_seconds(self): + def remaining_seconds(self) -> int: if self._s is None: self._s = self._seconds self._s = abs(self._s) % 60 * self._sign(self._s) @@ -186,44 +197,39 @@ def remaining_seconds(self): return self._s @property - def microseconds(self): + def microseconds(self) -> int: return self._microseconds @property - def invert(self): + def invert(self) -> bool: if self._invert is None: self._invert = self.total_seconds() < 0 return self._invert - def in_weeks(self): + def in_weeks(self) -> int: return int(self.total_weeks()) - def in_days(self): + def in_days(self) -> int: return int(self.total_days()) - def in_hours(self): + def in_hours(self) -> int: return int(self.total_hours()) - def in_minutes(self): + def in_minutes(self) -> int: return int(self.total_minutes()) - def in_seconds(self): + def in_seconds(self) -> int: return int(self.total_seconds()) - def in_words(self, locale=None, separator=" "): + def in_words(self, locale: str | None = None, separator: str = " ") -> str: """ Get the current interval in words in the current locale. Ex: 6 jours 23 heures 58 minutes :param locale: The locale to use. Defaults to current locale. - :type locale: str - :param separator: The separator to use between each unit - :type separator: str - - :rtype: str """ periods = [ ("year", self.years), @@ -238,46 +244,45 @@ def in_words(self, locale=None, separator=" "): if locale is None: locale = pendulum.get_locale() - locale = pendulum.locale(locale) + loaded_locale = pendulum.locale(locale) + parts = [] for period in periods: - unit, count = period - if abs(count) > 0: - translation = locale.translation( - f"units.{unit}.{locale.plural(abs(count))}" + unit, period_count = period + if abs(period_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(period_count))}" ) - parts.append(translation.format(count)) + parts.append(translation.format(period_count)) if not parts: + count: int | str = 0 if abs(self.microseconds) > 0: - unit = f"units.second.{locale.plural(1)}" + unit = f"units.second.{loaded_locale.plural(1)}" count = f"{abs(self.microseconds) / 1e6:.2f}" else: - unit = f"units.microsecond.{locale.plural(0)}" - count = 0 - translation = locale.translation(unit) + unit = f"units.microsecond.{loaded_locale.plural(0)}" + translation = loaded_locale.translation(unit) parts.append(translation.format(count)) return separator.join(parts) - def _sign(self, value): + def _sign(self, value: float) -> int: if value < 0: return -1 return 1 - def as_timedelta(self): + def as_timedelta(self) -> timedelta: """ Return the interval as a native timedelta. - - :rtype: timedelta """ return timedelta(seconds=self.total_seconds()) - def __str__(self): + def __str__(self) -> str: return self.in_words() - def __repr__(self): + def __repr__(self) -> str: rep = f"{self.__class__.__name__}(" if self._years: @@ -308,7 +313,7 @@ def __repr__(self): return rep.replace(", )", ")") - def __add__(self, other): + def __add__(self, other: timedelta) -> Duration: if isinstance(other, timedelta): return self.__class__(seconds=self.total_seconds() + other.total_seconds()) @@ -316,13 +321,13 @@ def __add__(self, other): __radd__ = __add__ - def __sub__(self, other): + def __sub__(self, other: timedelta) -> Duration: if isinstance(other, timedelta): return self.__class__(seconds=self.total_seconds() - other.total_seconds()) return NotImplemented - def __neg__(self): + def __neg__(self) -> Duration: return self.__class__( years=-self._years, months=-self._months, @@ -332,10 +337,10 @@ def __neg__(self): microseconds=-self._microseconds, ) - def _to_microseconds(self): + def _to_microseconds(self) -> int: return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds - def __mul__(self, other): + def __mul__(self, other: int | float) -> Duration: if isinstance(other, int): return self.__class__( years=self._years * other, @@ -353,13 +358,21 @@ def __mul__(self, other): __rmul__ = __mul__ - def __floordiv__(self, other): + @overload + def __floordiv__(self, other: timedelta) -> int: + ... + + @overload + def __floordiv__(self, other: int) -> Duration: + ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: if not isinstance(other, (int, timedelta)): return NotImplemented usec = self._to_microseconds() if isinstance(other, timedelta): - return usec // other._to_microseconds() + return cast(int, usec // other._to_microseconds()) # type: ignore[attr-defined] if isinstance(other, int): return self.__class__( @@ -370,13 +383,21 @@ def __floordiv__(self, other): months=self._months // other, ) - def __truediv__(self, other): + @overload + def __truediv__(self, other: timedelta) -> float: + ... + + @overload + def __truediv__(self, other: float) -> Duration: + ... + + def __truediv__(self, other: int | float | timedelta) -> Duration | float: if not isinstance(other, (int, float, timedelta)): return NotImplemented usec = self._to_microseconds() if isinstance(other, timedelta): - return usec / other._to_microseconds() + return cast(float, usec / other._to_microseconds()) # type: ignore[attr-defined] if isinstance(other, int): return self.__class__( @@ -400,17 +421,17 @@ def __truediv__(self, other): __div__ = __floordiv__ - def __mod__(self, other): + def __mod__(self, other: timedelta) -> Duration: if isinstance(other, timedelta): - r = self._to_microseconds() % other._to_microseconds() + r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] return self.__class__(0, 0, r) return NotImplemented - def __divmod__(self, other): + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: if isinstance(other, timedelta): - q, r = divmod(self._to_microseconds(), other._to_microseconds()) + q, r = divmod(self._to_microseconds(), other._to_microseconds()) # type: ignore[attr-defined] return q, self.__class__(0, 0, r) @@ -431,16 +452,16 @@ class AbsoluteDuration(Duration): def __new__( cls, - days=0, - seconds=0, - microseconds=0, - milliseconds=0, - minutes=0, - hours=0, - weeks=0, - years=0, - months=0, - ): + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, + ) -> AbsoluteDuration: if not isinstance(years, int) or not isinstance(months, int): raise ValueError("Float year and months are not supported") @@ -470,11 +491,11 @@ def __new__( return self - def total_seconds(self): + def total_seconds(self) -> float: return abs(self._total) @property - def invert(self): + def invert(self) -> bool: if self._invert is None: self._invert = self._total < 0 diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py index 0c6e725d..975c409a 100644 --- a/pendulum/formatting/__init__.py +++ b/pendulum/formatting/__init__.py @@ -2,5 +2,4 @@ from pendulum.formatting.formatter import Formatter - __all__ = ["Formatter"] diff --git a/pendulum/formatting/difference_formatter.py b/pendulum/formatting/difference_formatter.py index 4d738fa4..dad219dd 100644 --- a/pendulum/formatting/difference_formatter.py +++ b/pendulum/formatting/difference_formatter.py @@ -4,9 +4,8 @@ from pendulum.locales.locale import Locale - if t.TYPE_CHECKING: - from pendulum import Period + from pendulum import Duration class DifferenceFormatter: @@ -14,40 +13,29 @@ class DifferenceFormatter: Handles formatting differences in text. """ - def __init__(self, locale="en"): + def __init__(self, locale: str = "en") -> None: self._locale = Locale.load(locale) def format( self, - diff: Period, + diff: Duration, is_now: bool = True, absolute: bool = False, - locale: str | None = None, + locale: str | Locale | None = None, ) -> str: """ Formats a difference. :param diff: The difference to format - :type diff: pendulum.period.Period - :param is_now: Whether the difference includes now - :type is_now: bool - :param absolute: Whether it's an absolute difference or not - :type absolute: bool - :param locale: The locale to use - :type locale: str or None - - :rtype: str """ if locale is None: locale = self._locale else: locale = Locale.load(locale) - count = diff.remaining_seconds - if diff.years > 0: unit = "year" count = diff.years @@ -89,7 +77,7 @@ def format( time = locale.get("custom.units.few_second") if time is not None: if absolute: - return time + return t.cast(str, time) key = "custom" is_future = diff.invert @@ -104,7 +92,7 @@ def format( else: key += ".before" - return locale.get(key).format(time) + return t.cast(str, locale.get(key).format(time)) else: unit = "second" count = diff.remaining_seconds @@ -151,8 +139,8 @@ def format( else: key += ".before" - return locale.get(key).format(time) + return t.cast(str, locale.get(key).format(time)) key += f".{locale.plural(count)}" - return locale.get(key).format(count) + return t.cast(str, locale.get(key).format(count)) diff --git a/pendulum/formatting/formatter.py b/pendulum/formatting/formatter.py index 29d1acf6..f91d5d92 100644 --- a/pendulum/formatting/formatter.py +++ b/pendulum/formatting/formatter.py @@ -2,12 +2,20 @@ import datetime import re -import typing + +from typing import TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import Match +from typing import Sequence +from typing import cast import pendulum from pendulum.locales.locale import Locale +if TYPE_CHECKING: + from pendulum import Timezone _MATCH_1 = r"\d" _MATCH_2 = r"\d\d" @@ -35,8 +43,7 @@ class Formatter: - - _TOKENS = ( + _TOKENS: str = ( r"\[([^\[]*)\]|\\(.)|" "(" "Mo|MM?M?M?" @@ -54,11 +61,11 @@ class Formatter: ")" ) - _FORMAT_RE = re.compile(_TOKENS) + _FORMAT_RE: re.Pattern[str] = re.compile(_TOKENS) - _FROM_FORMAT_RE = re.compile(r"(? st Formats a DateTime instance with a given token and locale. :param dt: The instance to format - :type dt: pendulum.DateTime - :param token: The token to use - :type token: str - :param locale: The locale to use - :type locale: Locale - - :rtype: str """ if token in self._DATE_FORMATS: fmt = locale.get(f"custom.date_formats.{token}") @@ -306,6 +296,8 @@ def _format_token(self, dt: pendulum.DateTime, token: str, locale: Locale) -> st return f"{sign}{hour:02d}{separator}{minute:02d}" + return token + def _format_localizable_token( self, dt: pendulum.DateTime, token: str, locale: Locale ) -> str: @@ -314,26 +306,21 @@ def _format_localizable_token( with a given localizable token and locale. :param dt: The instance to format - :type dt: pendulum.DateTime - :param token: The token to use - :type token: str - :param locale: The locale to use - :type locale: Locale - - :rtype: str """ if token == "MMM": - return locale.get("translations.months.abbreviated")[dt.month] + return cast(str, locale.get("translations.months.abbreviated")[dt.month]) elif token == "MMMM": - return locale.get("translations.months.wide")[dt.month] + return cast(str, locale.get("translations.months.wide")[dt.month]) elif token == "dd": - return locale.get("translations.days.short")[dt.day_of_week] + return cast(str, locale.get("translations.days.short")[dt.day_of_week]) elif token == "ddd": - return locale.get("translations.days.abbreviated")[dt.day_of_week] + return cast( + str, locale.get("translations.days.abbreviated")[dt.day_of_week] + ) elif token == "dddd": - return locale.get("translations.days.wide")[dt.day_of_week] + return cast(str, locale.get("translations.days.wide")[dt.day_of_week]) elif token == "Do": return locale.ordinalize(dt.day) elif token == "do": @@ -353,7 +340,7 @@ def _format_localizable_token( else: key += ".am" - return locale.get(key) + return cast(str, locale.get(key)) else: return token @@ -363,7 +350,7 @@ def parse( fmt: str, now: pendulum.DateTime, locale: str | None = None, - ) -> dict[str, typing.Any]: + ) -> dict[str, Any]: """ Parses a time string matching a given format as a tuple. @@ -378,12 +365,12 @@ def parse( tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) if not tokens: - return time + raise ValueError("The given time string does not match the given format") if not locale: locale = pendulum.get_locale() - locale = Locale.load(locale) + loaded_locale: Locale = Locale.load(locale) parsed = { "year": None, @@ -402,19 +389,23 @@ def parse( } pattern = self._FROM_FORMAT_RE.sub( - lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt + lambda m: self._replace_tokens(m.group(0), loaded_locale), escaped_fmt ) if not re.search("^" + pattern + "$", time): raise ValueError(f"String does not match format {fmt}") - re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time) + _get_parsed_values: Callable[ + [Match[str]], Any + ] = lambda m: self._get_parsed_values(m, parsed, loaded_locale, now) + + re.sub(pattern, _get_parsed_values, time) return self._check_parsed(parsed, now) def _check_parsed( - self, parsed: dict[str, typing.Any], now: pendulum.DateTime - ) -> dict[str, typing.Any]: + self, parsed: dict[str, Any], now: pendulum.DateTime + ) -> dict[str, Any]: """ Checks validity of parsed elements. @@ -422,7 +413,7 @@ def _check_parsed( :return: The validated elements. """ - validated = { + validated: dict[str, int | Timezone | None] = { "year": parsed["year"], "month": parsed["month"], "day": parsed["day"], @@ -474,14 +465,17 @@ def _check_parsed( validated["year"] = now.year if parsed["day_of_year"] is not None: - dt = pendulum.parse(f'{validated["year"]}-{parsed["day_of_year"]:>03d}') + dt = cast( + pendulum.DateTime, + pendulum.parse(f'{validated["year"]}-{parsed["day_of_year"]:>03d}'), + ) validated["month"] = dt.month validated["day"] = dt.day if parsed["day_of_week"] is not None: dt = pendulum.datetime( - validated["year"], + cast(int, validated["year"]), validated["month"] or now.month, validated["day"] or now.day, ) @@ -534,8 +528,8 @@ def _check_parsed( def _get_parsed_values( self, - m: typing.Match[str], - parsed: dict[str, typing.Any], + m: Match[str], + parsed: dict[str, Any], locale: Locale, now: pendulum.DateTime, ) -> None: @@ -549,7 +543,7 @@ def _get_parsed_value( self, token: str, value: str, - parsed: dict[str, typing.Any], + parsed: dict[str, Any], now: pendulum.DateTime, ) -> None: parsed_token = self._PARSE_TOKENS[token](value) @@ -610,7 +604,7 @@ def _get_parsed_value( parsed["tz"] = pendulum.timezone(value) def _get_parsed_locale_value( - self, token: str, value: str, parsed: dict[str, typing.Any], locale: Locale + self, token: str, value: str, parsed: dict[str, Any], locale: Locale ) -> None: if token == "MMMM": unit = "month" @@ -619,7 +613,7 @@ def _get_parsed_locale_value( unit = "month" match = "months.abbreviated" elif token == "Do": - parsed["day"] = int(re.match(r"(\d+)", value).group(1)) + parsed["day"] = int(cast(Match[str], re.match(r"(\d+)", value)).group(1)) return elif token == "dddd": @@ -671,16 +665,18 @@ def _replace_tokens(self, token: str, locale: Locale) -> str: candidates = values(locale) else: candidates = tuple( - locale.translation(self._LOCALIZABLE_TOKENS[token]).values() + locale.translation( + cast(str, self._LOCALIZABLE_TOKENS[token]) + ).values() ) else: - candidates = self._REGEX_TOKENS[token] + candidates = cast(Sequence[str], self._REGEX_TOKENS[token]) if not candidates: raise ValueError(f"Unsupported token: {token}") if not isinstance(candidates, tuple): - candidates = (candidates,) + candidates = (cast(str, candidates),) pattern = f'(?P<{token}>{"|".join(candidates)})' diff --git a/pendulum/helpers.py b/pendulum/helpers.py index 6052e17e..f5b68f80 100644 --- a/pendulum/helpers.py +++ b/pendulum/helpers.py @@ -19,10 +19,9 @@ from pendulum.formatting.difference_formatter import DifferenceFormatter from pendulum.locales.locale import Locale - if TYPE_CHECKING: # Prevent import cycles - from pendulum.period import Period + from pendulum.duration import Duration with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" @@ -34,6 +33,7 @@ if not with_extensions or struct.calcsize("P") == 4: raise ImportError() + from pendulum._extensions._helpers import PreciseDiff from pendulum._extensions._helpers import days_in_year from pendulum._extensions._helpers import is_leap from pendulum._extensions._helpers import is_long_year @@ -42,55 +42,55 @@ from pendulum._extensions._helpers import timestamp from pendulum._extensions._helpers import week_day except ImportError: - from pendulum._extensions.helpers import days_in_year # noqa: F401 + from pendulum._extensions.helpers import PreciseDiff # type: ignore[misc] + from pendulum._extensions.helpers import days_in_year from pendulum._extensions.helpers import is_leap - from pendulum._extensions.helpers import is_long_year # noqa: F401 - from pendulum._extensions.helpers import local_time # noqa: F401 - from pendulum._extensions.helpers import precise_diff # noqa: F401 - from pendulum._extensions.helpers import timestamp # noqa: F401 - from pendulum._extensions.helpers import week_day # noqa: F401 - + 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 timestamp + from pendulum._extensions.helpers import week_day difference_formatter = DifferenceFormatter() @overload def add_duration( - dt: _DT, + dt: datetime, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, hours: int = 0, minutes: int = 0, - seconds: int = 0, + seconds: float = 0, microseconds: int = 0, -) -> _DT: - pass +) -> datetime: + ... @overload def add_duration( - dt: _D, + dt: date, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, -) -> _D: +) -> date: pass def add_duration( - dt, - years=0, - months=0, - weeks=0, - days=0, - hours=0, - minutes=0, - seconds=0, - microseconds=0, -): + dt: date | datetime, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, +) -> date | datetime: """ Adds a duration to a date/datetime instance. """ @@ -112,7 +112,7 @@ def add_duration( if abs(seconds) > 59: s = _sign(seconds) - div, mod = divmod(seconds * s, 60) + div, mod = divmod(seconds * s, 60) # type: ignore[assignment] seconds = mod * s minutes += div * s @@ -160,7 +160,10 @@ def add_duration( def format_diff( - diff: Period, is_now: bool = True, absolute: bool = False, locale: str | None = None + diff: Duration, + is_now: bool = True, + absolute: bool = False, + locale: str | None = None, ) -> str: if locale is None: locale = get_locale() @@ -168,7 +171,7 @@ def format_diff( return difference_formatter.format(diff, is_now, absolute, locale) -def _sign(x): +def _sign(x: float) -> int: return int(copysign(1, x)) @@ -222,3 +225,26 @@ def week_ends_at(wday: int) -> None: raise ValueError("Invalid week day as start of week.") pendulum._WEEK_ENDS_AT = wday + + +__all__ = [ + "PreciseDiff", + "days_in_year", + "is_leap", + "is_long_year", + "local_time", + "precise_diff", + "timestamp", + "week_day", + "add_duration", + "format_diff", + "test", + "set_test_now", + "get_test_now", + "has_test_now", + "locale", + "set_locale", + "get_locale", + "week_starts_at", + "week_ends_at", +] diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py index 7f24caf3..637509a1 100644 --- a/pendulum/locales/locale.py +++ b/pendulum/locales/locale.py @@ -1,12 +1,12 @@ -import re -import sys +from __future__ import annotations from importlib import import_module -from typing import Any -from typing import Dict -from typing import Optional -from typing import Union +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 @@ -19,15 +19,15 @@ class Locale: Represent a specific locale. """ - _cache = {} + _cache: dict[str, Locale] = {} def __init__(self, locale: str, data: Any) -> None: - self._locale = locale - self._data = data - self._key_cache = {} + self._locale: str = locale + self._data: Any = data + self._key_cache: dict[str, str] = {} @classmethod - def load(cls, locale: Union[str, "Locale"]) -> "Locale": + def load(cls, locale: str | Locale) -> Locale: if isinstance(locale, Locale): return locale @@ -37,7 +37,7 @@ def load(cls, locale: Union[str, "Locale"]) -> "Locale": # Checking locale existence actual_locale = locale - locale_path = resources.files(__package__).joinpath(actual_locale) + locale_path = cast(Path, resources.files(__package__).joinpath(actual_locale)) while not locale_path.exists(): if actual_locale == locale: raise ValueError(f"Locale [{locale}] does not exist.") @@ -58,7 +58,7 @@ def normalize_locale(cls, locale: str) -> str: else: return locale.lower() - def get(self, key: str, default: Optional[Any] = None) -> Any: + def get(self, key: str, default: Any | None = None) -> Any: if key in self._key_cache: return self._key_cache[key] @@ -78,10 +78,10 @@ def translation(self, key: str) -> Any: return self.get(f"translations.{key}") def plural(self, number: int) -> str: - return self._data["plural"](number) + return cast(str, self._data["plural"](number)) def ordinal(self, number: int) -> str: - return self._data["ordinal"](number) + return cast(str, self._data["ordinal"](number)) def ordinalize(self, number: int) -> str: ordinal = self.get(f"custom.ordinal.{self.ordinal(number)}") @@ -91,12 +91,12 @@ def ordinalize(self, number: int) -> str: return f"{number}{ordinal}" - def match_translation(self, key: str, value: Any) -> Optional[Dict]: + def match_translation(self, key: str, value: Any) -> dict[str, str] | None: translations = self.translation(key) if value not in translations.values(): return None - return {v: k for k, v in translations.items()}[value] + return cast(Dict[str, str], {v: k for k, v in translations.items()}[value]) def __repr__(self) -> str: return f"{self.__class__.__name__}('{self._locale}')" diff --git a/pendulum/mixins/default.py b/pendulum/mixins/default.py index f6f0315c..59f985e5 100644 --- a/pendulum/mixins/default.py +++ b/pendulum/mixins/default.py @@ -2,37 +2,28 @@ from pendulum.formatting import Formatter - _formatter = Formatter() class FormattableMixin: + _formatter: Formatter = _formatter - _formatter = _formatter - - def format(self, fmt, locale=None): + def format(self, fmt: str, locale: str | None = None) -> str: """ Formats the instance using the given format. :param fmt: The format to use - :type fmt: str - :param locale: The locale to use - :type locale: str or None - - :rtype: str """ return self._formatter.format(self, fmt, locale) - def for_json(self): + def for_json(self) -> str: """ - Methods for automatic json serialization by simplejson - - :rtype: str + Methods for automatic json serialization by simplejson. """ return str(self) - def __format__(self, format_spec): + def __format__(self, format_spec: str) -> str: if len(format_spec) > 0: if "%" in format_spec: return self.strftime(format_spec) @@ -41,5 +32,5 @@ def __format__(self, format_spec): return str(self) - def __str__(self): + def __str__(self) -> str: return self.isoformat() diff --git a/pendulum/parser.py b/pendulum/parser.py index dc3ab489..52d6dbd5 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -7,20 +7,19 @@ from pendulum.parsing import _Interval from pendulum.parsing import parse as base_parse -from pendulum.tz import UTC - +from pendulum.tz.timezone import UTC if t.TYPE_CHECKING: from pendulum.date import Date from pendulum.datetime import DateTime - from pendulum.time import Duration + from pendulum.duration import Duration + from pendulum.period import Period from pendulum.time import Time - try: from pendulum.parsing._iso8601 import Duration as CDuration except ImportError: - CDuration = None + CDuration = None # type: ignore[misc, assignment] def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: @@ -30,14 +29,11 @@ def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: return _parse(text, **options) -def _parse(text, **options): +def _parse(text: str, **options: t.Any) -> Date | DateTime | Time | Duration | Period: """ Parses a string with the given options. :param text: The string to parse. - :type text: str - - :rtype: mixed """ # Handling special cases if text == "now": @@ -86,7 +82,9 @@ def _parse(text, **options): ), ) - dt = pendulum.instance(parsed.end, tz=options.get("tz", UTC)) + dt = pendulum.instance( + t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC) + ) return pendulum.period( dt.subtract( @@ -103,8 +101,12 @@ def _parse(text, **options): ) return pendulum.period( - pendulum.instance(parsed.start, tz=options.get("tz", UTC)), - pendulum.instance(parsed.end, tz=options.get("tz", UTC)), + pendulum.instance( + t.cast(datetime.datetime, parsed.start), tz=options.get("tz", UTC) + ), + pendulum.instance( + t.cast(datetime.datetime, parsed.end), tz=options.get("tz", UTC) + ), ) if CDuration and isinstance(parsed, CDuration): diff --git a/pendulum/parsing/__init__.py b/pendulum/parsing/__init__.py index 58554630..0e64065c 100644 --- a/pendulum/parsing/__init__.py +++ b/pendulum/parsing/__init__.py @@ -9,22 +9,25 @@ from datetime import date from datetime import datetime from datetime import time +from typing import Any +from typing import Optional +from typing import cast from dateutil import parser from pendulum.parsing.exceptions import ParserError - with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" try: if not with_extensions or struct.calcsize("P") == 4: raise ImportError() + from pendulum.parsing._iso8601 import Duration from pendulum.parsing._iso8601 import parse_iso8601 except ImportError: - from pendulum.parsing.iso8601 import parse_iso8601 - + from pendulum.duration import Duration # type: ignore[misc] + from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[misc] COMMON = re.compile( # Date (optional) # noqa: E800 @@ -52,7 +55,6 @@ re.VERBOSE, ) - DEFAULT_OPTIONS = { "day_first": False, "year_first": True, @@ -62,35 +64,31 @@ } -def parse(text, **options): +def parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration: """ Parses a string with the given options. :param text: The string to parse. - :type text: str - - :rtype: Parsed """ - _options = copy.copy(DEFAULT_OPTIONS) + _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS) _options.update(options) return _normalize(_parse(text, **_options), **_options) -def _normalize(parsed, **options): +def _normalize( + parsed: datetime | date | time | _Interval | Duration, **options: Any +) -> datetime | date | time | _Interval | Duration: """ Normalizes the parsed element. :param parsed: The parsed elements. - :type parsed: Parsed - - :rtype: Parsed """ if options.get("exact"): return parsed if isinstance(parsed, time): - now = options["now"] or datetime.now() + now = cast(Optional[datetime], options["now"]) or datetime.now() return datetime( now.year, @@ -107,7 +105,7 @@ def _normalize(parsed, **options): return parsed -def _parse(text, **options): +def _parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration: # Trying to parse ISO8601 with contextlib.suppress(ValueError): return parse_iso8601(text) @@ -134,14 +132,11 @@ def _parse(text, **options): return dt -def _parse_common(text, **options): +def _parse_common(text: str, **options: Any) -> datetime | date | time: """ Tries to parse the string as a common datetime format. :param text: The string to parse. - :type text: str - - :rtype: dict or None """ m = COMMON.match(text) has_date = False @@ -202,13 +197,18 @@ class _Interval: Special class to handle ISO 8601 intervals """ - def __init__(self, start=None, end=None, duration=None): + def __init__( + self, + start: datetime | None = None, + end: datetime | None = None, + duration: Duration | None = None, + ) -> None: self.start = start self.end = end self.duration = duration -def _parse_iso8601_interval(text): +def _parse_iso8601_interval(text: str) -> _Interval: if "/" not in text: raise ParserError("Invalid interval") @@ -228,4 +228,6 @@ def _parse_iso8601_interval(text): start = parse_iso8601(first) end = parse_iso8601(last) - return _Interval(start, end, duration) + return _Interval( + cast(datetime, start), cast(datetime, end), cast(Duration, duration) + ) diff --git a/pendulum/parsing/_iso8601.pyi b/pendulum/parsing/_iso8601.pyi new file mode 100644 index 00000000..b9ce5d4e --- /dev/null +++ b/pendulum/parsing/_iso8601.pyi @@ -0,0 +1,22 @@ +from __future__ import annotations + +from datetime import date +from datetime import datetime +from datetime import time + +class Duration: + + years: int = 0 + months: int = 0 + weeks: int = 0 + days: int = 0 + remaining_days: int = 0 + hours: int = 0 + minutes: int = 0 + seconds: int = 0 + remaining_seconds: int = 0 + microseconds: int = 0 + +def parse_iso8601( + text: str, +) -> datetime | date | time | Duration: ... diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py index a792ba7c..907cf13f 100644 --- a/pendulum/parsing/iso8601.py +++ b/pendulum/parsing/iso8601.py @@ -3,6 +3,8 @@ import datetime import re +from typing import cast + from pendulum.constants import HOURS_PER_DAY from pendulum.constants import MINUTES_PER_HOUR from pendulum.constants import MONTHS_OFFSETS @@ -16,7 +18,6 @@ from pendulum.tz.timezone import UTC from pendulum.tz.timezone import FixedTimezone - ISO8601_DT = re.compile( # Date (optional) # noqa: E800 "^" @@ -56,7 +57,6 @@ re.VERBOSE, ) - ISO8601_DURATION = re.compile( "^P" # Duration P indicator # Years, months and days (optional) # noqa: E800 @@ -79,7 +79,9 @@ ) -def parse_iso8601(text): +def parse_iso8601( + text: str, +) -> datetime.datetime | datetime.date | datetime.time | Duration: """ ISO 8601 compliant parser. @@ -105,175 +107,178 @@ def parse_iso8601(text): minute = 0 second = 0 microsecond = 0 - tzinfo = None - - if m: - if m.group("date"): - # A date has been specified - is_date = True - - if m.group("isocalendar"): - # We have a ISO 8601 string defined - # by week number - if ( - m.group("weeksep") - and not m.group("weekdaysep") - and m.group("isoweekday") - ): - raise ParserError(f"Invalid date string: {text}") - - if not m.group("weeksep") and m.group("weekdaysep"): - raise ParserError(f"Invalid date string: {text}") - - try: - date = _get_iso_8601_week( - m.group("isoyear"), m.group("isoweek"), m.group("isoweekday") - ) - except ParserError: - raise - except ValueError: - raise ParserError(f"Invalid date string: {text}") - - year = date["year"] - month = date["month"] - day = date["day"] - else: - # We have a classic date representation - year = int(m.group("year")) + tzinfo: FixedTimezone | None = None + + if m.group("date"): + # A date has been specified + is_date = True + + if m.group("isocalendar"): + # We have a ISO 8601 string defined + # by week number + if ( + m.group("weeksep") + and not m.group("weekdaysep") + and m.group("isoweekday") + ): + raise ParserError(f"Invalid date string: {text}") + + if not m.group("weeksep") and m.group("weekdaysep"): + raise ParserError(f"Invalid date string: {text}") + + try: + date = _get_iso_8601_week( + m.group("isoyear"), m.group("isoweek"), m.group("isoweekday") + ) + except ParserError: + raise + except ValueError: + raise ParserError(f"Invalid date string: {text}") + + year = date["year"] + month = date["month"] + day = date["day"] + else: + # We have a classic date representation + year = int(m.group("year")) - if not m.group("monthday"): - # No month and day - month = 1 - day = 1 - else: - if m.group("month") and m.group("day"): - # Month and day - if not m.group("daysep") and len(m.group("day")) == 1: - # Ordinal day - ordinal = int(m.group("month") + m.group("day")) - leap = is_leap(year) - months_offsets = MONTHS_OFFSETS[leap] - - if ordinal > months_offsets[13]: - raise ParserError("Ordinal day is out of range") - - for i in range(1, 14): - if ordinal <= months_offsets[i]: - day = ordinal - months_offsets[i - 1] - month = i - 1 - - break - else: - month = int(m.group("month")) - day = int(m.group("day")) + if not m.group("monthday"): + # No month and day + month = 1 + day = 1 + else: + if m.group("month") and m.group("day"): + # Month and day + if not m.group("daysep") and len(m.group("day")) == 1: + # Ordinal day + ordinal = int(m.group("month") + m.group("day")) + leap = is_leap(year) + months_offsets = MONTHS_OFFSETS[leap] + + if ordinal > months_offsets[13]: + raise ParserError("Ordinal day is out of range") + + for i in range(1, 14): + if ordinal <= months_offsets[i]: + day = ordinal - months_offsets[i - 1] + month = i - 1 + + break else: - # Only month - if not m.group("monthsep"): - # The date looks like 201207 - # which is invalid for a date - # But it might be a time in the form hhmmss - ambiguous_date = True - month = int(m.group("month")) - day = 1 - - if not m.group("time"): - # No time has been specified - if ambiguous_date: - # We can "safely" assume that the ambiguous date - # was actually a time in the form hhmmss - hhmmss = f"{str(year)}{str(month):0>2}" + day = int(m.group("day")) + else: + # Only month + if not m.group("monthsep"): + # The date looks like 201207 + # which is invalid for a date + # But it might be a time in the form hhmmss + ambiguous_date = True + + month = int(m.group("month")) + day = 1 - return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:])) + if not m.group("time"): + # No time has been specified + if ambiguous_date: + # We can "safely" assume that the ambiguous date + # was actually a time in the form hhmmss + hhmmss = f"{str(year)}{str(month):0>2}" - return datetime.date(year, month, day) + return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:])) - if ambiguous_date: - raise ParserError(f"Invalid date string: {text}") + return datetime.date(year, month, day) - if is_date and not m.group("timesep"): - raise ParserError(f"Invalid date string: {text}") + if ambiguous_date: + raise ParserError(f"Invalid date string: {text}") - if not is_date: - is_time = True + if is_date and not m.group("timesep"): + raise ParserError(f"Invalid date string: {text}") - # Grabbing hh:mm:ss - hour = int(m.group("hour")) - minsep = m.group("minsep") + if not is_date: + is_time = True - if m.group("minute"): - minute = int(m.group("minute")) - elif minsep: - raise ParserError("Invalid ISO 8601 time part") + # Grabbing hh:mm:ss + hour = int(m.group("hour")) + minsep = m.group("minsep") - secsep = m.group("secsep") - if secsep and not minsep and m.group("minute"): - # minute/second separator but no hour/minute separator - raise ParserError("Invalid ISO 8601 time part") + if m.group("minute"): + minute = int(m.group("minute")) + elif minsep: + raise ParserError("Invalid ISO 8601 time part") - if m.group("second"): - if not secsep and minsep: - # No minute/second separator but hour/minute separator - raise ParserError("Invalid ISO 8601 time part") + secsep = m.group("secsep") + if secsep and not minsep and m.group("minute"): + # minute/second separator but no hour/minute separator + raise ParserError("Invalid ISO 8601 time part") - second = int(m.group("second")) - elif secsep: + if m.group("second"): + if not secsep and minsep: + # No minute/second separator but hour/minute separator raise ParserError("Invalid ISO 8601 time part") - # Grabbing subseconds, if any - if m.group("subsecondsection"): - # Limiting to 6 chars - subsecond = m.group("subsecond")[:6] + second = int(m.group("second")) + elif secsep: + raise ParserError("Invalid ISO 8601 time part") + + # Grabbing subseconds, if any + if m.group("subsecondsection"): + # Limiting to 6 chars + subsecond = m.group("subsecond")[:6] - microsecond = int(f"{subsecond:0<6}") + microsecond = int(f"{subsecond:0<6}") - # Grabbing timezone, if any - tz = m.group("tz") - if tz: - if tz == "Z": - tzinfo = UTC + # Grabbing timezone, if any + tz = m.group("tz") + if tz: + if tz == "Z": + tzinfo = UTC + else: + negative = bool(tz.startswith("-")) + tz = tz[1:] + if ":" not in tz: + if len(tz) == 2: + tz = f"{tz}00" + + off_hour = tz[0:2] + off_minute = tz[2:4] else: - negative = bool(tz.startswith("-")) - tz = tz[1:] - if ":" not in tz: - if len(tz) == 2: - tz = f"{tz}00" - - off_hour = tz[0:2] - off_minute = tz[2:4] - else: - off_hour, off_minute = tz.split(":") + off_hour, off_minute = tz.split(":") - offset = ((int(off_hour) * 60) + int(off_minute)) * 60 + offset = ((int(off_hour) * 60) + int(off_minute)) * 60 - if negative: - offset = -1 * offset + if negative: + offset = -1 * offset - tzinfo = FixedTimezone(offset) + tzinfo = FixedTimezone(offset) - if is_time: - return datetime.time(hour, minute, second, microsecond) + if is_time: + return datetime.time(hour, minute, second, microsecond) - return datetime.datetime( - year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo - ) + return datetime.datetime( + year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo + ) -def _parse_iso8601_duration(text, **options): +def _parse_iso8601_duration(text: str, **options: str) -> Duration | None: m = ISO8601_DURATION.match(text) if not m: - return + return None years = 0 months = 0 weeks = 0 - days = 0 - hours = 0 - minutes = 0 - seconds = 0 - microseconds = 0 + days: int | float = 0 + hours: int | float = 0 + minutes: int | float = 0 + seconds: int | float = 0 + microseconds: int | float = 0 fractional = False + _days: str | float + _hour: str | int | None + _minutes: str | int | None + _seconds: str | int | None if m.group("w"): # Weeks if m.group("ymd") or m.group("hms"): @@ -289,7 +294,7 @@ def _parse_iso8601_duration(text, **options): _weeks, portion = _weeks.split(".") weeks = int(_weeks) _days = int(portion) / 10 * 7 - days, hours = int(_days // 1), _days % 1 * HOURS_PER_DAY + days, hours = int(_days // 1), int(_days % 1 * HOURS_PER_DAY) else: weeks = int(_weeks) @@ -359,7 +364,7 @@ def _parse_iso8601_duration(text, **options): if fractional: raise ParserError("Invalid duration") - _hours = _hours.replace(",", ".").replace("H", "") + _hours = cast(str, _hours).replace(",", ".").replace("H", "") if "." in _hours: fractional = True @@ -374,7 +379,7 @@ def _parse_iso8601_duration(text, **options): if fractional: raise ParserError("Invalid duration") - _minutes = _minutes.replace(",", ".").replace("M", "") + _minutes = cast(str, _minutes).replace(",", ".").replace("M", "") if "." in _minutes: fractional = True @@ -389,7 +394,7 @@ def _parse_iso8601_duration(text, **options): if fractional: raise ParserError("Invalid duration") - _seconds = _seconds.replace(",", ".").replace("S", "") + _seconds = cast(str, _seconds).replace(",", ".").replace("S", "") if "." in _seconds: _seconds, _microseconds = _seconds.split(".") @@ -410,7 +415,9 @@ def _parse_iso8601_duration(text, **options): ) -def _get_iso_8601_week(year, week, weekday): +def _get_iso_8601_week( + year: int | str, week: int | str, weekday: int | str +) -> dict[str, int]: if not weekday: weekday = 1 else: diff --git a/pendulum/period.py b/pendulum/period.py index 589dafdf..eb1b232a 100644 --- a/pendulum/period.py +++ b/pendulum/period.py @@ -5,6 +5,11 @@ from datetime import date from datetime import datetime from datetime import timedelta +from typing import TYPE_CHECKING +from typing import Iterator +from typing import Union +from typing import cast +from typing import overload import pendulum @@ -12,6 +17,12 @@ from pendulum.duration import Duration from pendulum.helpers import precise_diff +if TYPE_CHECKING: + from typing import SupportsIndex + + from pendulum.helpers import PreciseDiff + from pendulum.locales.locale import Locale # noqa + class Period(Duration): """ @@ -19,7 +30,38 @@ class Period(Duration): time difference. """ - def __new__(cls, start, end, absolute=False): + @overload + def __new__( + cls, + start: pendulum.DateTime | datetime, + end: pendulum.DateTime | datetime, + absolute: bool = False, + ) -> Period: + ... + + @overload + def __new__( + cls, + start: pendulum.Date | date, + end: pendulum.Date | date, + absolute: bool = False, + ) -> Period: + ... + + def __new__( + cls, + start: pendulum.DateTime | pendulum.Date | datetime | date, + end: pendulum.DateTime | pendulum.Date | datetime | date, + absolute: bool = False, + ) -> Period: + if ( + isinstance(start, datetime) + and not isinstance(end, datetime) + or not isinstance(start, datetime) + and isinstance(end, datetime) + ): + raise ValueError("Both start and end of a Period must have the same type") + if ( isinstance(start, datetime) and isinstance(end, datetime) @@ -75,18 +117,26 @@ def __new__(cls, start, end, absolute=False): and _start.tzinfo is _end.tzinfo ): if _start.tzinfo is not None: - _start = (_start - start.utcoffset()).replace(tzinfo=None) + offset = cast(timedelta, cast(datetime, start).utcoffset()) + _start = (_start - offset).replace(tzinfo=None) if isinstance(end, datetime) and _end.tzinfo is not None: - _end = (_end - end.utcoffset()).replace(tzinfo=None) + offset = cast(timedelta, end.utcoffset()) + _end = (_end - offset).replace(tzinfo=None) - delta = _end - _start + delta: timedelta = _end - _start # type: ignore[operator] - return super().__new__(cls, seconds=delta.total_seconds()) + return cast(Period, super().__new__(cls, seconds=delta.total_seconds())) - def __init__(self, start, end, absolute=False): + def __init__( + self, + start: pendulum.DateTime | pendulum.Date | datetime | date, + end: pendulum.DateTime | pendulum.Date | datetime | date, + absolute: bool = False, + ) -> None: super().__init__() + _start: pendulum.DateTime | pendulum.Date | datetime | date if not isinstance(start, pendulum.Date): if isinstance(start, datetime): start = pendulum.instance(start) @@ -109,6 +159,7 @@ def __init__(self, start, end, absolute=False): else: _start = date(start.year, start.month, start.day) + _end: pendulum.DateTime | pendulum.Date | datetime | date if not isinstance(end, pendulum.Date): if isinstance(end, datetime): end = pendulum.instance(end) @@ -140,63 +191,59 @@ def __init__(self, start, end, absolute=False): _end, _start = _start, _end self._absolute = absolute - self._start = start - self._end = end - self._delta = precise_diff(_start, _end) + self._start: pendulum.DateTime | pendulum.Date = start + self._end: pendulum.DateTime | pendulum.Date = end + self._delta: PreciseDiff = precise_diff(_start, _end) @property - def years(self): + def years(self) -> int: return self._delta.years @property - def months(self): + def months(self) -> int: return self._delta.months @property - def weeks(self): + def weeks(self) -> int: return abs(self._delta.days) // 7 * self._sign(self._delta.days) @property - def days(self): + def days(self) -> int: return self._days @property - def remaining_days(self): + def remaining_days(self) -> int: return abs(self._delta.days) % 7 * self._sign(self._days) @property - def hours(self): + def hours(self) -> int: return self._delta.hours @property - def minutes(self): + def minutes(self) -> int: return self._delta.minutes @property - def start(self): + def start(self) -> pendulum.DateTime | pendulum.Date | datetime | date: return self._start @property - def end(self): + def end(self) -> pendulum.DateTime | pendulum.Date | datetime | date: return self._end - def in_years(self): + def in_years(self) -> int: """ Gives the duration of the Period in full years. - - :rtype: int """ return self.years - def in_months(self): + def in_months(self) -> int: """ Gives the duration of the Period in full months. - - :rtype: int """ return self.years * MONTHS_PER_YEAR + self.months - def in_weeks(self): + def in_weeks(self) -> int: days = self.in_days() sign = 1 @@ -205,23 +252,20 @@ def in_weeks(self): return sign * (abs(days) // 7) - def in_days(self): + def in_days(self) -> int: return self._delta.total_days - def in_words(self, locale=None, separator=" "): + def in_words(self, locale: str | None = None, separator: str = " ") -> str: """ Get the current interval in words in the current locale. Ex: 6 jours 23 heures 58 minutes :param locale: The locale to use. Defaults to current locale. - :type locale: str - :param separator: The separator to use between each unit - :type separator: str - - :rtype: str """ + from pendulum.locales.locale import Locale # noqa + periods = [ ("year", self.years), ("month", self.months), @@ -231,33 +275,32 @@ def in_words(self, locale=None, separator=" "): ("minute", self.minutes), ("second", self.remaining_seconds), ] - - if locale is None: - locale = pendulum.get_locale() - - locale = pendulum.locale(locale) + loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) parts = [] for period in periods: - unit, count = period - if abs(count) > 0: - translation = locale.translation( - f"units.{unit}.{locale.plural(abs(count))}" + unit, period_count = period + if abs(period_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(period_count))}" ) - parts.append(translation.format(count)) + parts.append(translation.format(period_count)) if not parts: + count: str | int = 0 if abs(self.microseconds) > 0: - unit = f"units.second.{locale.plural(1)}" + unit = f"units.second.{loaded_locale.plural(1)}" count = f"{abs(self.microseconds) / 1e6:.2f}" else: - unit = f"units.microsecond.{locale.plural(0)}" - count = 0 - translation = locale.translation(unit) + unit = f"units.microsecond.{loaded_locale.plural(0)}" + + translation = loaded_locale.translation(unit) parts.append(translation.format(count)) return separator.join(parts) - def range(self, unit, amount=1): + def range( + self, unit: str, amount: int = 1 + ) -> Iterator[pendulum.DateTime | pendulum.Date]: method = "add" op = operator.le if not self._absolute and self.invert: @@ -268,66 +311,82 @@ def range(self, unit, amount=1): i = amount while op(start, end): - yield start + yield cast(Union[pendulum.DateTime, pendulum.Date], start) start = getattr(self.start, method)(**{unit: i}) i += amount - def as_interval(self): + def as_interval(self) -> Duration: """ - Return the Period as an Duration. - - :rtype: Duration + Return the Period as a Duration. """ return Duration(seconds=self.total_seconds()) - def __iter__(self): + def __iter__(self) -> Iterator[pendulum.DateTime | pendulum.Date]: return self.range("days") - def __contains__(self, item): + def __contains__( + self, item: datetime | date | pendulum.DateTime | pendulum.Date + ) -> bool: return self.start <= item <= self.end - def __add__(self, other): + def __add__(self, other: timedelta) -> Duration: return self.as_interval().__add__(other) __radd__ = __add__ - def __sub__(self, other): + def __sub__(self, other: timedelta) -> Duration: return self.as_interval().__sub__(other) - def __neg__(self): + def __neg__(self) -> Period: return self.__class__(self.end, self.start, self._absolute) - def __mul__(self, other): + def __mul__(self, other: int | float) -> Duration: return self.as_interval().__mul__(other) __rmul__ = __mul__ - def __floordiv__(self, other): + @overload + def __floordiv__(self, other: timedelta) -> int: + ... + + @overload + def __floordiv__(self, other: int) -> Duration: + ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: return self.as_interval().__floordiv__(other) - def __truediv__(self, other): - return self.as_interval().__truediv__(other) + __div__ = __floordiv__ # type: ignore[assignment] + + @overload + def __truediv__(self, other: timedelta) -> float: + ... - __div__ = __floordiv__ + @overload + def __truediv__(self, other: float) -> Duration: + ... - def __mod__(self, other): + def __truediv__(self, other: float | timedelta) -> Duration | float: + return self.as_interval().__truediv__(other) + + def __mod__(self, other: timedelta) -> Duration: return self.as_interval().__mod__(other) - def __divmod__(self, other): + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: return self.as_interval().__divmod__(other) - def __abs__(self): - return self.__class__(self.start, self.end, True) + def __abs__(self) -> Period: + return self.__class__(self.start, self.end, absolute=True) - def __repr__(self): + def __repr__(self) -> str: return f" {self._end}]>" - def __str__(self): + def __str__(self) -> str: return self.__repr__() - def _cmp(self, other): + def _cmp(self, other: timedelta) -> int: # Only needed for PyPy assert isinstance(other, timedelta) @@ -338,24 +397,48 @@ def _cmp(self, other): return 0 if td == other else 1 if td > other else -1 - def _getstate(self, protocol=3): + def _getstate( + self, protocol: SupportsIndex = 3 + ) -> tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ]: start, end = self.start, self.end if self._invert and self._absolute: end, start = start, end - return (start, end, self._absolute) - - def __reduce__(self): + return start, end, self._absolute + + def __reduce__( + self, + ) -> tuple[ + type[Period], + tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ], + ]: return self.__reduce_ex__(2) - def __reduce_ex__(self, protocol): + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[ + type[Period], + tuple[ + pendulum.DateTime | pendulum.Date | datetime | date, + pendulum.DateTime | pendulum.Date | datetime | date, + bool, + ], + ]: return self.__class__, self._getstate(protocol) - def __hash__(self): + def __hash__(self) -> int: return hash((self.start, self.end, self._absolute)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, Period): return (self.start, self.end, self._absolute) == ( other.start, diff --git a/pendulum/time.py b/pendulum/time.py index 779f70a1..f979e254 100644 --- a/pendulum/time.py +++ b/pendulum/time.py @@ -1,7 +1,13 @@ from __future__ import annotations +import datetime + from datetime import time from datetime import timedelta +from typing import TYPE_CHECKING +from typing import Optional +from typing import cast +from typing import overload import pendulum @@ -12,6 +18,9 @@ from pendulum.duration import Duration from pendulum.mixins.default import FormattableMixin +if TYPE_CHECKING: + from typing import Literal + class Time(FormattableMixin, time): """ @@ -19,7 +28,7 @@ class Time(FormattableMixin, time): """ # String formatting - def __repr__(self): + def __repr__(self) -> str: us = "" if self.microsecond: us = f", {self.microsecond}" @@ -35,14 +44,9 @@ def __repr__(self): # Comparisons - def closest(self, dt1, dt2): + def closest(self, dt1: Time | time, dt2: Time | time) -> Time: """ Get the closest time from the instance. - - :type dt1: Time or time - :type dt2: Time or time - - :rtype: Time """ dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) @@ -52,14 +56,9 @@ def closest(self, dt1, dt2): return dt2 - def farthest(self, dt1, dt2): + def farthest(self, dt1: Time | time, dt2: Time | time) -> Time: """ Get the farthest time from the instance. - - :type dt1: Time or time - :type dt2: Time or time - - :rtype: Time """ dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond) dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond) @@ -71,23 +70,16 @@ def farthest(self, dt1, dt2): # ADDITIONS AND SUBSTRACTIONS - def add(self, hours=0, minutes=0, seconds=0, microseconds=0): + def add( + self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0 + ) -> Time: """ Add duration to the instance. :param hours: The number of hours - :type hours: int - :param minutes: The number of minutes - :type minutes: int - :param seconds: The number of seconds - :type seconds: int - :param microseconds: The number of microseconds - :type microseconds: int - - :rtype: Time """ from pendulum.datetime import DateTime @@ -99,7 +91,9 @@ def add(self, hours=0, minutes=0, seconds=0, microseconds=0): .time() ) - def subtract(self, hours=0, minutes=0, seconds=0, microseconds=0): + def subtract( + self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0 + ) -> Time: """ Add duration to the instance. @@ -127,41 +121,43 @@ def subtract(self, hours=0, minutes=0, seconds=0, microseconds=0): .time() ) - def add_timedelta(self, delta): + def add_timedelta(self, delta: datetime.timedelta) -> Time: """ Add timedelta duration to the instance. :param delta: The timedelta instance - :type delta: datetime.timedelta - - :rtype: Time """ if delta.days: raise TypeError("Cannot add timedelta with days to Time.") return self.add(seconds=delta.seconds, microseconds=delta.microseconds) - def subtract_timedelta(self, delta): + def subtract_timedelta(self, delta: datetime.timedelta) -> Time: """ Remove timedelta duration from the instance. :param delta: The timedelta instance - :type delta: datetime.timedelta - - :rtype: Time """ if delta.days: raise TypeError("Cannot subtract timedelta with days to Time.") return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds) - def __add__(self, other): + def __add__(self, other: datetime.timedelta) -> Time: if not isinstance(other, timedelta): return NotImplemented return self.add_timedelta(other) - def __sub__(self, other): + @overload + def __sub__(self, other: time) -> pendulum.Duration: + ... + + @overload + def __sub__(self, other: datetime.timedelta) -> Time: + ... + + def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time: if not isinstance(other, (Time, time, timedelta)): return NotImplemented @@ -178,7 +174,15 @@ def __sub__(self, other): return other.diff(self, False) - def __rsub__(self, other): + @overload + def __rsub__(self, other: time) -> pendulum.Duration: + ... + + @overload + def __rsub__(self, other: datetime.timedelta) -> Time: + ... + + def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time: if not isinstance(other, (Time, time)): return NotImplemented @@ -194,16 +198,12 @@ def __rsub__(self, other): # DIFFERENCES - def diff(self, dt=None, abs=True): + def diff(self, dt: time | None = None, abs: bool = True) -> Duration: """ Returns the difference between two Time objects as an Duration. - :type dt: Time or None - - :param abs: Whether to return an absolute interval or not - :type abs: bool - - :rtype: Duration + :param dt: The time to subtract from + :param abs: Whether to return an absolute duration or not """ if dt is None: dt = pendulum.now().time() @@ -224,19 +224,18 @@ def diff(self, dt=None, abs=True): return klass(microseconds=us2 - us1) - def diff_for_humans(self, other=None, absolute=False, locale=None): + def diff_for_humans( + self, + other: time | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: """ Get the difference in a human readable format in the current locale. - :type other: Time or time - + :param dt: The time to subtract from :param absolute: removes time difference modifiers ago, after, etc - :type absolute: bool - :param locale: The locale to use for localization - :type locale: str - - :rtype: str """ is_now = other is None @@ -250,8 +249,14 @@ def diff_for_humans(self, other=None, absolute=False, locale=None): # Compatibility methods def replace( - self, hour=None, minute=None, second=None, microsecond=None, tzinfo=True - ): + self, + hour: int | None = None, + minute: int | None = None, + second: int | None = None, + microsecond: int | None = None, + tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, + fold: int = 0, + ) -> Time: if tzinfo is True: tzinfo = self.tzinfo @@ -260,23 +265,36 @@ def replace( second = second if second is not None else self.second microsecond = microsecond if microsecond is not None else self.microsecond - t = super().replace(hour, minute, second, microsecond, tzinfo=tzinfo) + t = super().replace( + hour, + minute, + second, + microsecond, + tzinfo=cast(Optional[datetime.tzinfo], tzinfo), + fold=fold, + ) return self.__class__( t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo ) - def __getnewargs__(self): + def __getnewargs__(self) -> tuple[Time]: return (self,) - def _get_state(self, protocol=3): + def _get_state( + self, protocol: int = 3 + ) -> tuple[int, int, int, int, datetime.tzinfo | None]: tz = self.tzinfo - return (self.hour, self.minute, self.second, self.microsecond, tz) + return self.hour, self.minute, self.second, self.microsecond, tz - def __reduce__(self): + def __reduce__( + self, + ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]: return self.__reduce_ex__(2) - def __reduce_ex__(self, protocol): + def __reduce_ex__( # type: ignore[override] + self, protocol: int + ) -> 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 fd0a3b72..45c9855d 100644 --- a/pendulum/tz/__init__.py +++ b/pendulum/tz/__init__.py @@ -2,10 +2,6 @@ import sys -from typing import Union - -import tzdata - from pendulum.tz.local_timezone import get_local_timezone from pendulum.tz.local_timezone import set_local_timezone from pendulum.tz.local_timezone import test_local_timezone @@ -13,28 +9,25 @@ 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 - PRE_TRANSITION = "pre" POST_TRANSITION = "post" TRANSITION_ERROR = "error" _timezones = None +_tz_cache: dict[int, FixedTimezone] = {} -_tz_cache = {} - -def timezones(): +def timezones() -> tuple[str, ...]: global _timezones if _timezones is None: - with open(resources.files(tzdata).joinpath("zones")) as f: + with resources.files("tzdata").joinpath("zones").open() as f: _timezones = tuple(tz.strip() for tz in f.readlines()) return _timezones @@ -66,8 +59,22 @@ def fixed_timezone(offset: int) -> FixedTimezone: return tz -def local_timezone() -> Timezone: +def local_timezone() -> Timezone | FixedTimezone: """ Return the local timezone. """ return get_local_timezone() + + +__all__ = [ + "UTC", + "Timezone", + "FixedTimezone", + "set_local_timezone", + "get_local_timezone", + "test_local_timezone", + "timezone", + "fixed_timezone", + "local_timezone", + "timezones", +] diff --git a/pendulum/tz/data/windows.py b/pendulum/tz/data/windows.py index e76e6bbf..65aa6c3d 100644 --- a/pendulum/tz/data/windows.py +++ b/pendulum/tz/data/windows.py @@ -1,6 +1,5 @@ from __future__ import annotations - windows_timezones = { "AUS Central Standard Time": "Australia/Darwin", "AUS Eastern Standard Time": "Australia/Sydney", diff --git a/pendulum/tz/exceptions.py b/pendulum/tz/exceptions.py index 5d5a393e..b8833aca 100644 --- a/pendulum/tz/exceptions.py +++ b/pendulum/tz/exceptions.py @@ -1,31 +1,32 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from datetime import datetime -class TimezoneError(ValueError): +class TimezoneError(ValueError): pass class InvalidTimezone(TimezoneError): - pass class NonExistingTime(TimezoneError): - message = "The datetime {} does not exist." - def __init__(self, dt): + def __init__(self, dt: datetime) -> None: message = self.message.format(dt) super().__init__(message) class AmbiguousTime(TimezoneError): - message = "The datetime {} is ambiguous." - def __init__(self, dt): + def __init__(self, dt: datetime) -> None: message = self.message.format(dt) super().__init__(message) diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py index 6ddb9bff..41cf81bf 100644 --- a/pendulum/tz/local_timezone.py +++ b/pendulum/tz/local_timezone.py @@ -7,20 +7,17 @@ from contextlib import contextmanager from typing import Iterator +from typing import cast from pendulum.tz.exceptions import InvalidTimezone from pendulum.tz.timezone import FixedTimezone from pendulum.tz.timezone import Timezone - -try: - import _winreg as winreg -except ImportError: +if sys.platform == "win32": try: + import _winreg as winreg + except (ImportError, AttributeError): import winreg - except ImportError: - winreg = None - _mock_local_timezone = None _local_timezone = None @@ -64,81 +61,88 @@ def _get_system_timezone() -> Timezone: return _get_unix_timezone() -def _get_windows_timezone() -> Timezone: - from pendulum.tz.data.windows import windows_timezones +if sys.platform == "win32": + + def _get_windows_timezone() -> Timezone: + from pendulum.tz.data.windows import windows_timezones + + # Windows is special. It has unique time zone names (in several + # meanings of the word) available, but unfortunately, they can be + # translated to the language of the operating system, so we need to + # do a backwards lookup, by going through all time zones and see which + # one matches. + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + + tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + localtz = winreg.OpenKey(handle, tz_local_key_name) - # Windows is special. It has unique time zone names (in several - # meanings of the word) available, but unfortunately, they can be - # translated to the language of the operating system, so we need to - # do a backwards lookup, by going through all time zones and see which - # one matches. - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + timezone_info = {} + size = winreg.QueryInfoKey(localtz)[1] + for i in range(size): + data = winreg.EnumValue(localtz, i) + timezone_info[data[0]] = data[1] - tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" - localtz = winreg.OpenKey(handle, tz_local_key_name) + localtz.Close() - timezone_info = {} - size = winreg.QueryInfoKey(localtz)[1] - for i in range(size): - data = winreg.EnumValue(localtz, i) - timezone_info[data[0]] = data[1] + if "TimeZoneKeyName" in timezone_info: + # Windows 7 (and Vista?) - localtz.Close() + # For some reason this returns a string with loads of NUL bytes at + # least on some systems. I don't know if this is a bug somewhere, I + # just work around it. + tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0] + else: + # Windows 2000 or XP - if "TimeZoneKeyName" in timezone_info: - # Windows 7 (and Vista?) + # This is the localized name: + tzwin = timezone_info["StandardName"] - # For some reason this returns a string with loads of NUL bytes at - # least on some systems. I don't know if this is a bug somewhere, I - # just work around it. - tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0] - else: - # Windows 2000 or XP + # Open the list of timezones to look up the real name: + tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" + tzkey = winreg.OpenKey(handle, tz_key_name) - # This is the localized name: - tzwin = timezone_info["StandardName"] + # Now, match this value to Time Zone information + tzkeyname = None + for i in range(winreg.QueryInfoKey(tzkey)[0]): + subkey = winreg.EnumKey(tzkey, i) + sub = winreg.OpenKey(tzkey, subkey) - # Open the list of timezones to look up the real name: - tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" - tzkey = winreg.OpenKey(handle, tz_key_name) + info = {} + size = winreg.QueryInfoKey(sub)[1] + for i in range(size): + data = winreg.EnumValue(sub, i) + info[data[0]] = data[1] - # Now, match this value to Time Zone information - tzkeyname = None - for i in range(winreg.QueryInfoKey(tzkey)[0]): - subkey = winreg.EnumKey(tzkey, i) - sub = winreg.OpenKey(tzkey, subkey) + sub.Close() + with contextlib.suppress(KeyError): + # This timezone didn't have proper configuration. + # Ignore it. + if info["Std"] == tzwin: + tzkeyname = subkey + break - info = {} - size = winreg.QueryInfoKey(sub)[1] - for i in range(size): - data = winreg.EnumValue(sub, i) - info[data[0]] = data[1] + tzkey.Close() + handle.Close() - sub.Close() - with contextlib.suppress(KeyError): - # This timezone didn't have proper configuration. - # Ignore it. - if info["Std"] == tzwin: - tzkeyname = subkey - break + if tzkeyname is None: + raise LookupError("Can not find Windows timezone configuration") - tzkey.Close() - handle.Close() + timezone = windows_timezones.get(tzkeyname) + if timezone is None: + # Nope, that didn't work. Try adding "Standard Time", + # it seems to work a lot of times: + timezone = windows_timezones.get(tzkeyname + " Standard Time") - if tzkeyname is None: - raise LookupError("Can not find Windows timezone configuration") + # Return what we have. + if timezone is None: + raise LookupError("Unable to find timezone " + tzkeyname) - timezone = windows_timezones.get(tzkeyname) - if timezone is None: - # Nope, that didn't work. Try adding "Standard Time", - # it seems to work a lot of times: - timezone = windows_timezones.get(tzkeyname + " Standard Time") + return Timezone(timezone) - # Return what we have. - if timezone is None: - raise LookupError("Unable to find timezone " + tzkeyname) +else: - return Timezone(timezone) + def _get_windows_timezone() -> Timezone: + ... def _get_darwin_timezone() -> Timezone: @@ -160,12 +164,12 @@ def _get_unix_timezone(_root: str = "/") -> Timezone: tzpath = os.path.join(_root, "etc/timezone") if os.path.isfile(tzpath): with open(tzpath, "rb") as tzfile: - data = tzfile.read() + tzfile_data = tzfile.read() # Issue #3 was that /etc/timezone was a zoneinfo file. # That's a misconfiguration, but we need to handle it gracefully: - if data[:5] != "TZif2": - etctz = data.strip().decode() + if tzfile_data[:5] != b"TZif2": + etctz = tzfile_data.strip().decode() # Get rid of host definitions and comments: if " " in etctz: etctz, dummy = etctz.split(" ", 1) @@ -200,15 +204,19 @@ def _get_unix_timezone(_root: str = "/") -> Timezone: if match is not None: # Some setting existed line = line[match.end() :] - etctz = line[: end_re.search(line).start()] + etctz = line[ + : cast( + re.Match, end_re.search(line) # type: ignore[type-arg] + ).start() + ] parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep))) - tzpath = [] + tzpath_parts: list[str] = [] while parts: - tzpath.insert(0, parts.pop(0)) + tzpath_parts.insert(0, parts.pop(0)) with contextlib.suppress(InvalidTimezone): - return Timezone(os.path.join(*tzpath)) + return Timezone(os.path.join(*tzpath_parts)) # systemd distributions use symlinks that include the zone name, # see manpage of localtime(5) and timedatectl(1) @@ -217,11 +225,11 @@ def _get_unix_timezone(_root: str = "/") -> Timezone: parts = list( reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep)) ) - tzpath = [] + tzpath_parts: list[str] = [] # type: ignore[no-redef] while parts: - tzpath.insert(0, parts.pop(0)) + tzpath_parts.insert(0, parts.pop(0)) with contextlib.suppress(InvalidTimezone): - return Timezone(os.path.join(*tzpath)) + return Timezone(os.path.join(*tzpath_parts)) # No explicit setting existed. Use localtime for filename in ("etc/localtime", "usr/local/etc/localtime"): @@ -231,7 +239,7 @@ def _get_unix_timezone(_root: str = "/") -> Timezone: continue with open(tzpath, "rb") as f: - return Timezone.from_file(f) + return cast(Timezone, Timezone.from_file(f)) raise RuntimeError("Unable to find any timezone configuration") @@ -243,7 +251,7 @@ def _tz_from_env(tzenv: str) -> Timezone: # TZ specifies a file if os.path.isfile(tzenv): with open(tzenv, "rb") as f: - return Timezone.from_file(f) + return cast(Timezone, Timezone.from_file(f)) # TZ specifies a zoneinfo zone. try: diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py index 1ddf0741..f689004e 100644 --- a/pendulum/tz/timezone.py +++ b/pendulum/tz/timezone.py @@ -1,25 +1,20 @@ from __future__ import annotations +import datetime as datetime_ + from abc import ABC from abc import abstractmethod -from datetime import datetime -from datetime import timedelta -from datetime import tzinfo -from typing import TypeVar +from typing import cast from pendulum.tz.exceptions import AmbiguousTime from pendulum.tz.exceptions import InvalidTimezone from pendulum.tz.exceptions import NonExistingTime from pendulum.utils._compat import zoneinfo - POST_TRANSITION = "post" PRE_TRANSITION = "pre" TRANSITION_ERROR = "error" -_datetime = datetime -_D = TypeVar("_D", bound=datetime) - class PendulumTimezone(ABC): @property @@ -28,7 +23,9 @@ def name(self) -> str: raise NotImplementedError @abstractmethod - def convert(self, dt: datetime, dst_rule: str | None = None) -> datetime: + def convert( + self, dt: datetime_.datetime, raise_on_unknown_times: bool = False + ) -> datetime_.datetime: raise NotImplementedError @abstractmethod @@ -41,11 +38,11 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - ) -> datetime: + ) -> datetime_.datetime: raise NotImplementedError -class Timezone(zoneinfo.ZoneInfo, PendulumTimezone): +class Timezone(zoneinfo.ZoneInfo, PendulumTimezone): # type: ignore[misc] """ Represents a named timezone. @@ -57,15 +54,17 @@ class Timezone(zoneinfo.ZoneInfo, PendulumTimezone): def __new__(cls, key: str) -> Timezone: try: - return super().__new__(cls, key) + return cast(Timezone, super().__new__(cls, key)) except zoneinfo.ZoneInfoNotFoundError: raise InvalidTimezone(key) @property def name(self) -> str: - return self.key + return cast(str, self.key) - def convert(self, dt: datetime, raise_on_unknown_times: bool = False) -> datetime: + def convert( + self, dt: datetime_.datetime, raise_on_unknown_times: bool = False + ) -> datetime_.datetime: """ Converts a datetime in the current timezone. @@ -121,19 +120,21 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - ) -> _datetime: + ) -> datetime_.datetime: """ Return a normalized datetime for the current timezone. """ return self.convert( - datetime(year, month, day, hour, minute, second, microsecond, fold=1) + datetime_.datetime( + year, month, day, hour, minute, second, microsecond, fold=1 + ) ) def __repr__(self) -> str: return f"{self.__class__.__name__}('{self.name}')" -class FixedTimezone(tzinfo, PendulumTimezone): +class FixedTimezone(datetime_.tzinfo, PendulumTimezone): def __init__(self, offset: int, name: str | None = None) -> None: sign = "-" if offset < 0 else "+" @@ -145,13 +146,15 @@ def __init__(self, offset: int, name: str | None = None) -> None: self._name = name self._offset = offset - self._utcoffset = timedelta(seconds=offset) + self._utcoffset = datetime_.timedelta(seconds=offset) @property def name(self) -> str: return self._name - def convert(self, dt: datetime, raise_on_unknown_times: bool = False) -> datetime: + def convert( + self, dt: datetime_.datetime, raise_on_unknown_times: bool = False + ) -> datetime_.datetime: if dt.tzinfo is None: return dt.__class__( dt.year, @@ -176,29 +179,31 @@ def datetime( minute: int = 0, second: int = 0, microsecond: int = 0, - ) -> datetime: + ) -> datetime_.datetime: return self.convert( - datetime(year, month, day, hour, minute, second, microsecond, fold=1) + datetime_.datetime( + year, month, day, hour, minute, second, microsecond, fold=1 + ) ) @property def offset(self) -> int: return self._offset - def utcoffset(self, dt: datetime | None) -> timedelta: + def utcoffset(self, dt: datetime_.datetime | None) -> datetime_.timedelta: return self._utcoffset - def dst(self, dt: _datetime | None): - return timedelta() + def dst(self, dt: datetime_.datetime | None) -> datetime_.timedelta: + return datetime_.timedelta() - def fromutc(self, dt: datetime) -> datetime: + def fromutc(self, dt: datetime_.datetime) -> datetime_.datetime: # Use the stdlib datetime's add method to avoid infinite recursion - return (datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) + return (datetime_.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) - def tzname(self, dt: datetime | None) -> str | None: + def tzname(self, dt: datetime_.datetime | None) -> str | None: return self._name - def __getinitargs__(self) -> tuple: + def __getinitargs__(self) -> tuple[int, str]: return self._offset, self._name def __repr__(self) -> str: diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py index 60ad52cb..8f32f9e7 100644 --- a/pendulum/utils/_compat.py +++ b/pendulum/utils/_compat.py @@ -2,15 +2,12 @@ import sys - PYPY = hasattr(sys, "pypy_version_info") PY38 = sys.version_info[:2] >= (3, 8) - try: from backports import zoneinfo except ImportError: - import zoneinfo - + import zoneinfo # type: ignore[no-redef] __all__ = ["zoneinfo"] diff --git a/poetry.lock b/poetry.lock index 6ad69183..cddeae37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,10 +15,10 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope-interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope-interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope-interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope-interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope-interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope-interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "babel" @@ -159,7 +159,7 @@ python-versions = "*" python-dateutil = ">=2.8.1" [package.extras] -dev = ["wheel", "flake8", "markdown", "twine"] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "identify" @@ -185,9 +185,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl-flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl-flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "importlib-resources" @@ -201,8 +201,8 @@ python-versions = ">=3.7" zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "iniconfig" @@ -221,10 +221,10 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "jinja2" @@ -351,8 +351,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -430,7 +430,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -467,7 +467,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "python-dateutil" @@ -516,9 +516,9 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)", "sphinx-notfound-page (==0.8.3)", "sphinx-hoverxref (<2)", "pygments-github-lexers (==0.0.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-reredirects", "sphinxcontrib-towncrier", "furo"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-enabler (>=1.3)", "pytest-perf", "mock", "flake8-2020", "virtualenv (>=13.0.0)", "wheel", "pip (>=19.1)", "jaraco.envs (>=2.2)", "pytest-xdist", "jaraco.path (>=3.2.0)", "build", "filelock (>=3.4.0)", "pip-run (>=8.8)", "ini2toml[lite] (>=0.9)", "tomli-w (>=1.0.0)", "pytest-black (>=0.3.7)", "pytest-cov", "pytest-mypy (>=0.9.1)"] -testing-integration = ["pytest", "pytest-xdist", "pytest-enabler", "virtualenv (>=13.0.0)", "tomli", "wheel", "jaraco.path (>=3.2.0)", "jaraco.envs (>=2.2)", "build", "filelock (>=3.4.0)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -576,7 +576,7 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "typed-ast" @@ -586,6 +586,22 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "types-backports" +version = "0.1.3" +description = "Typing stubs for backports" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-python-dateutil" +version = "2.8.19" +description = "Typing stubs for python-dateutil" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "4.3.0" @@ -640,16 +656,18 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco-itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "8b2dbefd9d864bdc0e95e198e80296a9a314055cf0afaf3afb26e08261f7720a" +content-hash = "f2bb781400f4422396c7b1e6f5698bd1c91097f0cdc6f9fa56f334fd550de5f7" [metadata.files] -atomicwrites = [] +atomicwrites = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, @@ -717,7 +735,49 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] -coverage = [] +coverage = [ + {file = "coverage-6.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e"}, + {file = "coverage-6.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc"}, + {file = "coverage-6.4.2-cp310-cp310-win32.whl", hash = "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386"}, + {file = "coverage-6.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0"}, + {file = "coverage-6.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083"}, + {file = "coverage-6.4.2-cp37-cp37m-win32.whl", hash = "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7"}, + {file = "coverage-6.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120"}, + {file = "coverage-6.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"}, + {file = "coverage-6.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de"}, + {file = "coverage-6.4.2-cp38-cp38-win32.whl", hash = "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783"}, + {file = "coverage-6.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6"}, + {file = "coverage-6.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f"}, + {file = "coverage-6.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c"}, + {file = "coverage-6.4.2-cp39-cp39-win32.whl", hash = "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd"}, + {file = "coverage-6.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf"}, + {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"}, + {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"}, +] crashtest = [ {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"}, {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"}, @@ -738,7 +798,10 @@ identify = [ {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, ] -importlib-metadata = [] +importlib-metadata = [ + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, +] importlib-resources = [ {file = "importlib_resources-5.9.0-py3-none-any.whl", hash = "sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7"}, {file = "importlib_resources-5.9.0.tar.gz", hash = "sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681"}, @@ -816,7 +879,10 @@ mypy-extensions = [ {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"}, ] -nodeenv = [] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -833,7 +899,10 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [] +pre-commit = [ + {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, + {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -967,7 +1036,10 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tox = [] +tox = [ + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, +] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, @@ -994,6 +1066,14 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] +types-backports = [ + {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"}, +] +types-python-dateutil = [ + {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"}, +] typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, @@ -1033,4 +1113,7 @@ watchdog = [ {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"}, {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"}, ] -zipp = [] +zipp = [ + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, +] diff --git a/pyproject.toml b/pyproject.toml index ac8ea9ae..f5bd9005 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,25 +11,25 @@ documentation = "https://pendulum.eustace.io/docs" keywords = ['datetime', 'date', 'time'] packages = [ - {include = "pendulum"}, + { include = "pendulum" }, #{include = "tests", format = "sdist"}, ] include = [ - {path = "pendulum/py.typed"}, + { path = "pendulum/py.typed" }, # C extensions must be included in the wheel distributions - {path = "pendulum/_extensions/*.so", format = "wheel"}, - {path = "pendulum/_extensions/*.pyd", format = "wheel"}, - {path = "pendulum/parsing/*.so", format = "wheel"}, - {path = "pendulum/parsing/*.pyd", format = "wheel"}, + { path = "pendulum/_extensions/*.so", format = "wheel" }, + { path = "pendulum/_extensions/*.pyd", format = "wheel" }, + { path = "pendulum/parsing/*.so", format = "wheel" }, + { path = "pendulum/parsing/*.pyd", format = "wheel" }, ] [tool.poetry.dependencies] python = "^3.7" python-dateutil = "^2.6" -"backports.zoneinfo" = {version = "^0.2.1", python = ">=3.7,<3.9"} +"backports.zoneinfo" = { version = "^0.2.1", python = ">=3.7,<3.9" } tzdata = ">=2020.1" -importlib-resources = {version = "^5.9.0", python = ">=3.7,<3.9"} +importlib-resources = { version = "^5.9.0", python = ">=3.7,<3.9" } [tool.poetry.group.test.dependencies] pytest = "^7.1.2" @@ -44,9 +44,11 @@ pygments = "^2.2" markdown-include = "^0.5.1" [tool.poetry.group.lint.dependencies] -black = {version = "^22.6.0", markers = "implementation_name != 'pypy'"} +black = { version = "^22.6.0", markers = "implementation_name != 'pypy'" } isort = "^5.10.1" pre-commit = "^2.20.0" +types-backports = "^0.1.3" +types-python-dateutil = "^2.8.19" [tool.poetry.group.dev.dependencies] babel = "^2.10.3" @@ -61,7 +63,7 @@ script = "build.py" profile = "black" force_single_line = true atomic = true -lines_after_imports = 2 +lines_after_imports = -1 lines_between_types = 1 skip_glob = [ "pendulum/locales/**", @@ -91,26 +93,7 @@ pretty = true [[tool.mypy.overrides]] module = [ - "pendulum", - "pendulum._extensions.helpers", - "pendulum.date", - "pendulum.datetime", - "pendulum.duration", - "pendulum.formatting.formatter", - "pendulum.formatting.difference_formatter", - "pendulum.helpers", - "pendulum.locales.locale", "pendulum.mixins.default", - "pendulum.parser", - "pendulum.parsing", - "pendulum.parsing.iso8601", - "pendulum.period", - "pendulum.time", - "pendulum.tz", - "pendulum.tz.exceptions", - "pendulum.tz.local_timezone", - "pendulum.tz.timezone", - "pendulum.utils._compat", "tests.conftest", "tests.test_helpers", "tests.test_main", diff --git a/tests/date/test_diff.py b/tests/date/test_diff.py index 6373a9f6..59a87ec3 100644 --- a/tests/date/test_diff.py +++ b/tests/date/test_diff.py @@ -129,13 +129,13 @@ def test_diff_for_humans_now_and_nearly_month(today): def test_diff_for_humans_now_and_month(): - with pendulum.test(pendulum.datetime(2016, 3, 1)): + with pendulum.test(pendulum.datetime(2016, 4, 1)): today = pendulum.today().date() assert today.subtract(weeks=4).diff_for_humans() == "4 weeks ago" assert today.subtract(months=1).diff_for_humans() == "1 month ago" - with pendulum.test(pendulum.datetime(2017, 2, 28)): + with pendulum.test(pendulum.datetime(2017, 3, 1)): today = pendulum.today().date() assert today.subtract(weeks=4).diff_for_humans() == "1 month ago" @@ -183,23 +183,23 @@ def test_diff_for_humans_now_and_nearly_future_month(today): def test_diff_for_humans_now_and_future_month(): with pendulum.test(pendulum.datetime(2016, 3, 1)): - today = pendulum.today().date() + today = pendulum.today("UTC").date() assert today.add(weeks=4).diff_for_humans() == "in 4 weeks" assert today.add(months=1).diff_for_humans() == "in 1 month" with pendulum.test(pendulum.datetime(2017, 3, 31)): - today = pendulum.today().date() + today = pendulum.today("UTC").date() assert today.add(months=1).diff_for_humans() == "in 1 month" with pendulum.test(pendulum.datetime(2017, 4, 30)): - today = pendulum.today().date() + today = pendulum.today("UTC").date() assert today.add(months=1).diff_for_humans() == "in 1 month" with pendulum.test(pendulum.datetime(2017, 1, 31)): - today = pendulum.today().date() + today = pendulum.today("UTC").date() assert today.add(weeks=4).diff_for_humans() == "in 1 month" diff --git a/tests/datetime/test_behavior.py b/tests/datetime/test_behavior.py index 712a4e53..e02323ab 100644 --- a/tests/datetime/test_behavior.py +++ b/tests/datetime/test_behavior.py @@ -6,7 +6,6 @@ from datetime import date from datetime import datetime from datetime import time -from datetime import timedelta import pytest @@ -14,6 +13,7 @@ from pendulum import timezone from pendulum.tz.timezone import Timezone +from pendulum.utils._compat import zoneinfo @pytest.fixture @@ -147,8 +147,9 @@ def test_pickle_with_integer_tzinfo(): def test_proper_dst(): dt = pendulum.datetime(1941, 7, 1, tz="Europe/Amsterdam") + native_dt = datetime(1941, 7, 1, tzinfo=zoneinfo.ZoneInfo("Europe/Amsterdam")) - assert dt.dst() == timedelta(0, 6000) + assert dt.dst() == native_dt.dst() def test_deepcopy(): diff --git a/tests/localization/test_cs.py b/tests/localization/test_cs.py index 8551b61b..b938e63c 100644 --- a/tests/localization/test_cs.py +++ b/tests/localization/test_cs.py @@ -2,7 +2,6 @@ import pendulum - locale = "cs" diff --git a/tests/localization/test_da.py b/tests/localization/test_da.py index 7b455faf..ef46a083 100644 --- a/tests/localization/test_da.py +++ b/tests/localization/test_da.py @@ -2,7 +2,6 @@ import pendulum - locale = "da" diff --git a/tests/localization/test_de.py b/tests/localization/test_de.py index 0b051eef..45f544a7 100644 --- a/tests/localization/test_de.py +++ b/tests/localization/test_de.py @@ -2,7 +2,6 @@ import pendulum - locale = "de" diff --git a/tests/localization/test_es.py b/tests/localization/test_es.py index 4e8af450..7fb8d632 100644 --- a/tests/localization/test_es.py +++ b/tests/localization/test_es.py @@ -2,7 +2,6 @@ import pendulum - locale = "es" diff --git a/tests/localization/test_fa.py b/tests/localization/test_fa.py index 2c52b0e0..3fb9124f 100644 --- a/tests/localization/test_fa.py +++ b/tests/localization/test_fa.py @@ -2,7 +2,6 @@ import pendulum - locale = "fa" diff --git a/tests/localization/test_fo.py b/tests/localization/test_fo.py index d3c82a15..af0bd354 100644 --- a/tests/localization/test_fo.py +++ b/tests/localization/test_fo.py @@ -2,7 +2,6 @@ import pendulum - locale = "fo" diff --git a/tests/localization/test_fr.py b/tests/localization/test_fr.py index 5080061f..6df0d656 100644 --- a/tests/localization/test_fr.py +++ b/tests/localization/test_fr.py @@ -2,7 +2,6 @@ import pendulum - locale = "fr" diff --git a/tests/localization/test_he.py b/tests/localization/test_he.py index 53adfd20..f6b96c4a 100644 --- a/tests/localization/test_he.py +++ b/tests/localization/test_he.py @@ -2,7 +2,6 @@ import pendulum - locale = "he" diff --git a/tests/localization/test_id.py b/tests/localization/test_id.py index 9885c456..a34c10a5 100644 --- a/tests/localization/test_id.py +++ b/tests/localization/test_id.py @@ -2,7 +2,6 @@ import pendulum - locale = "id" diff --git a/tests/localization/test_it.py b/tests/localization/test_it.py index b7fca2a2..1c5c4192 100644 --- a/tests/localization/test_it.py +++ b/tests/localization/test_it.py @@ -2,7 +2,6 @@ import pendulum - locale = "it" diff --git a/tests/localization/test_ja.py b/tests/localization/test_ja.py index 9dba6091..920d3c56 100644 --- a/tests/localization/test_ja.py +++ b/tests/localization/test_ja.py @@ -2,7 +2,6 @@ import pendulum - locale = "ja" diff --git a/tests/localization/test_ko.py b/tests/localization/test_ko.py index 24a392f2..79add872 100644 --- a/tests/localization/test_ko.py +++ b/tests/localization/test_ko.py @@ -2,7 +2,6 @@ import pendulum - locale = "ko" diff --git a/tests/localization/test_lt.py b/tests/localization/test_lt.py index edeb9aa9..5dcdc387 100644 --- a/tests/localization/test_lt.py +++ b/tests/localization/test_lt.py @@ -2,7 +2,6 @@ import pendulum - locale = "lt" diff --git a/tests/localization/test_nb.py b/tests/localization/test_nb.py index 8e548d24..be12df2f 100644 --- a/tests/localization/test_nb.py +++ b/tests/localization/test_nb.py @@ -2,7 +2,6 @@ import pendulum - locale = "nb" diff --git a/tests/localization/test_nl.py b/tests/localization/test_nl.py index e6f8ee91..b4857319 100644 --- a/tests/localization/test_nl.py +++ b/tests/localization/test_nl.py @@ -2,7 +2,6 @@ import pendulum - locale = "nl" diff --git a/tests/localization/test_nn.py b/tests/localization/test_nn.py index bd896718..4c72644a 100644 --- a/tests/localization/test_nn.py +++ b/tests/localization/test_nn.py @@ -2,7 +2,6 @@ import pendulum - locale = "nn" diff --git a/tests/localization/test_pl.py b/tests/localization/test_pl.py index e525d716..81c8ff26 100644 --- a/tests/localization/test_pl.py +++ b/tests/localization/test_pl.py @@ -2,7 +2,6 @@ import pendulum - locale = "pl" diff --git a/tests/localization/test_ru.py b/tests/localization/test_ru.py index 5b7dd35d..c4fbcd7b 100644 --- a/tests/localization/test_ru.py +++ b/tests/localization/test_ru.py @@ -2,7 +2,6 @@ import pendulum - locale = "ru" diff --git a/tests/localization/test_sk.py b/tests/localization/test_sk.py index df44a3a5..1b47a379 100644 --- a/tests/localization/test_sk.py +++ b/tests/localization/test_sk.py @@ -2,7 +2,6 @@ import pendulum - locale = "sk" diff --git a/tests/localization/test_sv.py b/tests/localization/test_sv.py index b73ec50c..7d93a06b 100644 --- a/tests/localization/test_sv.py +++ b/tests/localization/test_sv.py @@ -2,7 +2,6 @@ import pendulum - locale = "sv" diff --git a/tests/parsing/test_parse_iso8601.py b/tests/parsing/test_parse_iso8601.py index 83f28810..0047791b 100644 --- a/tests/parsing/test_parse_iso8601.py +++ b/tests/parsing/test_parse_iso8601.py @@ -8,7 +8,6 @@ from pendulum.parsing import parse_iso8601 - try: from pendulum.parsing._extension import TZFixedOffset as FixedTimezone except ImportError: diff --git a/tests/tz/test_timezone.py b/tests/tz/test_timezone.py index 315deb07..655267d1 100644 --- a/tests/tz/test_timezone.py +++ b/tests/tz/test_timezone.py @@ -11,6 +11,7 @@ from pendulum.tz import fixed_timezone from pendulum.tz.exceptions import AmbiguousTime from pendulum.tz.exceptions import NonExistingTime +from pendulum.utils._compat import zoneinfo from tests.conftest import assert_datetime @@ -232,8 +233,9 @@ def test_utcoffset_pre_transition(): def test_dst(): tz = pendulum.timezone("Europe/Amsterdam") dst = tz.dst(datetime(1940, 7, 1)) + native_tz = zoneinfo.ZoneInfo("Europe/Amsterdam") - assert dst == timedelta(0, 6000) + assert dst == native_tz.dst(datetime(1940, 7, 1)) def test_short_timezones_should_not_modify_time():