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 /\/\ /\ >< `/
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"])

Attachment: signature.asc
Description: PGP signature

-- 
debian-science-maintainers mailing list
[email protected]
https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/debian-science-maintainers

Reply via email to