Skip to content

Commit ba2cc71

Browse files
Copilotborchero
andauthored
fix: Properly support Python 3.14 (#227)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: borchero <[email protected]> Co-authored-by: Oliver Borchert <[email protected]>
1 parent c68133a commit ba2cc71

File tree

2 files changed

+41
-1
lines changed

2 files changed

+41
-1
lines changed

dataframely/collection/_base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,11 @@ def _get_metadata(source: dict[str, Any]) -> Metadata:
202202
annotations = source["__annotations__"]
203203
elif sys.version_info >= (3, 14):
204204
if "__annotate_func__" in source:
205-
annotations = source["__annotate_func__"](Format.VALUE)
205+
annotate_func = source["__annotate_func__"]
206+
# __annotate_func__ can be None in Python 3.14 when a class
207+
# has no annotations or in certain metaclass scenarios
208+
if annotate_func is not None and callable(annotate_func):
209+
annotations = annotate_func(Format.VALUE)
206210
for attr, kls in annotations.items():
207211
result.members[attr] = CollectionMeta._derive_member_info(
208212
attr, kls, CollectionMember()

tests/collection/test_base.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright (c) QuantCo 2025-2025
22
# SPDX-License-Identifier: BSD-3-Clause
33

4+
import sys
5+
46
import polars as pl
57
import pytest
68
from polars.testing import assert_frame_equal
@@ -110,3 +112,37 @@ def test_collect_all_optional() -> None:
110112
assert isinstance(out, MyCollection)
111113
assert len(out.first.collect()) == 3
112114
assert out.second is None
115+
116+
117+
@pytest.mark.skipif(sys.version_info < (3, 14), reason="Python 3.14+ only")
118+
def test_annotate_func_none_py314() -> None:
119+
"""Test that __annotate_func__ = None doesn't cause TypeError in Python 3.14.
120+
121+
In Python 3.14 with PEP 649, __annotate_func__ can be None when:
122+
- A class has no annotations
123+
- Annotations are being processed during certain import contexts
124+
- Classes are created dynamically with __annotate_func__ set to None
125+
126+
This test ensures the metaclass handles this gracefully.
127+
"""
128+
from typing import cast
129+
130+
from dataframely.collection._base import BaseCollection, CollectionMeta
131+
132+
# Create a namespace with __annotate_func__ = None
133+
namespace = {
134+
"__module__": "__main__",
135+
"__qualname__": "TestCollection",
136+
"__annotate_func__": None,
137+
}
138+
139+
# This should not raise TypeError
140+
TestCollection = CollectionMeta(
141+
"TestCollection",
142+
(dy.Collection,),
143+
namespace,
144+
)
145+
146+
# Verify it has no members (since there are no annotations)
147+
# Cast to BaseCollection to satisfy mypy since CollectionMeta creates Collection classes
148+
assert cast(type[BaseCollection], TestCollection).members() == {}

0 commit comments

Comments
 (0)