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
2 changes: 1 addition & 1 deletion .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9"
python-version: "3.10"

- name: Get full Python version
id: full-python-version
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ jobs:
platform: linux
- os: windows
ls: dir
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10
interpreter: 3.10 3.11 3.12 3.13 3.14 pypy3.10
- os: windows
ls: dir
target: aarch64
interpreter: 3.11 3.12 3.13 3.14
- os: macos
target: aarch64
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10
interpreter: 3.10 3.11 3.12 3.13 3.14 pypy3.10
- os: ubuntu
platform: linux
target: aarch64
Expand All @@ -44,11 +44,11 @@ jobs:
- os: ubuntu
platform: linux
target: ppc64le
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
interpreter: 3.10 3.11 3.12 3.13 3.14
- os: ubuntu
platform: linux
target: s390x
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
interpreter: 3.10 3.11 3.12 3.13 3.14

runs-on: ${{ matrix.os }}-latest
steps:
Expand All @@ -66,7 +66,7 @@ jobs:
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
container: ${{ matrix.container }}
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10' }} ${{ matrix.extra-build-args }}
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.10 3.11 3.12 3.13 pypy3.10' }} ${{ matrix.extra-build-args }}
rust-toolchain: stable
docker-options: -e CI

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
strategy:
matrix:
os: [Ubuntu, MacOS, Windows]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
defaults:
run:
shell: bash
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Change Log

- Dropped support for Python 3.9 [#930](https://github.com/python-pendulum/pendulum/pull/930)

## [3.1.0] - 2025-04-19

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Pendulum

Python datetimes made easy.

Supports Python **3.9 and newer**.
Supports Python **3.10 and newer**.


.. code-block:: python
Expand Down
2 changes: 1 addition & 1 deletion clock
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ translations = {{}}
for k, v in d.items():
v = (
self.format_dict(v, tab + 1)
if isinstance(v, (dict, LocaleDataDict))
if isinstance(v, dict | LocaleDataDict)
else repr(v)
)

Expand Down
209 changes: 90 additions & 119 deletions poetry.lock

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ name = "pendulum"
version = "3.2.0.dev0"
description = "Python datetimes made easy"
readme = "README.rst"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = { text = "MIT License" }
authors = [{ name = "Sébastien Eustace", email = "[email protected]" }]
keywords = ['datetime', 'date', 'time']
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -24,7 +23,7 @@ dependencies = [

[project.optional-dependencies]
test = [
'time-machine>=2.6.0; implementation_name != "pypy"',
'time-machine>=3.0.0; implementation_name != "pypy"',
]

[project.urls]
Expand All @@ -35,7 +34,7 @@ Repository = "https://github.com/sdispater/pendulum"

[tool.poetry.group.test.dependencies]
pytest = "^7.1.2"
time-machine = ">=2.16.0"
time-machine = ">=3.0.0"
pytest-benchmark = "^4.0.0"

[tool.poetry.group.doc.dependencies]
Expand Down Expand Up @@ -72,7 +71,7 @@ include = [
[tool.ruff]
fix = true
line-length = 88
target-version = "py39"
target-version = "py310"
extend-exclude = [
# External to the project's coding standards:
"docs/*",
Expand Down
9 changes: 4 additions & 5 deletions src/pendulum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import datetime as _datetime

from typing import Any
from typing import Union
from typing import cast
from typing import overload

Expand Down Expand Up @@ -91,13 +90,13 @@ def _safe_timezone(
Creates a timezone instance
from a string, Timezone, TimezoneInfo or integer offset.
"""
if isinstance(obj, (Timezone, FixedTimezone)):
if isinstance(obj, Timezone | FixedTimezone):
return obj

if obj is None or obj == "local":
return local_timezone()

if isinstance(obj, (int, float)):
if isinstance(obj, int | float):
obj = int(obj * 60 * 60)
elif isinstance(obj, _datetime.tzinfo):
# zoneinfo
Expand All @@ -116,7 +115,7 @@ def _safe_timezone(

obj = int(offset.total_seconds())

obj = cast("Union[str, int]", obj)
obj = cast("str | int", obj)

return timezone(obj)

Expand Down Expand Up @@ -226,7 +225,7 @@ def instance(
"""
Create a DateTime/Date/Time instance from a datetime/date/time native one.
"""
if isinstance(obj, (DateTime, Date, Time)):
if isinstance(obj, DateTime | Date | Time):
return obj

if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
Expand Down
10 changes: 5 additions & 5 deletions src/pendulum/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
from typing import ClassVar
from typing import Optional
from typing import cast
from typing import overload

Expand Down Expand Up @@ -42,7 +40,9 @@


if TYPE_CHECKING:
from typing_extensions import Literal
from collections.abc import Callable
from typing import Literal

from typing_extensions import Self
from typing_extensions import SupportsIndex

Expand Down Expand Up @@ -268,7 +268,7 @@ def offset_hours(self) -> float | None:

@property
def timezone(self) -> Timezone | FixedTimezone | None:
if not isinstance(self.tzinfo, (Timezone, FixedTimezone)):
if not isinstance(self.tzinfo, Timezone | FixedTimezone):
return None

return self.tzinfo
Expand Down Expand Up @@ -1006,7 +1006,7 @@ def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self:
if unit not in ["month", "quarter", "year"]:
raise ValueError(f'Invalid unit "{unit}" for first_of()')

dt = cast("Optional[Self]", getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
dt = cast("Self | None", getattr(self, f"_nth_of_{unit}")(nth, day_of_week))
if not dt:
raise PendulumException(
f"Unable to find occurrence {nth}"
Expand Down
4 changes: 2 additions & 2 deletions src/pendulum/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ def __floordiv__(self, other: timedelta) -> int: ...
def __floordiv__(self, other: int) -> Self: ...

def __floordiv__(self, other: int | timedelta) -> int | Duration:
if not isinstance(other, (int, timedelta)):
if not isinstance(other, int | timedelta):
return NotImplemented

usec = self._to_microseconds()
Expand All @@ -407,7 +407,7 @@ def __truediv__(self, other: timedelta) -> float: ...
def __truediv__(self, other: float) -> Self: ...

def __truediv__(self, other: int | float | timedelta) -> Self | float:
if not isinstance(other, (int, float, timedelta)):
if not isinstance(other, int | float | timedelta):
return NotImplemented

usec = self._to_microseconds()
Expand Down
2 changes: 1 addition & 1 deletion src/pendulum/formatting/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from re import Match
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
from typing import ClassVar
from typing import cast

Expand All @@ -16,6 +15,7 @@


if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Sequence

from pendulum import Timezone
Expand Down
3 changes: 1 addition & 2 deletions src/pendulum/parsing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from datetime import datetime
from datetime import time
from typing import Any
from typing import Optional
from typing import cast

from dateutil import parser
Expand Down Expand Up @@ -90,7 +89,7 @@ def _normalize(
return parsed

if isinstance(parsed, time):
now = cast("Optional[datetime]", options["now"]) or datetime.now()
now = cast("datetime | None", options["now"]) or datetime.now()

return datetime(
now.year,
Expand Down
20 changes: 10 additions & 10 deletions src/pendulum/testing/traveller.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ def __init__(self, datetime_class: type[DateTime] = DateTime) -> None:
super().__init__(datetime_class)

self._started: bool = False
self._traveller: time_machine.travel | None = None
self._coordinates: time_machine.Coordinates | None = None
self._travel: time_machine.travel | None = None
self._traveller: time_machine.Traveller | None = None

def freeze(self) -> Self:
if self._started:
cast("time_machine.Coordinates", self._coordinates).move_to(
cast("time_machine.Traveller", self._traveller).move_to(
self._datetime_class.now(), tick=False
)
else:
Expand All @@ -83,9 +83,9 @@ def travel_back(self) -> Self:
if not self._started:
return self

cast("time_machine.travel", self._traveller).stop()
self._coordinates = None
cast("time_machine.travel", self._travel).stop()
self._traveller = None
self._travel = None
self._started = False

return self
Expand All @@ -105,7 +105,7 @@ def travel(
) -> Self:
self._start(freeze=freeze)

cast("time_machine.Coordinates", self._coordinates).move_to(
cast("time_machine.Traveller", self._traveller).move_to(
self._datetime_class.now().add(
years=years,
months=months,
Expand All @@ -123,20 +123,20 @@ def travel(
def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self:
self._start(freeze=freeze)

cast("time_machine.Coordinates", self._coordinates).move_to(dt)
cast("time_machine.Traveller", self._traveller).move_to(dt)

return self

def _start(self, freeze: bool = False) -> None:
if self._started:
return

if not self._traveller:
self._traveller = time_machine.travel(
if not self._travel:
self._travel = time_machine.travel(
self._datetime_class.now(), tick=not freeze
)

self._coordinates = self._traveller.start()
self._traveller = self._travel.start()

self._started = True

Expand Down
10 changes: 5 additions & 5 deletions src/pendulum/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from datetime import time
from datetime import timedelta
from typing import TYPE_CHECKING
from typing import Optional
from typing import cast
from typing import overload

Expand All @@ -21,7 +20,8 @@


if TYPE_CHECKING:
from typing_extensions import Literal
from typing import Literal

from typing_extensions import Self
from typing_extensions import SupportsIndex

Expand Down Expand Up @@ -174,7 +174,7 @@ def __sub__(self, other: time) -> pendulum.Duration: ...
def __sub__(self, other: datetime.timedelta) -> Time: ...

def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
if not isinstance(other, (Time, time, timedelta)):
if not isinstance(other, Time | time | timedelta):
return NotImplemented

if isinstance(other, timedelta):
Expand All @@ -197,7 +197,7 @@ def __rsub__(self, other: time) -> pendulum.Duration: ...
def __rsub__(self, other: datetime.timedelta) -> Time: ...

def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
if not isinstance(other, (Time, time)):
if not isinstance(other, Time | time):
return NotImplemented

if isinstance(other, time):
Expand Down Expand Up @@ -284,7 +284,7 @@ def replace(
minute,
second,
microsecond,
tzinfo=cast("Optional[datetime.tzinfo]", tzinfo),
tzinfo=cast("datetime.tzinfo | None", tzinfo),
fold=fold,
)
return self.__class__(
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
isolated_build = true
envlist = py37, py38, py39, py310, pypy3
envlist = py310, py311, py312, py313, pypy3

[testenv]
whitelist_externals = poetry
Expand Down