From 074017584498582baf8ab04bc7b94b5a39ce1d6e Mon Sep 17 00:00:00 2001 From: David Salvisberg Date: Tue, 11 Jun 2024 09:02:16 +0200 Subject: [PATCH] Adds Python 3.12 support and cleans up type hints a bit --- .github/workflows/python-pr.yaml | 2 +- .github/workflows/python-tox.yaml | 2 +- pyproject.toml | 5 +- sedate/__init__.py | 94 +++++++++++++++---------------- sedate/types.py | 2 +- setup.cfg | 5 +- 6 files changed, 56 insertions(+), 54 deletions(-) diff --git a/.github/workflows/python-pr.yaml b/.github/workflows/python-pr.yaml index ba6d107..ff6ee2c 100644 --- a/.github/workflows/python-pr.yaml +++ b/.github/workflows/python-pr.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, '3.10'] + python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/python-tox.yaml b/.github/workflows/python-tox.yaml index f5effef..b7e25cf 100644 --- a/.github/workflows/python-tox.yaml +++ b/.github/workflows/python-tox.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, '3.10'] + python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 diff --git a/pyproject.toml b/pyproject.toml index 3e621ff..1a8c35f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,14 +50,15 @@ envlist = py37,py38,py39,py310,lint,bandit,mypy [gh-actions] python = - 3.7: py37 3.8: py38,flake8,bandit,mypy 3.9: py39 3.10: py310 + 3.11: py310 + 3.12: py310 [testenv] setenv = - py{37,38,39,310}: COVERAGE_FILE = .coverage.{envname} + py{38,39,310,311,312}: COVERAGE_FILE = .coverage.{envname} deps = coverage[toml] pytest diff --git a/sedate/__init__.py b/sedate/__init__.py index 5a4343d..c5f66b9 100644 --- a/sedate/__init__.py +++ b/sedate/__init__.py @@ -13,6 +13,7 @@ it might very well make sense to use a datetime wrapper library. """ +from __future__ import annotations import operator import pytz @@ -22,17 +23,16 @@ from typing import overload -from typing import Iterable -from typing import Iterator -from typing import Tuple from typing import TYPE_CHECKING if TYPE_CHECKING: - from datetime import date as Date + from collections.abc import Iterable + from collections.abc import Iterator + from datetime import date as date_t from .types import DateLike from .types import DateOrDatetime + from .types import DateOrDatetimeT from .types import Direction - from .types import TDateOrDatetime from .types import TzInfo from .types import TzInfoOrName @@ -46,7 +46,7 @@ class NotTimezoneAware(Exception): pass -def ensure_timezone(timezone: 'TzInfoOrName') -> 'TzInfo': +def ensure_timezone(timezone: TzInfoOrName) -> TzInfo: """ Make sure the given timezone is a pytz timezone, not just a string. """ if isinstance(timezone, str): @@ -58,7 +58,7 @@ def ensure_timezone(timezone: 'TzInfoOrName') -> 'TzInfo': return timezone # type:ignore[return-value] -def as_datetime(value: 'DateLike') -> datetime: +def as_datetime(value: DateLike) -> datetime: """ Turns a date-ish object into a datetime object. """ if isinstance(value, datetime): return value @@ -66,7 +66,7 @@ def as_datetime(value: 'DateLike') -> datetime: return datetime(value.year, value.month, value.day) -def standardize_date(date: datetime, timezone: 'TzInfoOrName') -> datetime: +def standardize_date(date: datetime, timezone: TzInfoOrName) -> datetime: """ Takes the given date and converts it to UTC. The given timezone is set on timezone-naive dates and converted to @@ -87,7 +87,7 @@ def standardize_date(date: datetime, timezone: 'TzInfoOrName') -> datetime: def replace_timezone( date: datetime, - timezone: 'TzInfoOrName', + timezone: TzInfoOrName, *, is_dst: bool = False, raise_non_existent: bool = False, @@ -136,7 +136,7 @@ def replace_timezone( return timezone.normalize(localized) -def to_timezone(date: datetime, timezone: 'TzInfoOrName') -> datetime: +def to_timezone(date: datetime, timezone: TzInfoOrName) -> datetime: """ Takes the given date and converts it to the given timezone. The given date must already be timezone aware for this to work. @@ -152,13 +152,13 @@ def to_timezone(date: datetime, timezone: 'TzInfoOrName') -> datetime: def utcnow() -> datetime: """ Returns a timezone-aware datetime.utcnow(). """ - return replace_timezone(datetime.utcnow(), pytz.UTC) + return datetime.now(pytz.UTC) def is_whole_day( start: datetime, end: datetime, - timezone: 'TzInfoOrName' + timezone: TzInfoOrName ) -> bool: """Returns true if the given start, end range should be considered a whole-day range. This is so if the start time is 0:00:00 and the end @@ -194,15 +194,15 @@ def is_whole_day( def overlaps(start: datetime, end: datetime, otherstart: datetime, otherend: datetime) -> bool: ... @overload # noqa: E302 -def overlaps(start: 'Date', end: 'Date', - otherstart: 'Date', otherend: 'Date') -> bool: ... +def overlaps(start: date_t, end: date_t, + otherstart: date_t, otherend: date_t) -> bool: ... def overlaps( - start: 'DateOrDatetime', - end: 'DateOrDatetime', - otherstart: 'DateOrDatetime', - otherend: 'DateOrDatetime' + start: DateOrDatetime, + end: DateOrDatetime, + otherstart: DateOrDatetime, + otherend: DateOrDatetime ) -> bool: """ Returns True if the given dates overlap in any way. """ @@ -216,17 +216,17 @@ def overlaps( @overload -def count_overlaps(dates: Iterable[Tuple[datetime, datetime]], +def count_overlaps(dates: Iterable[tuple[datetime, datetime]], start: datetime, end: datetime) -> int: ... @overload # noqa: E302 -def count_overlaps(dates: Iterable[Tuple['Date', 'Date']], - start: 'Date', end: 'Date') -> int: ... +def count_overlaps(dates: Iterable[tuple[date_t, date_t]], + start: date_t, end: date_t) -> int: ... def count_overlaps( - dates: Iterable[Tuple['DateOrDatetime', 'DateOrDatetime']], - start: 'DateOrDatetime', - end: 'DateOrDatetime' + dates: Iterable[tuple[DateOrDatetime, DateOrDatetime]], + start: DateOrDatetime, + end: DateOrDatetime ) -> int: """ Goes through the list of start/end tuples in 'dates' and returns the number of times start/end overlaps with any of the dates. @@ -243,8 +243,8 @@ def count_overlaps( def align_date_to_day( date: datetime, - timezone: 'TzInfoOrName', - direction: 'Direction' + timezone: TzInfoOrName, + direction: Direction ) -> datetime: """ Aligns the given date to the beginning or end of the day, depending on the direction. The beginning of the day only makes sense with a timezone @@ -289,8 +289,8 @@ def align_date_to_day( def align_range_to_day( start: datetime, - end: datetime, timezone: 'TzInfoOrName' -) -> Tuple[datetime, datetime]: + end: datetime, timezone: TzInfoOrName +) -> tuple[datetime, datetime]: """ Takes the given start and end date and aligns it to the day depending on the given timezone. @@ -306,7 +306,7 @@ def align_range_to_day( def align_date_to_week( date: datetime, - timezone: 'TzInfoOrName', + timezone: TzInfoOrName, direction: 'Direction' ) -> datetime: """ Like :func:`align_date_to_day`, but for weeks. @@ -336,8 +336,8 @@ def align_date_to_week( def align_range_to_week( start: datetime, end: datetime, - timezone: 'TzInfoOrName' -) -> Tuple[datetime, datetime]: + timezone: TzInfoOrName +) -> tuple[datetime, datetime]: if start > end: raise ValueError(f'{start} - {end} is an invalid range') @@ -350,8 +350,8 @@ def align_range_to_week( def align_date_to_month( date: datetime, - timezone: 'TzInfoOrName', - direction: 'Direction' + timezone: TzInfoOrName, + direction: Direction ) -> datetime: """ Like :func:`align_date_to_day`, but for months. """ @@ -386,8 +386,8 @@ def align_date_to_month( def align_range_to_month( start: datetime, end: datetime, - timezone: 'TzInfoOrName' -) -> Tuple[datetime, datetime]: + timezone: TzInfoOrName +) -> tuple[datetime, datetime]: if start > end: raise ValueError(f'{start} - {end} is an invalid range') @@ -399,13 +399,13 @@ def align_range_to_month( def offset_date( - date: 'TDateOrDatetime', + date: DateOrDatetimeT, delta: timedelta, *, is_dst: bool = False, raise_non_existent: bool = False, raise_ambiguous: bool = False -) -> 'TDateOrDatetime': +) -> DateOrDatetimeT: """ For date and most datetimes it will be the same as adding the the date and delta naively, but for datetimes with a DstTzInfo it will make sure a DST <-> ST transition won't shift the time by an @@ -438,7 +438,7 @@ def get_date_range( is_dst: bool = False, raise_non_existent: bool = False, raise_ambiguous: bool = False -) -> Tuple[datetime, datetime]: +) -> tuple[datetime, datetime]: """ Returns the date-range of a date, a start and an end time. For timezones with daylight savings this might return a range @@ -509,12 +509,12 @@ def parse_time(timestring: str) -> time: def dtrange( - start: 'TDateOrDatetime', - end: 'DateOrDatetime', + start: DateOrDatetimeT, + end: DateOrDatetime, step: timedelta = timedelta(days=1), *, skip_missing: bool = False -) -> Iterator['TDateOrDatetime']: +) -> Iterator[DateOrDatetimeT]: """ Yields dates between start and end (inclusive) using the given step size. The step size may be negative iff end < start. @@ -562,7 +562,7 @@ def dtrange( if step.total_seconds() > 0: step = timedelta(seconds=step.total_seconds() * -1) - def date_iter() -> Iterator['TDateOrDatetime']: + def date_iter() -> Iterator[DateOrDatetimeT]: # dates are immutable, so no copy is needed current = start while remaining(current, end): @@ -585,14 +585,14 @@ def date_iter() -> Iterator['TDateOrDatetime']: pass -def weeknumber(date: 'DateOrDatetime') -> int: +def weeknumber(date: DateOrDatetime) -> int: return date.isocalendar()[1] def weekrange( - start: 'TDateOrDatetime', - end: 'TDateOrDatetime' -) -> Iterator[Tuple['TDateOrDatetime', 'TDateOrDatetime']]: + start: DateOrDatetimeT, + end: DateOrDatetimeT +) -> Iterator[tuple[DateOrDatetimeT, DateOrDatetimeT]]: """ Yields the weeks between start and end (inclusive). If start and end span less than a week, a single start/end pair is the @@ -614,7 +614,7 @@ def weekrange( week_step = timedelta(days=7) aligned_start = start + timedelta(days=6 - start.weekday()) else: - # here the start and end are backwards + # here the start and end are b`ackwards day_step = timedelta(days=-1) week_step = timedelta(days=-7) aligned_start = start - timedelta(days=start.weekday()) diff --git a/sedate/types.py b/sedate/types.py index 204eeea..672f9bb 100644 --- a/sedate/types.py +++ b/sedate/types.py @@ -16,7 +16,7 @@ class HasDateAttrs(Protocol): AmbiguousAction = Literal['skip_dst', 'skip_st', 'include_both'] DateOrDatetime = Union[datetime.date, datetime.datetime] DateLike = Union[DateOrDatetime, HasDateAttrs] -TDateOrDatetime = TypeVar('TDateOrDatetime', datetime.date, datetime.datetime) +DateOrDatetimeT = TypeVar('DateOrDatetimeT', datetime.date, datetime.datetime) Direction = Literal['up', 'down'] TzInfo = Union[pytz._UTCclass, pytz.tzinfo.StaticTzInfo, pytz.tzinfo.DstTzInfo] TzInfoOrName = Union[pytz.BaseTzInfo, str] diff --git a/setup.cfg b/setup.cfg index c6a2427..36835ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,10 +15,11 @@ classifiers = License :: OSI Approved :: GNU General Public License v2 (GPLv2) Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Intended Audience :: Developers Topic :: Software Development :: Libraries :: Python Modules @@ -28,7 +29,7 @@ include_package_data = True zip_safe = False packages = sedate -python_requires = >=3.7 +python_requires = >=3.8 install_requires = pytz