diff --git a/.github/workflows/daily_precommit.yml b/.github/workflows/daily_precommit.yml index 0ef646e975..8733b70352 100644 --- a/.github/workflows/daily_precommit.yml +++ b/.github/workflows/daily_precommit.yml @@ -110,6 +110,7 @@ jobs: - { os: {image_name: ubuntu-latest-64-cores, download_name: linux}, python-version: "3.11", cloud-provider: gcp } - { os: {image_name: ubuntu-latest-64-cores, download_name: linux}, python-version: "3.12", cloud-provider: aws } - { os: {image_name: ubuntu-latest-64-cores, download_name: linux}, python-version: "3.13", cloud-provider: azure } + - { os: {image_name: ubuntu-latest-64-cores, download_name: linux}, python-version: "3.14", cloud-provider: gcp } # macOS + rotating cloud providers - { os: {image_name: macos-latest, download_name: macos}, python-version: "3.9", cloud-provider: gcp } @@ -117,6 +118,7 @@ jobs: - { os: {image_name: macos-latest, download_name: macos}, python-version: "3.11", cloud-provider: azure } - { os: {image_name: macos-latest, download_name: macos}, python-version: "3.12", cloud-provider: gcp } - { os: {image_name: macos-latest, download_name: macos}, python-version: "3.13", cloud-provider: aws } + - { os: {image_name: macos-latest, download_name: macos}, python-version: "3.14", cloud-provider: azure } # Windows + rotating cloud providers - { os: {image_name: windows-latest-64-cores, download_name: windows}, python-version: "3.9", cloud-provider: azure } @@ -124,6 +126,7 @@ jobs: - { os: {image_name: windows-latest-64-cores, download_name: windows}, python-version: "3.11", cloud-provider: aws } - { os: {image_name: windows-latest-64-cores, download_name: windows}, python-version: "3.12", cloud-provider: azure } - { os: {image_name: windows-latest-64-cores, download_name: windows}, python-version: "3.13", cloud-provider: gcp } + - { os: {image_name: windows-latest-64-cores, download_name: windows}, python-version: "3.14", cloud-provider: aws } steps: - name: Checkout Code uses: actions/checkout@v4 @@ -418,7 +421,7 @@ jobs: image_name: windows-latest - download_name: ubuntu image_name: ubuntu-latest - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] cloud-provider: [azure] steps: - name: Checkout Code @@ -488,7 +491,7 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] # SNOW-2230787 test failing on Python 3.13 + python-version: ["3.9", "3.10", "3.11", "3.12", "3.14"] # SNOW-2230787 test failing on Python 3.13 cloud-provider: [gcp] protobuf-version: ["3.20.1", "4.25.3", "5.28.3"] steps: @@ -558,7 +561,7 @@ jobs: os: - image_name: macos-latest download_name: macos # it includes doctest - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] cloud-provider: [azure] steps: - name: Checkout Code diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index 8e42794cc8..a4d51972a2 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -132,6 +132,18 @@ jobs: - python-version: "3.13" cloud-provider: gcp os: windows-latest-64-cores + # run py 3.14 tests on aws/ubuntu + - python-version: "3.14" + cloud-provider: aws + os: ubuntu-latest-64-cores + # run py 3.14 doctests on azure/macos + - python-version: "3.14" + cloud-provider: azure + os: macos-latest + # # run py 3.14 tests on gcp on windows + - python-version: "3.14" + cloud-provider: gcp + os: windows-latest-64-cores steps: - name: Checkout Code uses: actions/checkout@v4 @@ -233,7 +245,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.13"] + python-version: ["3.14"] cloud-provider: [azure] steps: - name: Checkout Code @@ -436,7 +448,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - python-version: [ "3.13" ] # Test latest python + python-version: [ "3.14" ] # Test latest python cloud-provider: [ gcp ] # Test only one csp steps: - name: Checkout Code diff --git a/recipe/meta.yaml b/recipe/meta.yaml index e85a0dad44..ba8cd6e524 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -24,10 +24,11 @@ build: string: "py311_{{ build_number }}" # [py==311] string: "py312_{{ build_number }}" # [py==312] string: "py313_{{ build_number }}" # [py==313] + string: "py314_{{ build_number }}" # [py==314] {% endif %} -{% if noarch_build and py not in [39, 310, 311, 312, 313] %} -error: "Noarch build for Python version {{ py }} is not supported. Supported versions: 3.9, 3.10, 3.11, 3.12, or 3.13." +{% if noarch_build and py not in [39, 310, 311, 312, 313, 314] %} +error: "Noarch build for Python version {{ py }} is not supported. Supported versions: 3.9, 3.10, 3.11, 3.12, 3.13, or 3.14." {% else %} requirements: host: @@ -37,7 +38,9 @@ requirements: - wheel # Snowpark IR - protobuf==3.20.1 # [py<=310] - - protobuf==4.25.3 # [py>310] + - protobuf==4.25.3 # [py>310 and py<314] + - protobuf==5.29.3 # [py>=314] + - libprotobuf >=5.29.3 # [py>=314] # mypy-protobuf 3.7.0 requires protobuf >= 5.26 - mypy-protobuf <=3.6.0 run: @@ -51,6 +54,8 @@ requirements: - python >=3.12,<3.13.0a0 {% elif noarch_build and py == 313 %} - python >=3.13,<3.14.0a0 + {% elif noarch_build and py == 314 %} + - python >=3.14,<3.15.0a0 {% else %} - python {% endif %} diff --git a/scripts/conda_build.sh b/scripts/conda_build.sh index 783051b6fe..a520d39183 100755 --- a/scripts/conda_build.sh +++ b/scripts/conda_build.sh @@ -4,3 +4,4 @@ conda build recipe/ -c sfe1ed40 --python=3.10 --numpy=1.21 conda build recipe/ -c sfe1ed40 --python=3.11 --numpy=1.23 conda build recipe/ -c sfe1ed40 --python=3.12 --numpy=1.26 conda build recipe/ -c sfe1ed40 --python=3.13 --numpy=2.2.0 +conda build recipe/ -c sfe1ed40 --python=3.14 --numpy=2.4.2 diff --git a/setup.py b/setup.py index dc12bb82d6..276820bfd1 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ "python-dateutil", # Snowpark IR "tzlocal", # Snowpark IR ] -REQUIRED_PYTHON_VERSION = ">=3.9, <3.14" +REQUIRED_PYTHON_VERSION = ">=3.9, <3.15" if os.getenv("SNOWFLAKE_IS_PYTHON_RUNTIME_TEST", False): REQUIRED_PYTHON_VERSION = ">=3.9" @@ -71,7 +71,7 @@ # Snowpark pandas 3rd party library testing. Cap the scipy version because # Snowflake cannot find newer versions of scipy for python 3.11+. See # SNOW-2452791. - "scipy<=1.16.0", + "scipy<=1.16.3", "statsmodels", # Snowpark pandas 3rd party library testing "scikit-learn", # Snowpark pandas 3rd party library testing # plotly version restricted due to foreseen change in query counts in version 6.0.0+ @@ -80,7 +80,8 @@ # snowflake-ml-python is available on python 3.12. "snowflake-ml-python>=1.8.0; python_version<'3.12'", "s3fs", # Used in tests that read CSV files from s3 - "ray", # Used in data movement tests + # ray currently has no compatible wheels for Python 3.14. + "ray; python_version<'3.14'", # Used in data movement tests ] # read the version @@ -249,6 +250,7 @@ def run(self): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Software Development", "Topic :: Software Development :: Libraries", diff --git a/src/snowflake/snowpark/_internal/analyzer/expression.py b/src/snowflake/snowpark/_internal/analyzer/expression.py index d95dcdc95a..0d62d5a47d 100644 --- a/src/snowflake/snowpark/_internal/analyzer/expression.py +++ b/src/snowflake/snowpark/_internal/analyzer/expression.py @@ -2,7 +2,6 @@ # Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved. # -import copy import uuid from typing import TYPE_CHECKING, AbstractSet, Any, Dict, List, Optional, Tuple @@ -158,7 +157,9 @@ def expr_id(self) -> uuid.UUID: return self._expr_id def __copy__(self): - new = copy.copy(super()) + cls = self.__class__ + new = cls.__new__(cls) + new.__dict__.update(self.__dict__) new._expr_id = None # type: ignore return new diff --git a/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py b/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py index 69138e1701..54a28628e7 100644 --- a/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py +++ b/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py @@ -7,7 +7,7 @@ from logging import getLogger import re import sys -import uuid +import uuid as uuid_lib from collections import defaultdict, deque from enum import Enum from dataclasses import dataclass @@ -383,7 +383,7 @@ def add_single_quote(string: str) -> str: # and it's a reading XML query. def search_read_file_node( - node: Union[SnowflakePlan, Selectable] + node: Union[SnowflakePlan, Selectable], ) -> Optional[ReadFileNode]: source_plan = ( node.source_plan @@ -435,7 +435,7 @@ def __init__( # during the compilation stage. schema_query: Optional[str], post_actions: Optional[List["Query"]] = None, - expr_to_alias: Optional[Dict[uuid.UUID, str]] = None, + expr_to_alias: Optional[Dict[uuid_lib.UUID, str]] = None, source_plan: Optional[LogicalPlan] = None, is_ddl_on_temp_object: bool = False, api_calls: Optional[List[Dict]] = None, @@ -479,7 +479,9 @@ def __init__( if self.session._join_alias_fix else defaultdict(dict) ) - self._uuid = from_selectable_uuid if from_selectable_uuid else str(uuid.uuid4()) + self._uuid = ( + from_selectable_uuid if from_selectable_uuid else str(uuid_lib.uuid4()) + ) # We set the query line intervals for the last query in the queries list self.set_last_query_line_intervals() # In the placeholder query, subquery (child) is held by the ID of query plan diff --git a/src/snowflake/snowpark/mock/_plan.py b/src/snowflake/snowpark/mock/_plan.py index a0275600ae..0e25cb862f 100644 --- a/src/snowflake/snowpark/mock/_plan.py +++ b/src/snowflake/snowpark/mock/_plan.py @@ -590,7 +590,7 @@ def handle_function_expression( if param_name in exp.named_arguments: type_hint = str(type_hints.get(param_name, "")) keep_literal = "Column" not in type_hint - if type_hint == "typing.Optional[dict]": + if type_hint in ["typing.Optional[dict]", "dict | None"]: to_pass_kwargs[param_name] = json.loads( exp.named_arguments[param_name].sql.replace("'", '"') ) diff --git a/tests/integ/test_stored_procedure.py b/tests/integ/test_stored_procedure.py index ceaabb37fd..a5e3efd1bc 100644 --- a/tests/integ/test_stored_procedure.py +++ b/tests/integ/test_stored_procedure.py @@ -1181,7 +1181,12 @@ def _(_: Session, x, y: int) -> int: def _(_: Session, x: int, y: Union[int, float]) -> Union[int, float]: return x + y - assert "invalid type typing.Union[int, float]" in str(ex_info) + msgs = [ + "invalid type typing.Union[int, float]", + # python 3.14 changed the string representation of Union types + "invalid type int | float", + ] + assert any(msg in str(ex_info) for msg in msgs) with pytest.raises(TypeError) as ex_info: diff --git a/tests/integ/test_udf.py b/tests/integ/test_udf.py index d9029efd23..f13168e944 100644 --- a/tests/integ/test_udf.py +++ b/tests/integ/test_udf.py @@ -1458,7 +1458,12 @@ def _(x, y: int) -> int: def _(x: int, y: Union[int, float]) -> Union[int, float]: return x + y - assert "invalid type typing.Union[int, float]" in str(ex_info) + msgs = [ + "invalid type typing.Union[int, float]", + # python 3.14 changed the string representation of Union types + "invalid type int | float", + ] + assert any(msg in str(ex_info) for msg in msgs) with pytest.raises(ValueError) as ex_info: diff --git a/tests/mock/test_functions.py b/tests/mock/test_functions.py index df70994af6..90f610dc51 100644 --- a/tests/mock/test_functions.py +++ b/tests/mock/test_functions.py @@ -631,7 +631,11 @@ def test_ai_complete(session): # Mock the ai_complete function to return a simple response @patch("ai_complete") def mock_ai_complete( - model=None, prompt=None, response_format=None, model_parameters=None, **kwargs + model=None, + prompt=None, + response_format=None, + model_parameters=None, + **kwargs, ) -> ColumnEmulator: """Simple mock that returns 'AI response: ' for each input.""" assert ( diff --git a/tests/unit/test_code_generation.py b/tests/unit/test_code_generation.py index 52d15eeab6..52c7e54769 100644 --- a/tests/unit/test_code_generation.py +++ b/tests/unit/test_code_generation.py @@ -3,6 +3,7 @@ # import math +import pickle import pytest @@ -617,17 +618,18 @@ def func(): def test_variable_serialization(): nonlocalvar = "abc" + expected_hex = pickle.dumps(nonlocalvar).hex() def add(x, y): return x + y + nonlocalvar assert ( generate_source_code(add, code_as_comment=False) - == """\ + == f"""\ from __future__ import annotations import pickle -nonlocalvar = pickle.loads(bytes.fromhex('80049507000000000000008c03616263942e')) # nonlocalvar is of type and serialized by snowpark-python +nonlocalvar = pickle.loads(bytes.fromhex('{expected_hex}')) # nonlocalvar is of type and serialized by snowpark-python def add(x, y): return x + y + nonlocalvar func = add\ diff --git a/tox.ini b/tox.ini index 190992de62..28f4527b66 100644 --- a/tox.ini +++ b/tox.ini @@ -177,7 +177,7 @@ commands = coverage combine coverage report -m coverage xml -o {env:COV_REPORT_DIR:{toxworkdir}}/coverage.xml coverage html -d {env:COV_REPORT_DIR:{toxworkdir}}/htmlcov --show-contexts -depends = py39, py310, py311, py312, py313 +depends = py39, py310, py311, py312, py313, py314 [testenv:docs] basepython = python3.9