Skip to content
13 changes: 13 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ jobs:
- uses: psf/black@stable
with:
options: "--check"
pyright:
name: pyright
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Run pyright
run: |
pip install pyright
pyright unit_of_time/*.py
test:
name: run tests
runs-on: ubuntu-latest
Expand Down
75 changes: 40 additions & 35 deletions unit_of_time/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import math
from datetime import date, datetime, timedelta
from typing import Dict, Union
Comment on lines 1 to 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Python 3.8 compatibility: annotations will break without future import.

You’re using PEP 585 generics (list[...] / tuple[...]) while supporting 3.8. Add the future import (or switch to typing.List/Tuple). This is a real runtime error on 3.8.

+from __future__ import annotations
 import math
 from datetime import date, datetime, timedelta
-from typing import Dict, Union
+from typing import Dict, Union, ClassVar, Mapping, Type
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import math
from datetime import date, datetime, timedelta
from typing import Dict, Union
from __future__ import annotations
import math
from datetime import date, datetime, timedelta
from typing import Dict, Union, ClassVar, Mapping, Type
🤖 Prompt for AI Agents
In unit_of_time/__init__.py lines 1-3, the module may use PEP 585-style
annotations which break at runtime on Python 3.8; add the future import by
inserting "from __future__ import annotations" as the very first line of the
file (before any other imports) to enable postponed evaluation of annotations,
or alternatively replace PEP 585 types with typing.List, typing.Tuple, etc.,
throughout the file to maintain 3.8 compatibility.



def date_from_int(val, div=1):
def date_from_int(val: int, div=1) -> date:
val //= div
d = val % 100
val //= 100
Expand All @@ -11,26 +12,26 @@ def date_from_int(val, div=1):
return date(val, m, d)


def date_to_int(val, mul=1):
def date_to_int(val: date, mul: int = 1) -> int:
return mul * (val.year * 10000 + val.month * 100 + val.day)


class TimeunitKindMeta(type):
kind_int = None
formatter = None
_pre_registered = []
_registered = None
_multiplier = None
kind_int: int = -1
formatter: str = ""
_pre_registered: list["TimeunitKindMeta"] = []
_registered: Union[None, Dict[int, "TimeunitKindMeta"]] = None
_multiplier: int = -1

def __init__(cls, name, bases, attrs):
def __init__(cls, name, bases, attrs) -> None:
super().__init__(name, bases, attrs)
if cls.kind_int is not None:
TimeunitKindMeta._pre_registered.append(cls)
TimeunitKindMeta._registered = None
TimeunitKindMeta._multiplier = None
TimeunitKindMeta._multiplier = -1

@property
def unit_register(self):
def unit_register(self) -> Dict[int, "TimeunitKindMeta"]:
result = TimeunitKindMeta._registered
if result is None:
result = {
Expand All @@ -42,27 +43,29 @@ def unit_register(self):
return result

@property
def multiplier(cls):
def multiplier(cls) -> int:
result = TimeunitKindMeta._multiplier
if result is None:
result = max(1, *[k.kind_int for k in TimeunitKindMeta._pre_registered])
result = 10 ** math.ceil(math.log10(result))
if result == -1:
result = 1
for k in TimeunitKindMeta._pre_registered:
if k.kind_int >= result:
result *= 10
TimeunitKindMeta._multiplier = result
return result

def __int__(self):
def __int__(self) -> int:
return self.kind_int

def __index__(self):
def __index__(self) -> int:
return int(self)

def __hash__(self):
def __hash__(self) -> int:
"""
Return the hash value of the time unit, based on its integer encoding.
"""
return hash(int(self))

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
"""
Return True if this time unit kind is the same as another kind or matches the kind registered for the given integer.

Expand All @@ -73,10 +76,11 @@ def __eq__(self, other):
bool: True if both refer to the same time unit kind, otherwise False.
"""
if isinstance(other, int):
other = TimeunitKind.unit_register[other]
# if out of range, None will fail
other = TimeunitKind.unit_register.get(other)
return self is other

def __call__(cls, dt):
def __call__(cls, dt: Union["Timeunit", date]) -> "Timeunit":
"""
Creates a `Timeunit` instance of this kind from a given date or `Timeunit`.

Expand All @@ -99,7 +103,7 @@ def get_previous(cls, dt):
dt -= timedelta(days=1)
return cls(dt)

def last_day(cls, dt):
def last_day(cls, dt: date) -> date:
"""
Return the last date of the time unit containing the given date.

Expand All @@ -111,7 +115,7 @@ def last_day(cls, dt):
"""
return cls._next(dt) - timedelta(days=1)

def _next(cls, dt):
def _next(cls, dt: date) -> date:
"""
Return the first day of the next time unit following the given date.

Expand All @@ -123,7 +127,7 @@ def _next(cls, dt):
"""
return cls.last_day(dt) + timedelta(days=1)

def get_next(cls, dt):
def get_next(cls, dt: Union["Timeunit", date]) -> "Timeunit":
"""
Return the next time unit instance of this kind after the given date.

Expand All @@ -133,16 +137,16 @@ def get_next(cls, dt):
dt = dt.dt
return cls(cls._next(cls.truncate(dt)))

def to_str(cls, dt):
def to_str(cls, dt) -> str:
return dt.strftime(cls.formatter)

def truncate(cls, dt):
def truncate(cls, dt: date) -> date:
return datetime.strptime(cls.to_str(dt), cls.formatter).date()

def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur, dt, amount) -> Union[date, None]:
return None

def _shift(cls, cur, dt, amount):
def _shift(cls, cur: "Timeunit", dt: date, amount: int) -> "Timeunit":
new_dt = cls._inner_shift(cur, dt, amount)
if new_dt is not None:
return cls(new_dt)
Expand All @@ -159,8 +163,8 @@ def _shift(cls, cur, dt, amount):


class TimeunitKind(metaclass=TimeunitKindMeta):
kind_int = None
formatter = None
kind_int = -1
formatter = ""


class Year(TimeunitKind):
Expand All @@ -184,7 +188,7 @@ def to_str(cls, dt):
return f"{dt.year}Q{dt.month//3}"

@classmethod
def truncate(cls, dt):
def truncate(cls, dt: date) -> date:
return date(dt.year, 3 * ((dt.month - 1) // 3) + 1, 1)

@classmethod
Expand Down Expand Up @@ -229,7 +233,7 @@ def _inner_shift(cls, cur, dt, amount):
return dt + timedelta(days=7 * amount)

@classmethod
def truncate(cls, dt):
def truncate(cls, dt: date) -> date:
if isinstance(dt, datetime):
dt = dt.date()
return dt - timedelta(days=dt.weekday())
Expand All @@ -244,11 +248,11 @@ class Day(TimeunitKind):
formatter = "%Y-%m-%d"

@classmethod
def _inner_shift(cls, cur, dt, amount):
def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date:
return dt + timedelta(days=amount)

@classmethod
def _next(self, dt):
def _next(cls, dt: date) -> date:
return dt + timedelta(days=1)


Expand Down Expand Up @@ -370,7 +374,7 @@ def __repr__(self):
return f"{self.__class__.__name__}({self.kind.__qualname__}, {self.dt!r})"

@classmethod
def _get_range(cls, item):
def _get_range(cls, item) -> tuple[date, date]:
"""
Extracts a date range tuple from the given item.

Expand All @@ -396,7 +400,8 @@ def _get_range(cls, item):
try:
dt0, dt1 = item
if isinstance(dt0, date) and isinstance(dt1, date):
return item
return dt0, dt1
raise TypeError(f"Item {item!r} is not a date range.") from None
except TypeError:
raise TypeError(f"Item {item!r} has no date range.") from None

Expand Down