Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ jobs:
strategy:
matrix:
os: ['ubuntu-latest']
python: ['3.10', '3.11', '3.12', '3']
python: ['3.11', '3.12', '3']
include:
- os: ubuntu-22.04
python: '3.9'
python: '3.10'
- os: 'macos-latest'
python: '3.9'
python: '3.10'
name: ${{ matrix.os }} py-${{ matrix.python }}
steps:
- name: Checkout
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: ['3.10', '3.11', '3.12']
python-version: ['3.11', '3.12']
coverage: [false]
include:
# Modify existing configurations:
Expand All @@ -30,13 +30,13 @@ jobs:
tz: 'XXX-05:30' # UTC+05:30
# Add new configurations:
- os: ubuntu-22.04
python-version: '3.9' # oldest supported version
python-version: '3.10' # oldest supported version
coverage: false
- os: ubuntu-latest
python-version: '3.x'
python-version: '3'
coverage: true
- os: macos-latest
python-version: '3.9' # oldest supported version
python-version: '3.10' # oldest supported version
coverage: false
name: ${{ matrix.os }} py-${{ matrix.python-version }} ${{ matrix.tz }} ${{ matrix.coverage && '(coverage)' || '' }}
env:
Expand Down
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ updated. Only the first match gets replaced, so it's fine to leave the old
ones in. -->
## isodatetime 3.2.0 (<span actions:bind='release-date'>Upcoming</span>)

Requires Python 3.9+
Requires Python 3.10+

### Breaking changes

Expand Down
61 changes: 26 additions & 35 deletions metomi/isodatetime/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@
from math import floor
import operator
from typing import (
TYPE_CHECKING,
Dict,
List,
Optional,
Tuple,
Union,
Literal,
cast,
overload,
)
Expand All @@ -40,10 +35,6 @@
from .exceptions import BadInputError


if TYPE_CHECKING:
from typing_extensions import Literal


_operator_map = {op.__name__: op for op in [
operator.eq, operator.lt, operator.le, operator.gt, operator.ge]}

Expand Down Expand Up @@ -306,7 +297,7 @@ def get_is_valid(self, timepoint: 'TimePoint') -> bool:
return False
return False

def get_next(self, timepoint: 'TimePoint') -> Optional['TimePoint']:
def get_next(self, timepoint: 'TimePoint') -> 'TimePoint | None':
"""Return the next timepoint after this timepoint in the recurrence
series, or None."""
if self._repetitions == 1 or timepoint is None:
Expand All @@ -316,7 +307,7 @@ def get_next(self, timepoint: 'TimePoint') -> Optional['TimePoint']:
return next_timepoint
return None

def get_prev(self, timepoint: 'TimePoint') -> Optional['TimePoint']:
def get_prev(self, timepoint: 'TimePoint') -> 'TimePoint | None':
"""Return the previous timepoint before this timepoint in the
recurrence series, or None."""
if self._repetitions == 1 or timepoint is None:
Expand All @@ -326,7 +317,7 @@ def get_prev(self, timepoint: 'TimePoint') -> Optional['TimePoint']:
return prev_timepoint
return None

def get_first_after(self, timepoint: 'TimePoint') -> Optional['TimePoint']:
def get_first_after(self, timepoint: 'TimePoint') -> 'TimePoint | None':
"""Return the next timepoint in the series after the given timepoint
which is not necessarily part of the series.

Expand Down Expand Up @@ -656,7 +647,7 @@ def to_weeks(self):
def __abs__(self) -> 'Duration':
new = self.__class__(_is_empty_instance=True)
for attr in self.__slots__:
value: Union[int, float, None] = getattr(self, attr)
value: int | float | None = getattr(self, attr)
setattr(new, attr, abs(value) if value else value)
return new

Expand All @@ -671,7 +662,7 @@ def __add__(self, other: 'TimeRecurrence') -> 'TimeRecurrence': ...

def __add__(
self, other: object
) -> Union['Duration', 'TimePoint', 'TimeRecurrence']:
) -> 'Duration | TimePoint | TimeRecurrence':
if isinstance(other, Duration):
new = self._copy()
if new.get_is_in_weeks():
Expand Down Expand Up @@ -706,7 +697,7 @@ def __mul__(self, other: object) -> 'Duration':
return NotImplemented
new = self.__class__(_is_empty_instance=True)
for attr in self.__slots__:
value: Union[int, float, None] = getattr(self, attr)
value: int | float | None = getattr(self, attr)
setattr(new, attr, value * other if value else value)
return new

Expand Down Expand Up @@ -1448,15 +1439,15 @@ def to_ordinal_date(self) -> 'TimePoint':
new._week_of_year, new._day_of_week = (None, None)
return new

def get_largest_truncated_property_name(self) -> Optional[str]:
def get_largest_truncated_property_name(self) -> str | None:
"""Return the largest unit in a truncated representation."""
truncated_props = self.get_truncated_properties()
if not truncated_props:
return None
# Relies on dict being ordered in Python 3.6+:
return next(iter(truncated_props))

def get_smallest_missing_property_name(self) -> Optional[str]:
def get_smallest_missing_property_name(self) -> str | None:
"""Return the smallest unit missing from a truncated representation."""
if not self._truncated:
return None
Expand All @@ -1476,7 +1467,7 @@ def get_smallest_missing_property_name(self) -> Optional[str]:
return attr_value
return None

def get_truncated_properties(self) -> Optional[Dict[str, float]]:
def get_truncated_properties(self) -> dict[str, float] | None:
"""Return a map of properties if this is a truncated representation.

Ordered from largest unit to smallest.
Expand Down Expand Up @@ -1512,13 +1503,13 @@ def _add_truncated(self, other: 'TimePoint') -> 'TimePoint':
if unit not in props:
props[unit] = 0

year_of_century = cast('Optional[int]', props.get('year_of_century'))
year_of_decade = cast('Optional[int]', props.get('year_of_decade'))
month_of_year = cast('Optional[int]', props.get('month_of_year'))
week_of_year = cast('Optional[int]', props.get('week_of_year'))
day_of_year = cast('Optional[int]', props.get('day_of_year'))
day_of_month = cast('Optional[int]', props.get('day_of_month'))
day_of_week = cast('Optional[int]', props.get('day_of_week'))
year_of_century = cast('int | None', props.get('year_of_century'))
year_of_decade = cast('int | None', props.get('year_of_decade'))
month_of_year = cast('int | None', props.get('month_of_year'))
week_of_year = cast('int | None', props.get('week_of_year'))
day_of_year = cast('int | None', props.get('day_of_year'))
day_of_month = cast('int | None', props.get('day_of_month'))
day_of_week = cast('int | None', props.get('day_of_week'))
hour_of_day = props.get('hour_of_day')
minute_of_hour = props.get('minute_of_hour')
second_of_minute = props.get('second_of_minute')
Expand Down Expand Up @@ -1605,7 +1596,7 @@ def _add_truncated(self, other: 'TimePoint') -> 'TimePoint':
return new

def _next_month_and_day(
self, month: Optional[int], day: Optional[int]
self, month: int | None, day: int | None
) -> None:
"""Get the next TimePoint after this one that has the
same month and/or day as specified.
Expand All @@ -1618,7 +1609,7 @@ def _next_month_and_day(
"""
if day is None:
day = 1
years_to_check: List[int] = [self._year, self._year + 1]
years_to_check: list[int] = [self._year, self._year + 1]
for i, year in enumerate(years_to_check):
self._year = year
if month:
Expand Down Expand Up @@ -1811,7 +1802,7 @@ def __sub__(self, other: 'Duration') -> 'TimePoint': ...
@overload
def __sub__(self, other: 'TimePoint') -> 'Duration': ...

def __sub__(self, other: object) -> Union['TimePoint', 'Duration']:
def __sub__(self, other: object) -> 'TimePoint | Duration':
if isinstance(other, TimePoint):
if self._truncated or other._truncated:
raise ValueError(
Expand Down Expand Up @@ -2245,7 +2236,7 @@ def get_is_leap_year(year):

def find_next_leap_year(
year: int, step: 'Literal[1, 10, 100]' = 1
) -> Optional[int]:
) -> int | None:
"""Find the next leap year after or including this year.

Returns None if calendar does not have leap years, or it is not possible
Expand Down Expand Up @@ -2322,7 +2313,7 @@ def _get_days_in_year(year, _):

def get_days_in_month(
month_of_year: int,
year: Union[int, None, 'Literal["leap"]'] = "leap",
year: int | None | Literal["leap"] = "leap",
) -> int:
"""Return the number of days in the month of this particular year.
Year can also be "leap", or None for non-leap."""
Expand Down Expand Up @@ -2649,10 +2640,10 @@ def get_timepoint_properties_from_seconds_since_unix_epoch(num_seconds):

def iter_months_days(
year: int,
month_of_year: Optional[int] = None,
day_of_month: Optional[int] = None,
month_of_year: int | None = None,
day_of_month: int | None = None,
in_reverse: bool = False
) -> List[Tuple[int, int]]:
) -> list[tuple[int, int]]:
"""Iterate over each day in each month of year.

Args:
Expand All @@ -2675,7 +2666,7 @@ def _iter_months_days(
day_of_month: int,
_cal_mode,
in_reverse: bool = False
) -> List[Tuple[int, int]]:
) -> list[tuple[int, int]]:
if day_of_month is not None and month_of_year is None:
raise ValueError("Need to specify start month as well as day.")
source = CALENDAR.INDEXED_DAYS_IN_MONTHS
Expand Down
5 changes: 2 additions & 3 deletions metomi/isodatetime/tests/test_01.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
# ----------------------------------------------------------------------------
"""This tests the ISO 8601 data model functionality."""

from typing import Optional, Union
import pytest
import unittest

Expand Down Expand Up @@ -904,7 +903,7 @@ def test_timepoint_duration_subtract(test):
)
def test_timepoint_add(
timepoint: data.TimePoint,
other: Union[data.Duration, data.TimePoint],
other: data.Duration | data.TimePoint,
expected: data.TimePoint
):
"""Test adding to a timepoint"""
Expand Down Expand Up @@ -1006,7 +1005,7 @@ def test_timepoint_without_year():
]
)
def test_find_next_leap_year(
calendar_mode: str, year: int, expected: Optional[int],
calendar_mode: str, year: int, expected: int | None,
patch_calendar_mode
):
patch_calendar_mode(calendar_mode)
Expand Down
8 changes: 4 additions & 4 deletions metomi/isodatetime/tests/test_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

"""This tests the timezone module."""

from typing import Callable, Tuple
from typing import Callable
import pytest

from metomi.isodatetime import timezone
Expand Down Expand Up @@ -75,9 +75,9 @@
def test_get_local_time_zone(
dst: bool,
tz_seconds: int,
expected_no_dst: Tuple[int, int],
expected_no_dst: tuple[int, int],
tz_dst_seconds: int,
expected_with_dst: Tuple[int, int],
expected_with_dst: tuple[int, int],
mock_local_time_zone: Callable
):
"""Test that the hour/minute returned is correct.
Expand Down Expand Up @@ -134,7 +134,7 @@ def test_get_local_time_zone(
)
def test_get_local_time_zone_format(
tz_seconds: int,
expected_formats: Tuple[int, int, int],
expected_formats: tuple[int, int, int],
mock_local_time_zone: Callable
):
"""Test that the UTC offset string format is correct.
Expand Down
3 changes: 1 addition & 2 deletions metomi/isodatetime/timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"""This provides utilities for extracting the local time zone."""

import time
from typing import Tuple


class TimeZoneFormatMode(object):
Expand All @@ -34,7 +33,7 @@ class TimeZoneFormatMode(object):
}


def get_local_time_zone() -> Tuple[int, int]:
def get_local_time_zone() -> tuple[int, int]:
"""Return the current local UTC offset in hours and minutes."""
utc_offset_seconds = -time.timezone
if time.localtime().tm_isdst == 1 and time.daylight:
Expand Down
7 changes: 1 addition & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,12 @@ description = "Python ISO 8601 date time parser and data model/manipulation util
license = "LGPL-3.0-only"
readme = "README.md"
keywords = ["isodatetime", "datetime", "iso8601", "date", "time", "parser"]
requires-python = ">=3.9"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Other Environment",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Utilities"
Expand Down