diff --git a/.github/workflows/daily_precommit.yml b/.github/workflows/daily_precommit.yml index 0ef646e975..42fe3a0f3e 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 @@ -162,7 +165,8 @@ jobs: run: uv pip install -U setuptools pip wheel --system - name: Install tox run: uv pip install tox --system - - if: ${{ contains('macos', matrix.os.download_name) }} + # TODO: enable doctest for 3.14 + - if: ${{ contains('macos', matrix.os.download_name) && matrix.python-version != '3.14' }} name: Run doctests run: python -m tox -e "py${PYTHON_VERSION}-doctest-notudf-ci" env: @@ -173,7 +177,9 @@ jobs: # Specify SNOWFLAKE_IS_PYTHON_RUNTIME_TEST: 1 when adding >= python3.12 with no server-side support # For example, see https://github.com/snowflakedb/snowpark-python/pull/681 shell: bash - - name: Run tests (excluding doctests) + # TODO: enable for 3.14 + - if: ${{ matrix.python-version != '3.14' }} + name: Run tests (excluding doctests) run: python -m tox -e "py${PYTHON_VERSION/\./}-dailynotdoctest-ci" env: PYTHON_VERSION: ${{ matrix.python-version }} @@ -183,6 +189,16 @@ jobs: SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} shell: bash + # TODO: remove the test below and run udf tests for 3.14 + - if: ${{ matrix.python-version == '3.14' }} + name: Run tests (excluding udf, doctests) + run: python -m tox -e "py${PYTHON_VERSION/\./}-dailynotdoctestnotudf-ci" + env: + PYTHON_VERSION: ${{ matrix.python-version }} + cloud_provider: ${{ matrix.cloud-provider }} + PYTEST_ADDOPTS: --color=yes --tb=short + TOX_PARALLEL_NO_SPINNER: 1 + shell: bash - name: Install MS ODBC Driver (Ubuntu only) if: ${{ matrix.os.download_name == 'linux' }} run: | @@ -193,7 +209,8 @@ jobs: shell: bash - name: Run data source tests # psycopg2 is not supported on macos 3.9 - if: ${{ !(contains('macos', matrix.os.download_name) && matrix.python-version == '3.9') }} + # TODO: enable datasource tests for 3.14 + if: ${{ !(contains('macos', matrix.os.download_name) && matrix.python-version == '3.9') && !(matrix.python-version == '3.14') }} run: python -m tox -e datasource env: PYTHON_VERSION: ${{ matrix.python-version }} @@ -418,7 +435,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 +505,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 +575,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..3f833b7169 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -132,6 +132,14 @@ 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 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 @@ -171,7 +179,8 @@ jobs: - name: Install tox run: uv pip install tox --system # we only run doctest on macos - - if: ${{ matrix.os == 'macos-latest' }} + # TODO: enable doctest for 3.14 + - if: ${{ matrix.os == 'macos-latest' && matrix.python-version != '3.14' }} name: Run doctests run: python -m tox -e "py${PYTHON_VERSION}-doctest-notudf-ci" env: @@ -183,7 +192,7 @@ jobs: # For example, see https://github.com/snowflakedb/snowpark-python/pull/681 shell: bash # do not run other tests for macos - - if: ${{ matrix.os != 'macos-latest' }} + - if: ${{ matrix.os != 'macos-latest' && matrix.python-version != '3.14' }} name: Run tests (excluding doctests) run: python -m tox -e "py${PYTHON_VERSION/\./}-notdoctest-ci" env: @@ -194,6 +203,19 @@ jobs: SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} shell: bash + # TODO: Remove the test below and run udf tests for 3.14 + # for 3.14, skip udf, doctest + - if: ${{ matrix.os != 'macos-latest' && matrix.python-version == '3.14' }} + name: Run tests (excluding udf, doctests) + run: python -m tox -e "py${PYTHON_VERSION/\./}-notudfdoctest-ci" + env: + PYTHON_VERSION: ${{ matrix.python-version }} + cloud_provider: ${{ matrix.cloud-provider }} + PYTEST_ADDOPTS: --color=yes --tb=short + TOX_PARALLEL_NO_SPINNER: 1 + SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} + SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} + shell: bash - name: Install MS ODBC Driver (Ubuntu only) if: ${{ contains(matrix.os, 'ubuntu') }} run: | @@ -204,7 +226,8 @@ jobs: shell: bash - name: Run data source tests # psycopg2 is not supported on macos 3.9 - if: ${{ !(matrix.os == 'macos-latest' && matrix.python-version == '3.9') }} + # TODO: enable datasource tests for 3.14 + if: ${{ !(matrix.os == 'macos-latest' && matrix.python-version == '3.9') && !(matrix.python-version == '3.14') }} run: python -m tox -e datasource env: PYTHON_VERSION: ${{ matrix.python-version }} @@ -233,7 +256,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 +459,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 9e14fa999f..979685ce20 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 067e3d5f06..afdb536db2 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/src/snowflake/snowpark/session.py b/src/snowflake/snowpark/session.py index f80255d00d..bcc1e9b6dc 100644 --- a/src/snowflake/snowpark/session.py +++ b/src/snowflake/snowpark/session.py @@ -4981,7 +4981,7 @@ def _get_or_register_xpath_udf( handler_name, return_type=return_type_map[return_type], input_types=[StringType(), StringType()], - packages=["snowflake-snowpark-python", "lxml<6"], + packages=["snowflake-snowpark-python", "lxml<=6.0.2"], replace=True, _emit_ast=False, _suppress_local_package_warnings=True, diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index 9df63dcc92..b5c8b2c56a 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -357,6 +357,7 @@ def session( "alter session set ENABLE_EXTRACTION_PUSHDOWN_EXTERNAL_PARQUET_FOR_COPY_PHASE_I='Track';" ).collect() session.sql("alter session set ENABLE_ROW_ACCESS_POLICY=true").collect() + session.sql("alter session set ENABLE_PYTHON_3_14=true").collect() try: yield session diff --git a/tests/integ/test_data_source_api.py b/tests/integ/test_data_source_api.py index fda65324bd..c0a67e3955 100644 --- a/tests/integ/test_data_source_api.py +++ b/tests/integ/test_data_source_api.py @@ -500,6 +500,7 @@ def assert_datasource_statement_params_run_query(*args, **kwargs): RUNNING_ON_JENKINS, reason="SNOW-2089683: oracledb real connection test failed on jenkins", ) +@pytest.mark.udf def test_telemetry_tracking_for_udtf(caplog, session, ast_enabled): if ast_enabled: @@ -1013,6 +1014,7 @@ def test_empty_table(session, fetch_with_process): assert df.collect() == [] +@pytest.mark.udf def test_sql_server_udtf_ingestion(session): raw_schema = [ ("Id", int, None, None, 10, 0, False), 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/integ/test_udf_profiler.py b/tests/integ/test_udf_profiler.py index 3a91390416..24b5384abd 100644 --- a/tests/integ/test_udf_profiler.py +++ b/tests/integ/test_udf_profiler.py @@ -38,6 +38,7 @@ def setup(profiler_session, resources_path, local_testing_mode): "config.getoption('local_testing_mode', default=False)", reason="session.sql is not supported in localtesting", ) +@pytest.mark.udf def test_udf_profiler_basic(profiler_session): @udf( name="str_udf", replace=True, return_type=StringType(), session=profiler_session @@ -65,6 +66,7 @@ def str_udf(): "config.getoption('local_testing_mode', default=False)", reason="session.sql is not supported in localtesting", ) +@pytest.mark.udf def test_anonymous_udf(profiler_session): add_one = udf( lambda x: x + 1, diff --git a/tests/integ/test_xpath.py b/tests/integ/test_xpath.py index cb051ae7c7..ea2f679737 100644 --- a/tests/integ/test_xpath.py +++ b/tests/integ/test_xpath.py @@ -32,6 +32,7 @@ "config.getoption('local_testing_mode', default=False)", reason="list command not supported in local testing mode", ), + pytest.mark.udf, ] 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 e92831ffc5..fec74cd6a1 100644 --- a/tox.ini +++ b/tox.ini @@ -182,7 +182,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