Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor #75

Merged
merged 9 commits into from
May 10, 2024
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
8 changes: 4 additions & 4 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
python-version: "3.10"
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install & run interrogate
run: |
uv pip install interrogate==1.5.0 --system
make interrogate
# - name: Install & run interrogate
# run: |
# uv pip install interrogate==1.5.0 --system
# make interrogate
- name: Install & run linter
run: |
uv pip install ."[lint]" --system
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ repos:
rev: 1.7.0
hooks:
- id: interrogate
args: [-vv, --ignore-nested-functions, --ignore-module, --ignore-init-method, --ignore-private, --ignore-magic, --ignore-property-decorators, --fail-under=90, iso_week_date, tests]
args: [iso_week_date, tests]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ interrogate:
interrogate-badge:
interrogate --generate-badge docs/img/interrogate-shield.svg

check: interrogate lint test clean-folders
type:
mypy iso_week_date

check: interrogate lint test type clean-folders

docs-serve:
mkdocs serve
Expand Down
4 changes: 2 additions & 2 deletions docs/img/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ In a nutshell it provides:

- [`IsoWeek`](https://fbruzzesi.github.io/iso-week-date/api/isoweek/) and [`IsoWeekDate`](https://fbruzzesi.github.io/iso-week-date/api/isoweekdate/) classes that implement a series of methods to work with ISO Week (Date) formats directly, avoiding the pitfalls of going back and forth between string, date and datetime python objects.
- [pandas](https://fbruzzesi.github.io/iso-week-date/api/pandas/) and [polars](https://fbruzzesi.github.io/iso-week-date/api/polars/) functionalities (and namespaces) to work with series of ISO Week dates.
- [pydantic](https://fbruzzesi.github.io/iso-week-date/user-guide/pydantic/) compatible types, as described in their docs section on how to [customize validation with `__get_pydantic_core_schema__`](https://docs.pydantic.dev/latest/concepts/types/#customizing-validation-with-__get_pydantic_core_schema__)

---

Expand Down
17 changes: 12 additions & 5 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,25 @@
## Dependencies

- To work with `IsoWeek` and `IsoWeekDate` classes, no additional dependencies are required.
- _pandas_ and _polars_ functionalities require the installation of the respective libraries.
- _pandas_, _polars_ and/or _pydantic_ functionalities require the installation of the respective libraries.

=== "pandas"

```bash
pip install pandas
pip install iso-week-date[pandas]
python -m pip install "pandas>=1.0.0"
python -m pip install "iso-week-date[pandas]"
```

=== "polars"

```bash
pip install polars # polars>=0.18.0
pip install iso-week-date[polars]
python pip install "polars>=0.18.0"
python pip install "iso-week-date[polars]"
```

=== "pydantic"

```bash
python pip install "pydantic>=2.4.0"
python pip install "iso-week-date[pydantic]"
```
34 changes: 22 additions & 12 deletions iso_week_date/_utils.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from __future__ import annotations

import re
import sys
from typing import Callable, Generic, Type, TypeVar, Union
from importlib.metadata import version
from typing import Callable, Generic, Tuple, Type, TypeVar

if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover
if sys.version_info >= (3, 11): # pragma: no cover
from typing import Self
else: # pragma: no cover
from typing_extensions import Self


T = TypeVar("T")
R = TypeVar("R")


class classproperty(Generic[T, R]):
"""Decorator to create a class level property. It allows to define a property at the class level, which can be
accessed without creating an instance of the class.
class classproperty(Generic[T, R]): # noqa: N801
"""Decorator to create a class level property.

It allows to define a property at the class level, which can be accessed without creating an instance of the class.

Arguments:
func: Function to be decorated.
Expand All @@ -35,7 +40,7 @@ def __init__(self: Self, func: Callable[[Type[T]], R]) -> None:
"""Initialize classproperty."""
self.func = func

def __get__(self: Self, obj: Union[T, None], owner: Type[T]) -> R:
def __get__(self: Self, obj: T | None, owner: Type[T]) -> R:
"""Get the value of the class property.

Arguments:
Expand All @@ -47,7 +52,6 @@ def __get__(self: Self, obj: Union[T, None], owner: Type[T]) -> R:

def format_err_msg(_fmt: str, _value: str) -> str: # pragma: no cover
"""Format error message given a format and a value."""

return (
"Invalid isoweek date format. "
f"Format must match the '{_fmt}' pattern, "
Expand All @@ -61,7 +65,7 @@ def format_err_msg(_fmt: str, _value: str) -> str: # pragma: no cover


def p_of_year(year: int) -> int:
"""Returns the day of the week of 31 December"""
"""Returns the day of the week of 31 December."""
return (year + year // 4 - year // 100 + year // 400) % 7


Expand All @@ -79,4 +83,10 @@ def weeks_of_year(year: int) -> int:
Returns:
Number of weeks in the year (either 52 or 53)
"""
return 52 + (p_of_year(year) == 4 or p_of_year(year - 1) == 3)
return 52 + (p_of_year(year) == 4 or p_of_year(year - 1) == 3) # noqa: PLR2004


def parse_version(module: str) -> Tuple[int, ...]:
"""Parses a module version and return a tuple of integers."""
module_version = version(module).split(".")
return tuple(int(re.sub(r"\D", "", str(v))) for v in module_version)
123 changes: 86 additions & 37 deletions iso_week_date/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
from iso_week_date._utils import classproperty, format_err_msg, weeks_of_year
from iso_week_date.mixin import ComparatorMixin, ConverterMixin, IsoWeekProtocol, ParserMixin

if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover
if sys.version_info >= (3, 11): # pragma: no cover
from typing import Self
else: # pragma: no cover
from typing_extensions import Self

BaseIsoWeek_T = TypeVar("BaseIsoWeek_T", str, date, datetime, "BaseIsoWeek", covariant=True)
BaseIsoWeek_T = TypeVar("BaseIsoWeek_T", str, date, datetime, "BaseIsoWeek", covariant=True) # noqa: PLC0105


class InclusiveEnum(str, Enum):
"""Inclusive enum"""
"""Enum describing the Inclusive values."""

both = "both"
left = "left"
Expand Down Expand Up @@ -76,7 +76,8 @@ def _validate(cls: Type[Self], value: str) -> str:
year, week = int(_match.group(1)), int(_match.group(2)[1:])

if weeks_of_year(year) < week:
raise ValueError(f"Invalid week number. Year {year} has only {weeks_of_year(year)} weeks.")
msg = f"Invalid week number. Year {year} has only {weeks_of_year(year)} weeks."
raise ValueError(msg)

return value

Expand All @@ -89,12 +90,12 @@ def __str__(self: Self) -> str:
return self.value_

@classproperty
def _compact_pattern(cls: Type[IsoWeekProtocol]) -> re.Pattern:
def _compact_pattern(cls: Type[IsoWeekProtocol]) -> re.Pattern: # noqa: N805
"""Returns compiled compact pattern."""
return re.compile(cls._pattern.pattern.replace(")-(", ")("))

@classproperty
def _compact_format(cls: Type[IsoWeekProtocol]) -> str:
def _compact_format(cls: Type[IsoWeekProtocol]) -> str: # noqa: N805
"""Returns compact format as string."""
return cls._format.replace("-", "")

Expand Down Expand Up @@ -133,8 +134,9 @@ def week(self: Self) -> int:

@property
def quarter(self: Self) -> int:
"""Returns quarter number as integer. The first three quarters have 13 weeks, while the last one has either 13
or 14 weeks depending on the year.
"""Returns quarter number as integer.

The first three quarters have 13 weeks, while the last one has either 13 or 14 weeks depending on the year:

- Q1: weeks from 1 to 13
- Q2: weeks from 14 to 26
Expand All @@ -149,49 +151,56 @@ def quarter(self: Self) -> int:
IsoWeekDate("2023-W52-1").quarter # 4
```
"""

return min((self.week - 1) // 13 + 1, 4)

@overload
def __add__(self: Self, other: Union[int, timedelta]) -> Self: # pragma: no cover
"""Implementation of addition operator."""
...
def __add__(self: Self, other: Union[int, timedelta]) -> Self: ... # pragma: no cover

@overload
def __add__(self: Self, other: Iterable[Union[int, timedelta]]) -> Generator[Self, None, None]: # pragma: no cover
"""Implementation of addition operator."""
...
def __add__(
self: Self,
other: Iterable[Union[int, timedelta]],
) -> Generator[Self, None, None]: ... # pragma: no cover

@overload
def __add__(
self: Self,
other: Union[int, timedelta, Iterable[Union[int, timedelta]]],
) -> Union[Self, Generator[Self, None, None]]: ... # pragma: no cover

@abstractmethod
def __add__(
self: Self, other: Union[int, timedelta, Iterable[Union[int, timedelta]]]
self: Self,
other: Union[int, timedelta, Iterable[Union[int, timedelta]]],
) -> Union[Self, Generator[Self, None, None]]: # pragma: no cover
"""Implementation of addition operator."""
...

@overload
def __sub__(self: Self, other: Union[int, timedelta]) -> Self: # pragma: no cover
"""Annotation for subtraction with `int` and `timedelta`"""
...
def __sub__(self: Self, other: Union[int, timedelta]) -> Self: ... # pragma: no cover

@overload
def __sub__(self: Self, other: Self) -> int: # pragma: no cover
"""Annotation for subtraction with other `BaseIsoWeek`"""
...
def __sub__(self: Self, other: Self) -> int: ... # pragma: no cover

@overload
def __sub__(self: Self, other: Iterable[Union[int, timedelta]]) -> Generator[Self, None, None]: # pragma: no cover
"""Annotation for subtraction with other `BaseIsoWeek`"""
...
def __sub__(
self: Self,
other: Iterable[Union[int, timedelta]],
) -> Generator[Self, None, None]: ... # pragma: no cover

@overload
def __sub__(self: Self, other: Iterable[Self]) -> Generator[int, None, None]: # pragma: no cover
"""Annotation for subtraction with other `Self`"""
...
def __sub__(self: Self, other: Iterable[Self]) -> Generator[int, None, None]: ... # pragma: no cover

@overload
def __sub__(
self: Self,
other: Union[int, timedelta, Self, Iterable[Union[int, timedelta, Self]]],
) -> Union[int, Self, Generator[Union[int, Self], None, None]]: ... # pragma: no cover

@abstractmethod
def __sub__(
self: Self, other: Union[int, timedelta, Self, Iterable[Union[int, timedelta, Self]]]
self: Self,
other: Union[int, timedelta, Self, Iterable[Union[int, timedelta, Self]]],
) -> Union[int, Self, Generator[Union[int, Self], None, None]]: # pragma: no cover
"""Implementation of subtraction operator."""
...
Expand All @@ -200,11 +209,48 @@ def __next__(self: Self) -> Self:
"""Implementation of next operator."""
return self + 1

@overload
@classmethod
def range(
cls: Type[Self],
start: BaseIsoWeek_T,
end: BaseIsoWeek_T,
*,
step: int = 1,
inclusive: Literal["both", "left", "right", "neither"] = "both",
as_str: Literal[True],
) -> Generator[str, None, None]: ... # pragma: no cover

@overload
@classmethod
def range(
cls: Type[Self],
start: BaseIsoWeek_T,
end: BaseIsoWeek_T,
*,
step: int = 1,
inclusive: Literal["both", "left", "right", "neither"] = "both",
as_str: Literal[False],
) -> Generator[Self, None, None]: ... # pragma: no cover

@overload
@classmethod
def range(
cls: Type[Self],
start: BaseIsoWeek_T,
end: BaseIsoWeek_T,
*,
step: int = 1,
inclusive: Literal["both", "left", "right", "neither"] = "both",
as_str: bool = True,
) -> Generator[Union[str, Self], None, None]: ... # pragma: no cover

@classmethod
def range(
cls: Type[Self],
start: BaseIsoWeek_T,
end: BaseIsoWeek_T,
*,
step: int = 1,
inclusive: Literal["both", "left", "right", "neither"] = "both",
as_str: bool = True,
Expand Down Expand Up @@ -247,21 +293,24 @@ def range(
# ('2023-W01', '2023-W03', '2023-W05', '2023-W07')
```
"""

_start = cls._cast(start)
_end = cls._cast(end)

if _start > _end:
raise ValueError(f"`start` must be before `end` value, found: {_start} > {_end}")
msg = f"`start` must be before `end` value, found: {_start} > {_end}"
raise ValueError(msg)

if not isinstance(step, int):
raise TypeError(f"`step` must be integer, found {type(step)}")
msg = f"`step` must be integer, found {type(step)}"
raise TypeError(msg)

if step < 1:
raise ValueError(f"`step` value must be greater than or equal to 1, found {step}")
msg = f"`step` value must be greater than or equal to 1, found {step}"
raise ValueError(msg)

if inclusive not in _inclusive_values:
raise ValueError(f"Invalid `inclusive` value. Must be one of {_inclusive_values}")
msg = f"Invalid `inclusive` value. Must be one of {_inclusive_values}"
raise ValueError(msg)

_delta = _end - _start
range_start = 0 if inclusive in ("both", "left") else 1
Expand Down
Loading