Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/pendulum/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,13 @@ def replace(
month: SupportsIndex | None = None,
day: SupportsIndex | None = None,
) -> Self:
# bool is a subclass of int; reject so year=True does not become year 1
for name, value in (("year", year), ("month", month), ("day", day)):
if isinstance(value, bool):
raise TypeError(
f"{name} must be an integer, not bool (got {value!r})."
)

year = year if year is not None else self.year
month = month if month is not None else self.month
day = day if day is not None else self.day
Expand Down
32 changes: 32 additions & 0 deletions src/pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ def set(
microsecond: int | None = None,
tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None,
) -> Self:
# bool is a subclass of int; reject so year=True does not become year 1
for name, value in (
("year", year),
("month", month),
("day", day),
("hour", hour),
("minute", minute),
("second", second),
("microsecond", microsecond),
):
if isinstance(value, bool):
raise TypeError(
f"{name} must be an integer, not bool (got {value!r})."
)

if year is None:
year = self.year
if month is None:
Expand Down Expand Up @@ -1299,6 +1314,23 @@ def replace(
tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
fold: int | None = None,
) -> Self:
# bool is a subclass of int; reject so year=True does not become year 1
# (tzinfo=True remains the sentinel for "keep current timezone")
for name, value in (
("year", year),
("month", month),
("day", day),
("hour", hour),
("minute", minute),
("second", second),
("microsecond", microsecond),
("fold", fold),
):
if isinstance(value, bool):
raise TypeError(
f"{name} must be an integer, not bool (got {value!r})."
)

if year is None:
year = self.year
if month is None:
Expand Down
17 changes: 17 additions & 0 deletions src/pendulum/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ def __new__(
years: float = 0,
months: float = 0,
) -> Self:
# bool is a subclass of int; reject so years=True does not become 1 year
for name, value in (
("days", days),
("seconds", seconds),
("microseconds", microseconds),
("milliseconds", milliseconds),
("minutes", minutes),
("hours", hours),
("weeks", weeks),
("years", years),
("months", months),
):
if isinstance(value, bool):
raise TypeError(
f"{name} must be a number, not bool (got {value!r})."
)

if not isinstance(years, int) or not isinstance(months, int):
raise ValueError("Float year and months are not supported")

Expand Down
16 changes: 16 additions & 0 deletions src/pendulum/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ def add_duration(
"""
Adds a duration to a date/datetime instance.
"""
# bool is a subclass of int; reject so days=True does not add 1 day
for name, value in (
("years", years),
("months", months),
("weeks", weeks),
("days", days),
("hours", hours),
("minutes", minutes),
("seconds", seconds),
("microseconds", microseconds),
):
if isinstance(value, bool):
raise TypeError(
f"{name} must be an integer, not bool (got {value!r})."
)

days += weeks * 7

if (
Expand Down
9 changes: 9 additions & 0 deletions tests/date/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,12 @@ def test_addition_invalid_type():

with pytest.raises(TypeError):
3 + d


def test_add_rejects_bool():
import pytest

d = pendulum.date(2020, 5, 15)
for key in ("years", "months", "weeks", "days"):
with pytest.raises(TypeError, match=key):
d.add(**{key: True})
11 changes: 11 additions & 0 deletions tests/date/test_fluent_setters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import pytest

import pendulum

from tests.conftest import assert_date
Expand Down Expand Up @@ -27,3 +29,12 @@ def test_fluid_day_setter():

assert new.day == 9
assert d.day == 2


def test_replace_rejects_bool():
d = pendulum.date(2020, 5, 15)
for key in ("year", "month", "day"):
with pytest.raises(TypeError, match=key):
d.replace(**{key: True})
with pytest.raises(TypeError, match=key):
d.replace(**{key: False})
9 changes: 9 additions & 0 deletions tests/datetime/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,12 @@ def test_interval_over_midnight_tz():
new_end = start + interval

assert new_end == end


def test_add_rejects_bool():
dt = pendulum.datetime(2020, 5, 15, 12, 30, 45)
for key in ("years", "months", "weeks", "days", "hours", "minutes", "seconds", "microseconds"):
with pytest.raises(TypeError, match=key):
dt.add(**{key: True})
with pytest.raises(TypeError, match=key):
dt.add(**{key: False})
11 changes: 11 additions & 0 deletions tests/datetime/test_fluent_setters.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,14 @@ def test_replace_tzinfo_dst_transitioning_off():
assert new.is_dst()
assert new.offset == 7200
assert new.timezone_name == "Europe/Paris"


def test_set_rejects_bool():
import pytest

dt = pendulum.datetime(2020, 5, 15, 12, 30, 45)
for key in ("year", "month", "day", "hour", "minute", "second", "microsecond"):
with pytest.raises(TypeError, match=key):
dt.set(**{key: True})
with pytest.raises(TypeError, match=key):
dt.set(**{key: False})
18 changes: 18 additions & 0 deletions tests/datetime/test_replace.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import pytest

import pendulum

from tests.conftest import assert_datetime
Expand Down Expand Up @@ -59,3 +61,19 @@ def test_replace_tzinfo_dst_transitioning_off():
assert not in_paris.is_dst()
assert in_paris.offset == 3600
assert in_paris.timezone_name == "Europe/Paris"


def test_replace_rejects_bool():
dt = pendulum.datetime(2020, 5, 15, 12, 30, 45)
for key in ("year", "month", "day", "hour", "minute", "second", "microsecond", "fold"):
with pytest.raises(TypeError, match=key):
dt.replace(**{key: True})
with pytest.raises(TypeError, match=key):
dt.replace(**{key: False})


def test_replace_tzinfo_true_still_allowed():
# tzinfo=True is the documented sentinel for keeping the current timezone
dt = pendulum.datetime(2020, 5, 15, 12, 30, 45, tz="UTC")
replaced = dt.replace(tzinfo=True)
assert replaced.timezone_name == dt.timezone_name
18 changes: 18 additions & 0 deletions tests/duration/test_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,21 @@ def test_float_years_and_months():

with pytest.raises(ValueError):
pendulum.duration(months=1.5)


def test_duration_rejects_bool():
for key in (
"days",
"seconds",
"microseconds",
"milliseconds",
"minutes",
"hours",
"weeks",
"years",
"months",
):
with pytest.raises(TypeError, match=key):
pendulum.duration(**{key: True})
with pytest.raises(TypeError, match=key):
pendulum.duration(**{key: False})