Skip to content

Commit 26ae383

Browse files
authored
Add support for Python 3.14 (#1962)
### Summary This PR enables compatibility with the latest Python 3.14, while dropping support for the old Python 3.10. Removing 3.10 was necessary because there's no numpy version which supports 3.10 and 3.14 at the same time. This PR also upgrades some dependency packages to a version compatible with 3.14; for breaking changes (e.g. in `shapely`) I have updated the source code. CI was also updated to test both the minimum (3.11) and maximum (3.14) supported versions. Resolves #1960 ### Checklist - [x] I have added tests to cover my changes or documented any manual tests. - [x] I have updated the [documentation](https://github.com/open-edge-platform/datumaro/tree/develop/docs) accordingly --------- Signed-off-by: Leonardo Lai <[email protected]>
1 parent 2c711b5 commit 26ae383

File tree

17 files changed

+759
-815
lines changed

17 files changed

+759
-815
lines changed

.github/workflows/linter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
persist-credentials: false
2929
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
3030
with:
31-
python-version: "3.10"
31+
python-version: "3.11"
3232

3333
# Install pre-commit
3434
- name: Install pre-commit

.github/workflows/pr_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
fail-fast: false
3232
matrix:
3333
os: ["ubuntu-24.04", "windows-2022", "macos-15"]
34-
python-version: ["3.10", "3.13"]
34+
python-version: ["3.11", "3.14"]
3535
name: pr test (${{ matrix.os }}, Python ${{ matrix.python-version }})
3636
runs-on: ${{ matrix.os }}
3737
steps:

.github/workflows/publish_to_pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ jobs:
5353
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
5454
with:
5555
persist-credentials: false
56-
- name: Set up Python 3.10
56+
- name: Set up Python
5757
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
5858
with:
59-
python-version: "3.10"
59+
python-version: "3.14"
6060
- name: Install pypa/build
6161
run: python -m pip install build
6262
- name: Build sdist

pyproject.toml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,31 @@ maintainers = [
3131
]
3232
license-files = ["LICENSE"]
3333
readme = "README.md"
34-
requires-python = ">=3.10,<3.14"
34+
requires-python = ">=3.11,<=3.14"
3535
classifiers = [
3636
"Programming Language :: Python :: 3",
37-
"Programming Language :: Python :: 3.10",
3837
"Programming Language :: Python :: 3.11",
3938
"Programming Language :: Python :: 3.12",
4039
"Programming Language :: Python :: 3.13",
40+
"Programming Language :: Python :: 3.14",
4141
"Operating System :: OS Independent",
4242
]
4343
dependencies = [
4444
"attrs>=21.3.0",
4545
"defusedxml>=0.7.0",
4646
"imagesize>=1.4.1",
4747
"lxml<7,>=5.2.0",
48-
"numpy<2.3",
48+
"numpy>=2.3.5",
4949
"orjson>=3.10.0",
5050
"Pillow>=10.3.0",
5151
"ruamel.yaml>=0.17.0",
52-
"shapely>=1.7",
52+
"shapely>=2.1.2",
5353
"typing_extensions>=3.7.4.3",
5454
"tqdm>=4.67.1",
5555
"pycocotools>=2.0.4",
5656
"PyYAML==6.0.3",
57-
"pandas>=1.4.0",
58-
"pyarrow",
57+
"pandas>=2.3.3",
58+
"pyarrow>=22.0",
5959
"json-stream",
6060
"opencv-python-headless>=4.11.0.86",
6161
]
@@ -97,7 +97,7 @@ tf = [
9797
#]
9898

9999
# PyTorch support
100-
torch = ["torch", "torchvision"]
100+
torch = ["torch>=2.9", "torchvision>=0.24"]
101101

102102
# Kaggle download support
103103
kaggle = ["kaggle"]
@@ -119,15 +119,15 @@ cli = ["tensorboardX>=1.8,!=2.3", "tabulate", "scipy", "matplotlib>=3.3.1"]
119119
visualizer = ["matplotlib>=3.3.1"]
120120

121121
# NYU Depth Dataset v2 file format
122-
h5py = ["h5py>=2.10.0"]
122+
h5py = ["h5py>=3.15.0"]
123123

124124
# BraTS file format
125125
nibabel = ["nibabel>=3.2.1"]
126126

127127
# AVA dataset
128128
protobuf = ["protobuf"]
129129

130-
experimental = ["polars>=1.31.0,<2.0.0"]
130+
experimental = ["polars~=1.35"]
131131

132132
[tool.coverage.run]
133133
branch = true

src/datumaro/components/annotation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
import attr
1414
import cv2
1515
import numpy as np
16-
import shapely.geometry as sg
1716
from attr import asdict, attrs, field
17+
from shapely import Polygon as ShapelyPolygon
1818
from typing_extensions import Literal
1919

2020
from datumaro.components.media import Image
@@ -899,8 +899,8 @@ def __eq__(self, other):
899899

900900
self_points = self.get_points()
901901
other_points = other.get_points()
902-
self_polygon = sg.Polygon(self_points)
903-
other_polygon = sg.Polygon(other_points)
902+
self_polygon = ShapelyPolygon(self_points)
903+
other_polygon = ShapelyPolygon(other_points)
904904
# if polygon is not valid, compare points
905905
if not (self_polygon.is_valid and other_polygon.is_valid):
906906
return self_points == other_points

src/datumaro/experimental/converters/annotation_converters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ def convert(self, df: pl.DataFrame) -> pl.DataFrame:
9393

9494
if field.multi_label or field.is_list:
9595
mapping_expr = pl.col(input_col).list.eval(
96-
pl.element().replace(list(index_mapping.keys()), list(index_mapping.values()), default=None)
96+
pl.element().replace_strict(list(index_mapping.keys()), list(index_mapping.values()), default=None)
9797
)
9898
else:
99-
mapping_expr = pl.col(input_col).replace(
99+
mapping_expr = pl.col(input_col).replace_strict(
100100
list(index_mapping.keys()), list(index_mapping.values()), default=None
101101
)
102102

src/datumaro/experimental/tiling/tilers.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
import numpy as np
1313
import polars as pl
14-
import shapely.geometry as sg
15-
import shapely.ops as so
14+
from shapely import GeometryCollection, MultiPolygon, box, transform
15+
from shapely import Polygon as ShapelyPolygon
16+
from shapely.geometry.base import BaseGeometry
1617

1718
from datumaro.experimental.fields import (
1819
BBoxField,
@@ -326,9 +327,9 @@ def extract_tile(
326327
return pl.DataFrame(results)
327328

328329

329-
def _apply_offset(geom: sg.base.BaseGeometry, offset_x: float, offset_y: float) -> sg.base.BaseGeometry:
330+
def _apply_offset(geom: BaseGeometry, offset_x: float, offset_y: float) -> BaseGeometry:
330331
"""Apply offset to geometry."""
331-
return so.transform(lambda x, y: (x - offset_x, y - offset_y), geom)
332+
return transform(geometry=geom, transformation=lambda x, y: (x - offset_x, y - offset_y), interleaved=False)
332333

333334

334335
@TilerRegistry.register(PolygonField)
@@ -366,7 +367,7 @@ def tile(self, df: pl.DataFrame, tiles_df: pl.DataFrame, slice_offset: int = 0)
366367
source_polygons = df[source_idx, column_name]
367368

368369
# Create tile polygon
369-
tile_poly = sg.box(
370+
tile_poly = box(
370371
tile_row["x"],
371372
tile_row["y"],
372373
tile_row["x"] + tile_row["width"],
@@ -378,13 +379,13 @@ def tile(self, df: pl.DataFrame, tiles_df: pl.DataFrame, slice_offset: int = 0)
378379
polygon_keeps = [] # Track which polygons to keep
379380

380381
for poly_coords in source_polygons:
381-
polygon = sg.Polygon(poly_coords)
382+
polygon = ShapelyPolygon(poly_coords)
382383

383384
# Get intersection and apply offset
384385
intersection = polygon.intersection(tile_poly)
385386

386387
# NOTE: intersection may return a GeometryCollection or MultiPolygon
387-
if isinstance(intersection, sg.GeometryCollection | sg.MultiPolygon):
388+
if isinstance(intersection, GeometryCollection | MultiPolygon):
388389
shapes = [(geom, geom.area) for geom in list(intersection.geoms) if geom.is_valid]
389390
if not shapes:
390391
tiled_polygons.append(None) # Placeholder for dropped polygon
@@ -393,7 +394,7 @@ def tile(self, df: pl.DataFrame, tiles_df: pl.DataFrame, slice_offset: int = 0)
393394

394395
intersection, _ = max(shapes, key=operator.itemgetter(1))
395396

396-
if not isinstance(intersection, sg.Polygon) or intersection.is_empty or not intersection.is_valid:
397+
if not isinstance(intersection, ShapelyPolygon) or intersection.is_empty or not intersection.is_valid:
397398
tiled_polygons.append(None) # Placeholder for dropped polygon
398399
polygon_keeps.append(False)
399400
continue

src/datumaro/plugins/data_formats/kaggle/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ def _parse_annotations(self, img_file: str, ann_file: str):
425425
label_name = self._parse_field(object_elem, "name", str, required=True)
426426

427427
bbox_elem = object_elem.find("bndbox")
428-
if not bbox_elem:
428+
if bbox_elem is None:
429429
raise MissingFieldError("bndbox")
430430

431431
xmin = self._parse_field(bbox_elem, "xmin", float)

src/datumaro/plugins/data_formats/mpii/mpii_mat.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ def _load_items(self, path):
5151
group_num = 1
5252

5353
image = getattr(item, "image", "")
54-
if isinstance(image, spio.matlab.mio5_params.mat_struct):
54+
if isinstance(image, spio.matlab.mat_struct):
5555
image = getattr(image, "name", "")
5656

5757
anno_values = getattr(item, "annorect", [])
58-
if isinstance(anno_values, spio.matlab.mio5_params.mat_struct):
58+
if isinstance(anno_values, spio.matlab.mat_struct):
5959
anno_values = [anno_values]
6060

6161
for val in anno_values:
@@ -72,12 +72,12 @@ def _load_items(self, path):
7272
attributes["scale"] = scale
7373

7474
objpos = getattr(val, "objpos", None)
75-
if isinstance(objpos, spio.matlab.mio5_params.mat_struct):
75+
if isinstance(objpos, spio.matlab.mat_struct):
7676
attributes["center"] = [getattr(objpos, "x", 0), getattr(objpos, "y", 0)]
7777

7878
annopoints = getattr(val, "annopoints", None)
79-
if isinstance(annopoints, spio.matlab.mio5_params.mat_struct) and not isinstance(
80-
getattr(annopoints, "point"), spio.matlab.mio5_params.mat_struct
79+
if isinstance(annopoints, spio.matlab.mat_struct) and not isinstance(
80+
getattr(annopoints, "point"), spio.matlab.mat_struct
8181
):
8282
for point in getattr(annopoints, "point"):
8383
point_id = getattr(point, "id")

src/datumaro/plugins/data_formats/voc/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def _parse_attribute(self, object_elem):
250250
@classmethod
251251
def _parse_bbox(cls, object_elem):
252252
bbox_elem = object_elem.find("bndbox")
253-
if not bbox_elem:
253+
if bbox_elem is None:
254254
raise MissingFieldError("bndbox")
255255

256256
xmin = cls._parse_field(bbox_elem, "xmin", float)

0 commit comments

Comments
 (0)