Skip to content

Commit d3cf2da

Browse files
rtibblesbotclaude
andcommitted
Add Python 3.14 support
Replace deprecated datetime.utcnow() and datetime.utcfromtimestamp() with timezone-aware equivalents using datetime.now(tz=timezone.utc) and datetime.fromtimestamp(tz=timezone.utc) to avoid DeprecationWarnings on Python 3.14. Guard locale.getdefaultlocale() with hasattr check for its removal in Python 3.15+. Add monkey-patch for Django 3.2's BaseContext.__copy__ which breaks on Python 3.14 because super() objects no longer support __dict__ attribute setting. Guard the import with try/except ImportError so pip install succeeds before Django is installed. Update CI matrices, tox.ini, and setup.py to include Python 3.14 with upper bound extended to <3.15. Fixes #13823 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f4b587e commit d3cf2da

File tree

12 files changed

+86
-10
lines changed

12 files changed

+86
-10
lines changed

.github/workflows/morango_integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
INTEGRATION_TEST: 'true'
3131
strategy:
3232
matrix:
33-
python-version: [3.9, '3.10', '3.11', '3.12', '3.13']
33+
python-version: [3.9, '3.10', '3.11', '3.12', '3.13', '3.14']
3434

3535
steps:
3636
- name: Checkout repository

.github/workflows/pr_build_kolibri.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ jobs:
105105
needs: whl
106106
strategy:
107107
matrix:
108-
python-version: [3.9, '3.10', '3.11', '3.12', '3.13']
108+
python-version: [3.9, '3.10', '3.11', '3.12', '3.13', '3.14']
109109
cext: [true, false]
110110
runs-on: ubuntu-latest
111111
steps:

.github/workflows/tox.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
strategy:
3131
max-parallel: 5
3232
matrix:
33-
python-version: [3.9, '3.10', '3.11', '3.12', '3.13']
33+
python-version: [3.9, '3.10', '3.11', '3.12', '3.13', '3.14']
3434

3535
steps:
3636
- uses: actions/checkout@v6

kolibri/core/auth/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import time
33
from datetime import datetime
44
from datetime import timedelta
5+
from datetime import timezone
56
from itertools import groupby
67
from uuid import UUID
78
from uuid import uuid4
@@ -1358,7 +1359,7 @@ def get_session_response(self, request):
13581359
}
13591360
)
13601361

1361-
visitor_cookie_expiry = datetime.utcnow() + timedelta(days=365)
1362+
visitor_cookie_expiry = datetime.now(tz=timezone.utc) + timedelta(days=365)
13621363

13631364
if isinstance(user, AnonymousUser):
13641365
response = Response(session)

kolibri/core/tasks/test/taskrunner/test_scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_schedule_a_function_gives_value_error_repeat_zero_interval(
9393
def test_schedule_a_function_gives_value_error_not_timezone_aware_datetime(
9494
self, job_storage, job
9595
):
96-
now = datetime.datetime.utcnow()
96+
now = datetime.datetime.now()
9797
with pytest.raises(ValueError) as error:
9898
job_storage.schedule(now, job)
9999
assert "timezone aware datetime object" in str(error.value)

kolibri/plugins/facility/views.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
from datetime import datetime as dt
4+
from datetime import timezone
45

56
from django.core.exceptions import PermissionDenied
67
from django.core.files.storage import default_storage
@@ -82,7 +83,9 @@ def first_log_date(request, facility_id):
8283
.order_by("start_timestamp")
8384
.first()
8485
)
85-
first_log_date = first_log.start_timestamp if first_log is not None else dt.utcnow()
86+
first_log_date = (
87+
first_log.start_timestamp if first_log is not None else dt.now(tz=timezone.utc)
88+
)
8689
response = {
8790
"first_log_date": first_log_date.isoformat(),
8891
}

kolibri/utils/env.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,29 @@ def forward_port_cgi_module():
143143
sys.modules["cgi"] = module
144144

145145

146+
def monkey_patch_base_context():
147+
"""
148+
Monkey patch Django's BaseContext.__copy__ for Python 3.14 compatibility.
149+
In Python 3.14, super() objects no longer support __dict__ attribute setting,
150+
which breaks Django 3.2's BaseContext.__copy__ that does copy(super()).
151+
This can be removed when we upgrade to Django 4.2+.
152+
"""
153+
if sys.version_info < (3, 14):
154+
return
155+
try:
156+
from django.template.context import BaseContext
157+
except ImportError:
158+
return
159+
160+
def __copy__(self):
161+
duplicate = object.__new__(self.__class__)
162+
duplicate.__dict__.update(self.__dict__)
163+
duplicate.dicts = self.dicts[:]
164+
return duplicate
165+
166+
BaseContext.__copy__ = __copy__
167+
168+
146169
def set_env():
147170
"""
148171
Sets the Kolibri environment for the CLI or other application worker
@@ -166,6 +189,7 @@ def set_env():
166189

167190
# Depends on Django, so we need to wait until our dist has been registered.
168191
forward_port_cgi_module()
192+
monkey_patch_base_context()
169193

170194
# Set default env
171195
for key, value in ENVIRONMENT_VARIABLES.items():

kolibri/utils/i18n.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ def _get_language_info():
8080

8181

8282
def get_system_default_language():
83-
for loc in (locale.getlocale()[0], locale.getdefaultlocale()[0]):
83+
if hasattr(locale, "getdefaultlocale"):
84+
default_locale = locale.getdefaultlocale()[0]
85+
else:
86+
# locale.getdefaultlocale() removed in Python 3.15+
87+
default_locale = None
88+
for loc in (locale.getlocale()[0], default_locale):
8489
if loc:
8590
lang = to_language(loc)
8691
for lang_code in (lang, lang.split("-")[0]):

kolibri/utils/version.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ def get_git_changeset():
133133
# This does not fail if git is not available or current dir isn't a git
134134
# repo - it's safe.
135135
timestamp = git_log.communicate()[0]
136-
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
136+
timestamp = datetime.datetime.fromtimestamp(
137+
int(timestamp), tz=datetime.timezone.utc
138+
)
137139
# We have some issues because something normalizes separators to "."
138140
# From PEP440: With a local version, in addition to the use of . as a
139141
# separator of segments, the use of - and _ is also acceptable. The

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ def run(self):
100100
"Programming Language :: Python :: 3.11",
101101
"Programming Language :: Python :: 3.12",
102102
"Programming Language :: Python :: 3.13",
103+
"Programming Language :: Python :: 3.14",
103104
"Programming Language :: Python :: Implementation :: PyPy",
104105
],
105106
cmdclass={"install_scripts": gen_windows_batch_files},
106-
python_requires=">=3.6, <3.14",
107+
python_requires=">=3.6, <3.15",
107108
)

0 commit comments

Comments
 (0)