Skip to content

Commit c9a8f95

Browse files
lithomas1mroeschke
andauthored
CI: Test Python 3.12 (#53743)
* CI: Test Python 3.12 * Update unit-tests.yml * Update config.yml * update * fix condition * fix some tests * Remove wheel building for Python 3.12 * fix more * Use timezone.utc * Address typing, utcfromtimestamp * fix some slice changes * fix all indexing bugs? * fix import * go for green * disable macos for now, fix other tests * Update indexing.py * finally fix? * Update expr.py * Update pandas/tests/computation/test_eval.py Co-authored-by: Matthew Roeschke <[email protected]> * Update test_eval.py * Update test_eval.py * fixes * formatting --------- Co-authored-by: Matthew Roeschke <[email protected]>
1 parent ee5cf2c commit c9a8f95

File tree

22 files changed

+134
-47
lines changed

22 files changed

+134
-47
lines changed

.circleci/config.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ jobs:
4747
- run:
4848
name: Build aarch64 wheels
4949
command: |
50-
pip3 install cibuildwheel==2.12.1
51-
cibuildwheel --output-dir wheelhouse
50+
pip3 install cibuildwheel==2.14.1
51+
cibuildwheel --prerelease-pythons --output-dir wheelhouse
5252
environment:
5353
CIBW_BUILD: << parameters.cibw-build >>
5454

@@ -91,4 +91,5 @@ workflows:
9191
only: /^v.*/
9292
matrix:
9393
parameters:
94-
cibw-build: ["cp39-manylinux_aarch64", "cp310-manylinux_aarch64", "cp311-manylinux_aarch64"]
94+
# TODO: Enable Python 3.12 wheels when numpy releases a version that supports Python 3.12
95+
cibw-build: ["cp39-manylinux_aarch64", "cp310-manylinux_aarch64", "cp311-manylinux_aarch64"]#, "cp312-manylinux_aarch64"]

.github/workflows/unit-tests.yml

+10-6
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,16 @@ jobs:
311311
# To freeze this file, uncomment out the ``if: false`` condition, and migrate the jobs
312312
# to the corresponding posix/windows-macos/sdist etc. workflows.
313313
# Feel free to modify this comment as necessary.
314-
if: false # Uncomment this to freeze the workflow, comment it to unfreeze
314+
#if: false # Uncomment this to freeze the workflow, comment it to unfreeze
315315
runs-on: ${{ matrix.os }}
316316
strategy:
317317
fail-fast: false
318318
matrix:
319-
os: [ubuntu-22.04, macOS-latest, windows-latest]
319+
# TODO: Disable macOS for now, Github Actions bug where python is not
320+
# symlinked correctly to 3.12
321+
# xref https://github.com/actions/setup-python/issues/701
322+
#os: [ubuntu-22.04, macOS-latest, windows-latest]
323+
os: [ubuntu-22.04, windows-latest]
320324

321325
timeout-minutes: 180
322326

@@ -340,21 +344,21 @@ jobs:
340344
- name: Set up Python Dev Version
341345
uses: actions/setup-python@v4
342346
with:
343-
python-version: '3.11-dev'
347+
python-version: '3.12-dev'
344348

345349
- name: Install dependencies
346350
run: |
347351
python --version
348-
python -m pip install --upgrade pip setuptools wheel
352+
python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.0.1 meson-python==0.13.1
349353
python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy
350354
python -m pip install git+https://github.com/nedbat/coveragepy.git
351355
python -m pip install versioneer[toml]
352-
python -m pip install python-dateutil pytz cython hypothesis>=6.46.1 pytest>=7.3.2 pytest-xdist>=2.2.0 pytest-cov pytest-asyncio>=0.17
356+
python -m pip install python-dateutil pytz tzdata cython hypothesis>=6.46.1 pytest>=7.3.2 pytest-xdist>=2.2.0 pytest-cov pytest-asyncio>=0.17
353357
python -m pip list
354358
355359
- name: Build Pandas
356360
run: |
357-
python -m pip install -e . --no-build-isolation --no-index
361+
python -m pip install -ve . --no-build-isolation --no-index
358362
359363
- name: Build Version
360364
run: |

.github/workflows/wheels.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ jobs:
9393
- [macos-12, macosx_*]
9494
- [windows-2022, win_amd64]
9595
# TODO: support PyPy?
96-
python: [["cp39", "3.9"], ["cp310", "3.10"], ["cp311", "3.11"]]
96+
# TODO: Enable Python 3.12 wheels when numpy releases a version that supports Python 3.12
97+
python: [["cp39", "3.9"], ["cp310", "3.10"], ["cp311", "3.11"]]#, ["cp312", "3.12"]]
9798
env:
9899
IS_PUSH: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
99100
IS_SCHEDULE_DISPATCH: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
@@ -117,6 +118,7 @@ jobs:
117118
#with:
118119
# package-dir: ./dist/${{ needs.build_sdist.outputs.sdist_file }}
119120
env:
121+
CIBW_PRERELEASE_PYTHONS: True
120122
CIBW_BUILD: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}
121123

122124
- name: Set up Python

meson.build

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ versioneer = files('generate_version.py')
2727
add_project_arguments('-DNPY_NO_DEPRECATED_API=0', language : 'c')
2828
add_project_arguments('-DNPY_NO_DEPRECATED_API=0', language : 'cpp')
2929

30+
# Allow supporting older numpys than the version compiled against
31+
# Set the define to the min supported version of numpy for pandas
32+
# e.g. right now this is targeting numpy 1.21+
33+
add_project_arguments('-DNPY_TARGET_VERSION=NPY_1_21_API_VERSION', language : 'c')
34+
add_project_arguments('-DNPY_TARGET_VERSION=NPY_1_21_API_VERSION', language : 'cpp')
35+
36+
3037
if fs.exists('_version_meson.py')
3138
py.install_sources('_version_meson.py', pure: false, subdir: 'pandas')
3239
else

pandas/compat/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
ISMUSL,
2020
PY310,
2121
PY311,
22+
PY312,
2223
PYPY,
2324
)
2425
import pandas.compat.compressors
@@ -189,5 +190,6 @@ def get_bz2_file() -> type[pandas.compat.compressors.BZ2File]:
189190
"ISMUSL",
190191
"PY310",
191192
"PY311",
193+
"PY312",
192194
"PYPY",
193195
]

pandas/compat/_constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
PY310 = sys.version_info >= (3, 10)
1717
PY311 = sys.version_info >= (3, 11)
18+
PY312 = sys.version_info >= (3, 12)
1819
PYPY = platform.python_implementation() == "PyPy"
1920
ISMUSL = "musl" in (sysconfig.get_config_var("HOST_GNU_TYPE") or "")
2021
REF_COUNT = 2 if PY311 else 3
@@ -24,5 +25,6 @@
2425
"ISMUSL",
2526
"PY310",
2627
"PY311",
28+
"PY312",
2729
"PYPY",
2830
]

pandas/core/computation/expr.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -543,15 +543,18 @@ def visit_UnaryOp(self, node, **kwargs):
543543
def visit_Name(self, node, **kwargs):
544544
return self.term_type(node.id, self.env, **kwargs)
545545

546+
# TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min
546547
def visit_NameConstant(self, node, **kwargs) -> Term:
547548
return self.const_type(node.value, self.env)
548549

550+
# TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min
549551
def visit_Num(self, node, **kwargs) -> Term:
550-
return self.const_type(node.n, self.env)
552+
return self.const_type(node.value, self.env)
551553

552554
def visit_Constant(self, node, **kwargs) -> Term:
553-
return self.const_type(node.n, self.env)
555+
return self.const_type(node.value, self.env)
554556

557+
# TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min
555558
def visit_Str(self, node, **kwargs):
556559
name = self.env.add_tmp(node.s)
557560
return self.term_type(name, self.env)

pandas/core/indexes/base.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from collections import abc
34
from datetime import datetime
45
import functools
56
from itertools import zip_longest
@@ -3788,6 +3789,11 @@ def get_loc(self, key):
37883789
try:
37893790
return self._engine.get_loc(casted_key)
37903791
except KeyError as err:
3792+
if isinstance(casted_key, slice) or (
3793+
isinstance(casted_key, abc.Iterable)
3794+
and any(isinstance(x, slice) for x in casted_key)
3795+
):
3796+
raise InvalidIndexError(key)
37913797
raise KeyError(key) from err
37923798
except TypeError:
37933799
# If we have a listlike key, _check_indexing_error will raise

pandas/core/indexes/datetimelike.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
to_offset,
3333
)
3434
from pandas.compat.numpy import function as nv
35-
from pandas.errors import NullFrequencyError
35+
from pandas.errors import (
36+
InvalidIndexError,
37+
NullFrequencyError,
38+
)
3639
from pandas.util._decorators import (
3740
Appender,
3841
cache_readonly,
@@ -165,7 +168,7 @@ def __contains__(self, key: Any) -> bool:
165168
hash(key)
166169
try:
167170
self.get_loc(key)
168-
except (KeyError, TypeError, ValueError):
171+
except (KeyError, TypeError, ValueError, InvalidIndexError):
169172
return False
170173
return True
171174

pandas/core/indexing.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,12 @@ def _get_setitem_indexer(self, key):
742742

743743
ax = self.obj._get_axis(0)
744744

745-
if isinstance(ax, MultiIndex) and self.name != "iloc" and is_hashable(key):
745+
if (
746+
isinstance(ax, MultiIndex)
747+
and self.name != "iloc"
748+
and is_hashable(key)
749+
and not isinstance(key, slice)
750+
):
746751
with suppress(KeyError, InvalidIndexError):
747752
# TypeError e.g. passed a bool
748753
return ax.get_loc(key)
@@ -1063,6 +1068,14 @@ def _getitem_nested_tuple(self, tup: tuple):
10631068
# we have a nested tuple so have at least 1 multi-index level
10641069
# we should be able to match up the dimensionality here
10651070

1071+
def _contains_slice(x: object) -> bool:
1072+
# Check if object is a slice or a tuple containing a slice
1073+
if isinstance(x, tuple):
1074+
return any(isinstance(v, slice) for v in x)
1075+
elif isinstance(x, slice):
1076+
return True
1077+
return False
1078+
10661079
for key in tup:
10671080
check_dict_or_set_indexers(key)
10681081

@@ -1073,7 +1086,10 @@ def _getitem_nested_tuple(self, tup: tuple):
10731086
if self.name != "loc":
10741087
# This should never be reached, but let's be explicit about it
10751088
raise ValueError("Too many indices") # pragma: no cover
1076-
if all(is_hashable(x) or com.is_null_slice(x) for x in tup):
1089+
if all(
1090+
(is_hashable(x) and not _contains_slice(x)) or com.is_null_slice(x)
1091+
for x in tup
1092+
):
10771093
# GH#10521 Series should reduce MultiIndex dimensions instead of
10781094
# DataFrame, IndexingError is not raised when slice(None,None,None)
10791095
# with one row.
@@ -1422,7 +1438,15 @@ def _convert_to_indexer(self, key, axis: AxisInt):
14221438
):
14231439
raise IndexingError("Too many indexers")
14241440

1425-
if is_scalar(key) or (isinstance(labels, MultiIndex) and is_hashable(key)):
1441+
# Slices are not valid keys passed in by the user,
1442+
# even though they are hashable in Python 3.12
1443+
contains_slice = False
1444+
if isinstance(key, tuple):
1445+
contains_slice = any(isinstance(v, slice) for v in key)
1446+
1447+
if is_scalar(key) or (
1448+
isinstance(labels, MultiIndex) and is_hashable(key) and not contains_slice
1449+
):
14261450
# Otherwise get_loc will raise InvalidIndexError
14271451

14281452
# if we are a label return me

pandas/core/series.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1022,7 +1022,12 @@ def __getitem__(self, key):
10221022
elif key_is_scalar:
10231023
return self._get_value(key)
10241024

1025-
if is_hashable(key):
1025+
# Convert generator to list before going through hashable part
1026+
# (We will iterate through the generator there to check for slices)
1027+
if is_iterator(key):
1028+
key = list(key)
1029+
1030+
if is_hashable(key) and not isinstance(key, slice):
10261031
# Otherwise index.get_value will raise InvalidIndexError
10271032
try:
10281033
# For labels that don't resolve as scalars like tuples and frozensets
@@ -1042,9 +1047,6 @@ def __getitem__(self, key):
10421047
# Do slice check before somewhat-costly is_bool_indexer
10431048
return self._getitem_slice(key)
10441049

1045-
if is_iterator(key):
1046-
key = list(key)
1047-
10481050
if com.is_bool_indexer(key):
10491051
key = check_bool_indexer(self.index, key)
10501052
key = np.asarray(key, dtype=bool)

pandas/io/sql.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -2070,6 +2070,11 @@ class SQLiteTable(SQLTable):
20702070
"""
20712071

20722072
def __init__(self, *args, **kwargs) -> None:
2073+
super().__init__(*args, **kwargs)
2074+
2075+
self._register_date_adapters()
2076+
2077+
def _register_date_adapters(self) -> None:
20732078
# GH 8341
20742079
# register an adapter callable for datetime.time object
20752080
import sqlite3
@@ -2080,8 +2085,27 @@ def _adapt_time(t) -> str:
20802085
# This is faster than strftime
20812086
return f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}.{t.microsecond:06d}"
20822087

2088+
# Also register adapters for date/datetime and co
2089+
# xref https://docs.python.org/3.12/library/sqlite3.html#adapter-and-converter-recipes
2090+
# Python 3.12+ doesn't auto-register adapters for us anymore
2091+
2092+
adapt_date_iso = lambda val: val.isoformat()
2093+
adapt_datetime_iso = lambda val: val.isoformat()
2094+
adapt_datetime_epoch = lambda val: int(val.timestamp())
2095+
20832096
sqlite3.register_adapter(time, _adapt_time)
2084-
super().__init__(*args, **kwargs)
2097+
2098+
sqlite3.register_adapter(date, adapt_date_iso)
2099+
sqlite3.register_adapter(datetime, adapt_datetime_iso)
2100+
sqlite3.register_adapter(datetime, adapt_datetime_epoch)
2101+
2102+
convert_date = lambda val: date.fromisoformat(val.decode())
2103+
convert_datetime = lambda val: datetime.fromisoformat(val.decode())
2104+
convert_timestamp = lambda val: datetime.fromtimestamp(int(val))
2105+
2106+
sqlite3.register_converter("date", convert_date)
2107+
sqlite3.register_converter("datetime", convert_datetime)
2108+
sqlite3.register_converter("timestamp", convert_timestamp)
20852109

20862110
def sql_schema(self) -> str:
20872111
return str(";\n".join(self.table))

pandas/io/xml.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ def _validate_names(self) -> None:
501501
children = self.iterparse[next(iter(self.iterparse))]
502502
else:
503503
parent = self.xml_doc.find(self.xpath, namespaces=self.namespaces)
504-
children = parent.findall("*") if parent else []
504+
children = parent.findall("*") if parent is not None else []
505505

506506
if is_list_like(self.names):
507507
if len(self.names) < len(children):

pandas/tests/computation/test_eval.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import numpy as np
1010
import pytest
1111

12+
from pandas.compat import PY312
1213
from pandas.errors import (
1314
NumExprClobberingError,
1415
PerformanceWarning,
@@ -561,22 +562,16 @@ def test_unary_in_array(self):
561562
# TODO: 2022-01-29: result return list with numexpr 2.7.3 in CI
562563
# but cannot reproduce locally
563564
result = np.array(
564-
pd.eval(
565-
"[-True, True, ~True, +True,"
566-
"-False, False, ~False, +False,"
567-
"-37, 37, ~37, +37]"
568-
),
565+
pd.eval("[-True, True, +True, -False, False, +False, -37, 37, ~37, +37]"),
569566
dtype=np.object_,
570567
)
571568
expected = np.array(
572569
[
573570
-True,
574571
True,
575-
~True,
576572
+True,
577573
-False,
578574
False,
579-
~False,
580575
+False,
581576
-37,
582577
37,
@@ -705,9 +700,13 @@ def test_disallow_python_keywords(self):
705700

706701
def test_true_false_logic(self):
707702
# GH 25823
708-
assert pd.eval("not True") == -2
709-
assert pd.eval("not False") == -1
710-
assert pd.eval("True and not True") == 0
703+
# This behavior is deprecated in Python 3.12
704+
with tm.maybe_produces_warning(
705+
DeprecationWarning, PY312, check_stacklevel=False
706+
):
707+
assert pd.eval("not True") == -2
708+
assert pd.eval("not False") == -1
709+
assert pd.eval("True and not True") == 0
711710

712711
def test_and_logic_string_match(self):
713712
# GH 25823

pandas/tests/frame/indexing/test_where.py

+2
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ def _check_align(df, cond, other, check_dtypes=True):
143143
check_dtypes = all(not issubclass(s.type, np.integer) for s in df.dtypes)
144144
_check_align(df, cond, np.nan, check_dtypes=check_dtypes)
145145

146+
# Ignore deprecation warning in Python 3.12 for inverting a bool
147+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
146148
def test_where_invalid(self):
147149
# invalid conditions
148150
df = DataFrame(np.random.randn(5, 3), columns=["A", "B", "C"])

pandas/tests/indexes/test_indexing.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,8 @@ def test_contains_requires_hashable_raises(self, index):
176176

177177
class TestGetLoc:
178178
def test_get_loc_non_hashable(self, index):
179-
# MultiIndex and Index raise TypeError, others InvalidIndexError
180-
181-
with pytest.raises((TypeError, InvalidIndexError), match="slice"):
182-
index.get_loc(slice(0, 1))
179+
with pytest.raises(InvalidIndexError, match="[0, 1]"):
180+
index.get_loc([0, 1])
183181

184182
def test_get_loc_non_scalar_hashable(self, index):
185183
# GH52877

0 commit comments

Comments
 (0)