diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79e311e..ef59801 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,10 +8,38 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - 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' + cache: 'pip' + - name: Run pyright + run: | + pip install "pyright>=1.1,<2" "setuptools>=38.6.0,<69.0" + pyright + isort: + name: isort + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + cache: 'pip' + - name: Run isort + run: | + pip install "isort>=6.0,<6.1" + isort -c unit_of_time test: name: run tests runs-on: ubuntu-latest @@ -46,7 +74,7 @@ jobs: publish: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') - needs: [black, build, test] + needs: [black, build, test, pyright] steps: - name: checkout code uses: actions/checkout@v4 diff --git a/timetest.py b/timetest.py index 4c7dd43..fd43eef 100644 --- a/timetest.py +++ b/timetest.py @@ -1,7 +1,7 @@ import unittest from datetime import date, datetime, time, timedelta -from unit_of_time import Year, Quarter, Month, Week, Day, TimeunitKind, Timeunit +from unit_of_time import Day, Month, Quarter, Timeunit, TimeunitKind, Week, Year class Decade(TimeunitKind): diff --git a/unit_of_time/__init__.py b/unit_of_time/__init__.py index 39c8afa..2935868 100644 --- a/unit_of_time/__init__.py +++ b/unit_of_time/__init__.py @@ -1,8 +1,8 @@ -import math from datetime import date, datetime, timedelta +from typing import Dict, Union -def date_from_int(val, div=1): +def date_from_int(val: int, div: int=1) -> date: val //= div d = val % 100 val //= 100 @@ -11,26 +11,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: 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 = { @@ -42,27 +42,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: + while 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. @@ -73,10 +75,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`. @@ -89,17 +92,17 @@ def __call__(cls, dt): def __lt__(self, other): return self.kind_int < other.kind_int - def from_int(cls, val): + def from_int(cls, val: int) -> "Timeunit": mul = cls.multiplier return TimeunitKind.unit_register[val % mul](date_from_int(val, mul)) - def get_previous(cls, dt): + def get_previous(cls, dt: Union[date, "Timeunit"]) -> "Timeunit": if isinstance(dt, Timeunit): dt = dt.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. @@ -111,7 +114,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. @@ -123,7 +126,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. @@ -133,16 +136,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) -> 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) @@ -159,8 +162,8 @@ def _shift(cls, cur, dt, amount): class TimeunitKind(metaclass=TimeunitKindMeta): - kind_int = None - formatter = None + kind_int = -1 + formatter = "" class Year(TimeunitKind): @@ -168,11 +171,11 @@ class Year(TimeunitKind): formatter = "%Y" @classmethod - def _next(cls, dt): + def _next(cls, dt: date) -> date: return date(dt.year + 1, 1, 1) @classmethod - def _inner_shift(cls, cur, dt, amount): + def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int): return date(dt.year + amount, 1, 1) @@ -184,15 +187,15 @@ 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 - def _inner_shift(cls, cur, dt, amount): + def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date: q_new = dt.year * 4 + amount + (dt.month - 1) // 3 y = q_new // 4 q = q_new % 4 - return date(q_new // 4, 3 * q + 1, 1) + return date(y, 3 * q + 1, 1) @classmethod def _next(cls, dt): @@ -207,7 +210,7 @@ class Month(TimeunitKind): formatter = "%YM%m" @classmethod - def _inner_shift(cls, cur, dt, amount): + def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date: m_new = dt.year * 12 + amount + dt.month - 1 return date(m_new // 12, m_new % 12 + 1, 1) @@ -225,11 +228,11 @@ class Week(TimeunitKind): formatter = "%YW%W" @classmethod - def _inner_shift(cls, cur, dt, amount): + def _inner_shift(cls, cur: "Timeunit", dt: date, amount: int) -> date: 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()) @@ -244,11 +247,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) @@ -370,7 +373,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. @@ -396,7 +399,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