Skip to content
Closed
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
3 changes: 1 addition & 2 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
import time
from datetime import datetime
from datetime import timedelta
from itertools import groupby
from uuid import UUID
Expand Down Expand Up @@ -1358,7 +1357,7 @@ def get_session_response(self, request):
}
)

visitor_cookie_expiry = datetime.utcnow() + timedelta(days=365)
visitor_cookie_expiry = now() + timedelta(days=365)

if isinstance(user, AnonymousUser):
response = Response(session)
Expand Down
12 changes: 8 additions & 4 deletions kolibri/core/content/test/test_content_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,13 +491,17 @@ def test_contentnode_list_long(self):

def _recurse_and_assert(self, data, nodes, recursion_depth=0):
recursion_depths = []
for actual, expected in zip(data, nodes):
nodes_by_id = {n.id: n for n in nodes}
for actual in data:
expected = nodes_by_id[actual["id"]]
children = actual.pop("children", None)
self._assert_node(actual, expected)
if children:
child_nodes = content.ContentNode.objects.filter(
available=True, parent=expected
).order_by("lft")
child_nodes = list(
content.ContentNode.objects.filter(
available=True, parent=expected
).order_by("lft")
)
if children["more"] is None:
self.assertEqual(len(child_nodes), len(children["results"]))
else:
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/tasks/test/taskrunner/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def test_schedule_a_function_gives_value_error_repeat_zero_interval(
def test_schedule_a_function_gives_value_error_not_timezone_aware_datetime(
self, job_storage, job
):
now = datetime.datetime.utcnow()
now = datetime.datetime.now()
with pytest.raises(ValueError) as error:
job_storage.schedule(now, job)
assert "timezone aware datetime object" in str(error.value)
Expand Down
4 changes: 2 additions & 2 deletions kolibri/plugins/facility/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
import logging
from datetime import datetime as dt

from django.core.exceptions import PermissionDenied
from django.core.files.storage import default_storage
Expand All @@ -27,6 +26,7 @@
)
from kolibri.core.logger.models import ContentSessionLog
from kolibri.core.logger.models import GenerateCSVLogRequest
from kolibri.utils.time_utils import utc_now

CSV_EXPORT_FILENAMES = {}
CSV_EXPORT_FILENAMES.update(LOGGER_CSV_EXPORT_FILENAMES)
Expand Down Expand Up @@ -82,7 +82,7 @@ def first_log_date(request, facility_id):
.order_by("start_timestamp")
.first()
)
first_log_date = first_log.start_timestamp if first_log is not None else dt.utcnow()
first_log_date = first_log.start_timestamp if first_log is not None else utc_now()
response = {
"first_log_date": first_log_date.isoformat(),
}
Expand Down
24 changes: 24 additions & 0 deletions kolibri/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,29 @@ def forward_port_cgi_module():
sys.modules["cgi"] = module


def monkey_patch_base_context():
"""
Monkey patch Django's BaseContext.__copy__ for Python 3.14 compatibility.
In Python 3.14, super() objects no longer support __dict__ attribute setting,
which breaks Django 3.2's BaseContext.__copy__ that does copy(super()).
This can be removed when we upgrade to Django 4.2+.
"""
if sys.version_info < (3, 14):
return
try:
from django.template.context import BaseContext
except ImportError:
return

def __copy__(self):
duplicate = object.__new__(self.__class__)
duplicate.__dict__.update(self.__dict__)
duplicate.dicts = self.dicts[:]
return duplicate

BaseContext.__copy__ = __copy__


def set_env():
"""
Sets the Kolibri environment for the CLI or other application worker
Expand All @@ -166,6 +189,7 @@ def set_env():

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

# Set default env
for key, value in ENVIRONMENT_VARIABLES.items():
Expand Down
7 changes: 6 additions & 1 deletion kolibri/utils/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ def _get_language_info():


def get_system_default_language():
for loc in (locale.getlocale()[0], locale.getdefaultlocale()[0]):
if hasattr(locale, "getdefaultlocale"):
default_locale = locale.getdefaultlocale()[0]
else:
# locale.getdefaultlocale() removed in Python 3.15+
default_locale = None
for loc in (locale.getlocale()[0], default_locale):
if loc:
lang = to_language(loc)
for lang_code in (lang, lang.split("-")[0]):
Expand Down
4 changes: 3 additions & 1 deletion kolibri/utils/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ def get_git_changeset():
# This does not fail if git is not available or current dir isn't a git
# repo - it's safe.
timestamp = git_log.communicate()[0]
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
timestamp = datetime.datetime.fromtimestamp(
int(timestamp), tz=datetime.timezone.utc
)
# We have some issues because something normalizes separators to "."
# From PEP440: With a local version, in addition to the use of . as a
# separator of segments, the use of - and _ is also acceptable. The
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ def run(self):
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: PyPy",
],
cmdclass={"install_scripts": gen_windows_batch_files},
python_requires=">=3.6, <3.14",
python_requires=">=3.6, <3.15",
)
41 changes: 41 additions & 0 deletions test/test_env_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from copy import copy

from kolibri.utils.env import forward_port_cgi_module
from kolibri.utils.env import monkey_patch_base_context


def test_base_context_copy_works_after_monkey_patch():
"""
Verify that Django's BaseContext.__copy__ works after monkey-patching.
On Python 3.14+, the original Django 3.2 implementation fails because
super() objects no longer support __dict__ attribute setting.
"""
forward_port_cgi_module()
monkey_patch_base_context()

from django.template.context import BaseContext

ctx = BaseContext({"foo": "bar"})
ctx_copy = copy(ctx)
assert ctx_copy.dicts == ctx.dicts
assert ctx_copy.dicts is not ctx.dicts # Must be a shallow copy, not same list


def test_request_context_copy_works_after_monkey_patch():
"""
Verify that RequestContext (which requires a request argument in __init__)
can be copied after monkey-patching.
"""
forward_port_cgi_module()
monkey_patch_base_context()

from django.template.context import RequestContext
from django.test import RequestFactory

factory = RequestFactory()
request = factory.get("/")
ctx = RequestContext(request, {"foo": "bar"})
ctx_copy = copy(ctx)
assert ctx_copy.dicts == ctx.dicts
assert ctx_copy.dicts is not ctx.dicts
assert ctx_copy.request is request # __dict__ attributes must survive copy
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py{3.6,3.7,3.8,3.9,3.10,3.11,3.12,3.13}, postgres
envlist = py{3.6,3.7,3.8,3.9,3.10,3.11,3.12,3.13,3.14}, postgres

[testenv]
usedevelop = True
Expand All @@ -22,6 +22,7 @@ basepython =
py3.11: python3.11
py3.12: python3.12
py3.13: python3.13
py3.14: python3.14
deps =
-r{toxinidir}/requirements/test.txt
-r{toxinidir}/requirements/base.txt
Expand Down
Loading