pandas: FTBFS building against python 3.15
Maximiliano Curia <[email protected]>
| Newsgroups | gmane.linux.debian.devel.python |
|---|---|
| Message-ID | <[email protected]> |
Package: src:pandas Version: 3.0.5+dfsg-1 User: [email protected] Usertags: python3.15 Tags: patch Hi! While rebuilding the Python related packages against the Python 3.15rc1 version we found that pandas fails to build from source[1]. The upstream code already has fixes for these issues and since there are many packages that require pandas to build I already applied these fixes in the packages that are in the rebuild sandbox[2]. Please consider applying this patches in your next upload, or consider updating pandas when a new release including these patches is available. Happy hacking, [1]: https://debusine.debian.net/debian/r-python-python3.15/work-request/1012982/ [2]: https://debusine.debian.net/debian/r-python-python3.15/ -- "recursividad 95, 154, 156, 201, 224, 293" -- El Lenguaje de Programacion C, pag. 293 (Kernighan & Ritchie) Saludos /\/\ /\ >< `/
python3p15_compat.patch
(text/x-diff, 17.5 KB)
Description: Python 3.15 compatibility fixes
Backport of upstream commit 7bb284ac43 (PR #66473) for Python 3.15 support:
- Support optional %z and %:z (colon_z) in strptime
- Explicitly close XML iterparse parser to avoid unclosed iterator ResourceWarning
- Accept Python 3.15 TypeError / ValueError error message changes in tests
- Define and export PY315 in pandas.compat
Origin: upstream, https://github.com/pandas-dev/pandas/commit/7bb284ac438d2b981a7f42e3bdd0fe5ee26aada0
Bug: https://github.com/pandas-dev/pandas/issues/66473
Forwarded: not-needed
Index: pandas/pandas/_libs/tslibs/strptime.pyx
===================================================================
--- pandas.orig/pandas/_libs/tslibs/strptime.pyx
+++ pandas/pandas/_libs/tslibs/strptime.pyx
@@ -167,6 +167,7 @@ cdef dict _parse_code_table = {"y": 0,
"Z": 17,
"p": 18, # an additional key, only with I
"z": 19,
+ "colon_z": 24,
"G": 20,
"V": 21,
"u": 22}
@@ -178,7 +179,7 @@ cdef _validate_fmt(str fmt):
raise ValueError("Cannot use '%W' or '%U' without day and year")
if "%A" not in fmt and "%a" not in fmt and "%w" not in fmt:
raise ValueError("Cannot use '%W' or '%U' without day and year")
- elif "%Z" in fmt and "%z" in fmt:
+ elif "%Z" in fmt and ("%z" in fmt or "%:z" in fmt):
raise ValueError("Cannot parse both %Z and %z")
elif "%j" in fmt and "%G" in fmt:
raise ValueError("Day of the year directive '%j' is not "
@@ -618,9 +619,19 @@ cdef tzinfo _parse_with_format(
f"time data \"{val}\" doesn't match format \"{fmt}\""
)
if len(val) != found.end():
+ rest = val[found.end():]
+ # Specific check for '%:z' directive
+ if (
+ "colon_z" in found.re.groupindex
+ and found.group("colon_z") is not None
+ and rest[0] != ":"
+ ):
+ raise ValueError(
+ f"Missing colon in %:z before '{rest}', got '{val}'"
+ )
raise ValueError(
"unconverted data remains when parsing with "
- f"format \"{fmt}\": \"{val[found.end():]}\""
+ f"format \"{fmt}\": \"{rest}\""
)
else:
@@ -760,9 +771,15 @@ cdef tzinfo _parse_with_format(
elif parse_code == 17:
# e.g. val='2011-12-30T00:00:00.000000UTC'; fmt='%Y-%m-%dT%H:%M:%S.%f%Z'
tz = zoneinfo.ZoneInfo(found_dict["Z"])
- elif parse_code == 19:
+ # elif group_key in ('z', 'colon_z'):
+ elif parse_code == 19 or parse_code == 24:
# e.g. val='March 1, 2018 12:00:00+0400'; fmt='%B %d, %Y %H:%M:%S%z'
- tz = parse_timezone_directive(found_dict["z"])
+ if found_dict[group_key] is None:
+ raise ValueError(
+ f"time data \"{val}\" doesn't match format \"{fmt}\""
+ )
+
+ tz = parse_timezone_directive(found_dict[group_key])
elif parse_code == 20:
# e.g. val='2015-1-7'; fmt='%G-%V-%u'
iso_year = int(found_dict["G"])
Index: pandas/pandas/compat/__init__.py
===================================================================
--- pandas.orig/pandas/compat/__init__.py
+++ pandas/pandas/compat/__init__.py
@@ -21,6 +21,7 @@ from pandas.compat._constants import (
ISMUSL,
PY312,
PY314,
+ PY315,
PYPY,
WASM,
)
@@ -161,6 +162,7 @@ __all__ = [
"ISMUSL",
"PY312",
"PY314",
+ "PY315",
"PYARROW_INSTALLED",
"PYARROW_MIN_VERSION",
"PYPY",
Index: pandas/pandas/compat/_constants.py
===================================================================
--- pandas.orig/pandas/compat/_constants.py
+++ pandas/pandas/compat/_constants.py
@@ -15,6 +15,7 @@ IS64 = sys.maxsize > 2**32
PY312 = sys.version_info >= (3, 12)
PY314 = sys.version_info >= (3, 14)
+PY315 = sys.version_info >= (3, 15)
PYPY = platform.python_implementation() == "PyPy"
WASM = (sys.platform == "emscripten") or (platform.machine() in ["wasm32", "wasm64"])
ISMUSL = "musl" in (sysconfig.get_config_var("HOST_GNU_TYPE") or "")
@@ -30,6 +31,7 @@ __all__ = [
"ISMUSL",
"PY312",
"PY314",
+ "PY315",
"PYPY",
"WASM",
]
Index: pandas/pandas/io/xml.py
===================================================================
--- pandas.orig/pandas/io/xml.py
+++ pandas/pandas/io/xml.py
@@ -337,44 +337,53 @@ class _XMLFrameParser:
set(self.iterparse[row_node])
)
- for event, elem in iterparse(self.path_or_buffer, events=("start", "end")):
- curr_elem = elem.tag.split("}")[1] if "}" in elem.tag else elem.tag
-
- if event == "start":
- if curr_elem == row_node:
- row = {}
-
- if row is not None:
- if self.names and iterparse_repeats:
- for col, nm in zip(
- self.iterparse[row_node], self.names, strict=True
- ):
- if curr_elem == col:
- elem_val = elem.text if elem.text else None
- if elem_val not in row.values() and nm not in row:
- row[nm] = elem_val
-
- if col in elem.attrib:
- if elem.attrib[col] not in row.values() and nm not in row:
- row[nm] = elem.attrib[col]
- else:
- for col in self.iterparse[row_node]:
- if curr_elem == col:
- row[col] = elem.text if elem.text else None
- if col in elem.attrib:
- row[col] = elem.attrib[col]
-
- if event == "end":
- if curr_elem == row_node and row is not None:
- dicts.append(row)
- row = None
-
- elem.clear()
- if hasattr(elem, "getprevious"):
- while (
- elem.getprevious() is not None and elem.getparent() is not None
- ):
- del elem.getparent()[0]
+ parser = iterparse(self.path_or_buffer, events=("start", "end"))
+ try:
+ for event, elem in parser:
+ curr_elem = elem.tag.split("}")[1] if "}" in elem.tag else elem.tag
+
+ if event == "start":
+ if curr_elem == row_node:
+ row = {}
+
+ if row is not None:
+ if self.names and iterparse_repeats:
+ for col, nm in zip(
+ self.iterparse[row_node], self.names, strict=True
+ ):
+ if curr_elem == col:
+ elem_val = elem.text if elem.text else None
+ if elem_val not in row.values() and nm not in row:
+ row[nm] = elem_val
+
+ if col in elem.attrib:
+ if (
+ elem.attrib[col] not in row.values()
+ and nm not in row
+ ):
+ row[nm] = elem.attrib[col]
+ else:
+ for col in self.iterparse[row_node]:
+ if curr_elem == col:
+ row[col] = elem.text if elem.text else None
+ if col in elem.attrib:
+ row[col] = elem.attrib[col]
+
+ if event == "end":
+ if curr_elem == row_node and row is not None:
+ dicts.append(row)
+ row = None
+
+ elem.clear()
+ if hasattr(elem, "getprevious"):
+ while (
+ elem.getprevious() is not None
+ and elem.getparent() is not None
+ ):
+ del elem.getparent()[0]
+ finally:
+ if hasattr(parser, "close"):
+ parser.close()
if dicts == []:
raise ParserError("No result from selected items in iterparse.")
Index: pandas/pandas/tests/extension/decimal/test_decimal.py
===================================================================
--- pandas.orig/pandas/tests/extension/decimal/test_decimal.py
+++ pandas/pandas/tests/extension/decimal/test_decimal.py
@@ -151,7 +151,12 @@ class TestDecimalArray(base.ExtensionTes
# GH#57723
# EAs that don't have special logic for None will raise, unlike pandas'
# which interpret None as the NA value for the dtype.
- msg = "conversion from NoneType to Decimal is not supported"
+ msg = "|".join(
+ [
+ "Cannot convert None to Decimal", # PY315 - maybe linux specific
+ "conversion from NoneType to Decimal is not supported",
+ ]
+ )
with pytest.raises(TypeError, match=msg):
super().test_fillna_with_none(data_missing)
Index: pandas/pandas/tests/frame/indexing/test_indexing.py
===================================================================
--- pandas.orig/pandas/tests/frame/indexing/test_indexing.py
+++ pandas/pandas/tests/frame/indexing/test_indexing.py
@@ -12,6 +12,7 @@ import pytest
from pandas._libs import iNaT
from pandas.errors import InvalidIndexError
+from pandas.compat import PY315
from pandas.core.dtypes.common import is_integer
import pandas as pd
@@ -30,7 +31,10 @@ from pandas import (
import pandas._testing as tm
# We pass through a TypeError raised by numpy
-_slice_msg = "slice indices must be integers or None or have an __index__ method"
+if PY315:
+ _slice_msg = "slice indices must be integers or have an __index__ method"
+else:
+ _slice_msg = "slice indices must be integers or None or have an __index__ method"
class TestDataFrameIndexing:
Index: pandas/pandas/tests/indexes/period/test_partial_slicing.py
===================================================================
--- pandas.orig/pandas/tests/indexes/period/test_partial_slicing.py
+++ pandas/pandas/tests/indexes/period/test_partial_slicing.py
@@ -1,6 +1,8 @@
import numpy as np
import pytest
+from pandas.compat import PY315
+
from pandas import (
DataFrame,
PeriodIndex,
@@ -53,7 +55,10 @@ class TestPeriodIndex:
# GH#6716
idx = make_range(start="2013/01/01", freq="D", periods=400)
- msg = "slice indices must be integers or None or have an __index__ method"
+ if PY315:
+ msg = "slice indices must be integers or have an __index__ method"
+ else:
+ msg = "slice indices must be integers or None or have an __index__ method"
# slices against index should raise IndexError
values = [
"2014",
@@ -82,7 +87,10 @@ class TestPeriodIndex:
def test_range_slice_seconds(self, make_range):
# GH#6716
idx = make_range(start="2013/01/01 09:00:00", freq="s", periods=4000)
- msg = "slice indices must be integers or None or have an __index__ method"
+ if PY315:
+ msg = "slice indices must be integers or have an __index__ method"
+ else:
+ msg = "slice indices must be integers or None or have an __index__ method"
# slices against index should raise IndexError
values = [
Index: pandas/pandas/tests/indexes/test_old_base.py
===================================================================
--- pandas.orig/pandas/tests/indexes/test_old_base.py
+++ pandas/pandas/tests/indexes/test_old_base.py
@@ -7,6 +7,7 @@ import numpy as np
import pytest
from pandas._libs.tslibs import Timestamp
+from pandas.compat import PY315
from pandas.errors import Pandas4Warning
from pandas.core.dtypes.common import (
@@ -452,6 +453,8 @@ class TestBase:
if len(index) == 0:
# 0 vs 0.5 in error message varies with numpy version
msg = "index (0|0.5) is out of bounds for axis 0 with size 0"
+ elif PY315:
+ msg = "slice indices must be integers or have an __index__ method"
else:
msg = "slice indices must be integers or None or have an __index__ method"
Index: pandas/pandas/tests/indexing/test_floats.py
===================================================================
--- pandas.orig/pandas/tests/indexing/test_floats.py
+++ pandas/pandas/tests/indexing/test_floats.py
@@ -1,6 +1,8 @@
import numpy as np
import pytest
+from pandas.compat import PY315
+
from pandas import (
DataFrame,
Index,
@@ -251,7 +253,12 @@ class TestFloatIndexers:
# setitem
if indexer_sli is tm.iloc:
# otherwise we keep the same message as above
- msg = "slice indices must be integers or None or have an __index__ method"
+ if PY315:
+ msg = "slice indices must be integers or have an __index__ method"
+ else:
+ msg = (
+ "slice indices must be integers or None or have an __index__ method"
+ )
with pytest.raises(TypeError, match=msg):
indexer_sli(s)[idx] = 0
Index: pandas/pandas/tests/scalar/timestamp/test_constructors.py
===================================================================
--- pandas.orig/pandas/tests/scalar/timestamp/test_constructors.py
+++ pandas/pandas/tests/scalar/timestamp/test_constructors.py
@@ -18,7 +18,10 @@ import pytest
import pandas.util._test_decorators as td
from pandas._libs.tslibs.dtypes import NpyDatetimeUnit
-from pandas.compat import PY314
+from pandas.compat import (
+ PY314,
+ PY315,
+)
from pandas.errors import (
OutOfBoundsDatetime,
Pandas4Warning,
@@ -240,7 +243,13 @@ class TestTimestampConstructorPositional
def test_constructor_keyword(self):
# GH#10758
- msg = "function missing required argument 'day'|Required argument 'day'"
+ msg = "|".join(
+ [
+ r"datetime\(\) missing required argument 'day'", # PY315
+ "function missing required argument 'day'",
+ "Required argument 'day'",
+ ]
+ )
with pytest.raises(TypeError, match=msg):
Timestamp(year=2000, month=1)
@@ -299,7 +308,15 @@ class TestTimestampConstructorPositional
# GH#31200
# The exact error message of datetime() depends on its version
- msg1 = r"function missing required argument '(year|month|day)' \(pos [123]\)"
+ if PY315:
+ msg1 = (
+ r"datetime\(\) missing required argument "
+ r"'(year|month|day)' \(pos [123]\)"
+ )
+ else:
+ msg1 = (
+ r"function missing required argument '(year|month|day)' \(pos [123]\)"
+ )
msg2 = r"Required argument '(year|month|day)' \(pos [123]\) not found"
msg = "|".join([msg1, msg2])
Index: pandas/pandas/tests/tools/test_to_datetime.py
===================================================================
--- pandas.orig/pandas/tests/tools/test_to_datetime.py
+++ pandas/pandas/tests/tools/test_to_datetime.py
@@ -23,6 +23,7 @@ from pandas._libs.tslibs import (
)
from pandas.compat import (
PY314,
+ PY315,
WASM,
)
from pandas.errors import (
@@ -517,6 +518,39 @@ class TestTimeConversionFormats:
expected = DatetimeIndex(expected_dates)
tm.assert_index_equal(result, expected)
+ @pytest.mark.xfail(
+ not PY315, reason="%:z directive not supported prior to 3.15", raises=ValueError
+ )
+ def test_to_datetime_colon_z_offset(self):
+ dates = [
+ "2010-01-01 12:00:00+04:00",
+ "2010-01-01 12:00:00+04:30",
+ "2010-01-01 12:00:00-05:00",
+ ]
+ expected_dates = [
+ "2010-01-01 08:00:00+00:00",
+ "2010-01-01 07:30:00+00:00",
+ "2010-01-01 17:00:00+00:00",
+ ]
+ fmt = "%Y-%m-%d %H:%M:%S%:z"
+
+ result = to_datetime(dates, format=fmt, utc=True)
+ expected = DatetimeIndex(expected_dates)
+ tm.assert_index_equal(result, expected)
+
+ def test_to_datetime_missing_colon_z_offset(self):
+ # test adapted from python/cpython#136961
+ dates = ["+04:0030"]
+ fmt = "%:z"
+
+ if PY315:
+ msg = r"Missing colon in %:z before '30', got '\+04:0030'"
+ else:
+ msg = "':' is a bad directive in format '%:z'"
+
+ with pytest.raises(ValueError, match=msg):
+ to_datetime(dates, format=fmt, utc=True)
+
@pytest.mark.parametrize(
"offset", ["+0", "-1foo", "UTCbar", ":10", "+01:000:01", ""]
)
@@ -1390,9 +1424,12 @@ class TestToDatetime:
@pytest.mark.parametrize("errors", ["coerce", "raise"])
def test_invalid_format_raises(self, errors):
# https://github.com/pandas-dev/pandas/issues/50255
- with pytest.raises(
- ValueError, match="':' is a bad directive in format 'H%:M%:S%"
- ):
+ if PY315:
+ msg = r"':M' is a bad directive in format 'H%:M%:S%"
+ else:
+ msg = "':' is a bad directive in format 'H%:M%:S%"
+
+ with pytest.raises(ValueError, match=msg):
to_datetime(["00:00:00"], format="H%:M%:S%", errors=errors)
@pytest.mark.parametrize("value", ["a", "00:01:99"])
signature.asc
(application/pgp-signature, 833 B)
-----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEE+JIdOnQEyG4RNSIVxxl2mbKbIyoFAmqFvW0ACgkQxxl2mbKb IyrAgA//Ygzp+W5fEMheXVEQk+psGFvymtZJbrU+H9YAEqD+/6oSvu7MAvSR2ZWC K8ROBmam3dlfKPO8sRuzuLWtQFY/+86p6GHBaD6XnJYnYEaCnH3nCkpXT4hVpJO1 qiuM2+bF80N5N2Rm0jclatj6/pQmGyYi1ubnS2nfGYNW2JhXptQh7XA4G1erOM0e +6FDibT9aBWmv4Hd2rd7n9SGDdKxNj/WJ/xWq/Pn2vgHqs1TpP5+4N8qOipl2KeO d/Q2Wahcz/KH+a/AQwdBZeif4+RoBE3zoX5MQ53kHXOOesz/0tzNSXLBZIFn7zkL q1Obqu/XkgXX5atJTCY27pnnM9MaetVWZ3LIsEJnC5VwIiIP06h1oVhpNTSohmzU 9VCKVN4pfG94nloQKaRW3FS5TB+MVMdG1FoujAuLVsWUEkFs+Zi39nssEYxwu350 hUMHjPkl8/w8+U1CwfZH6UIdhS9OdER+dMXgXK/dKlvWQQAr5D732Hoja8CSELxt ZX7creVpLcboUzTuIIOliCV6no44f75mzKEL7+By1rCUsN49RbwATDgmyslQGlU3 BtNo3TRvKxIAcMlhGRg6Q3bhtaGHxUBGty92dK3W5xfUsuRq5pVucyGvyXg0W355 vf1xBNXhViJxO3tR/g3noKf6+mu8H1F16UOGRPbV4+wiIJoKXls= =lc6u -----END PGP SIGNATURE-----