Skip to content

Commit 7601197

Browse files
SNOW-3375255: enable Python 3.14 UDF tests (#4172)
1 parent e7c3d2e commit 7601197

30 files changed

Lines changed: 486 additions & 185 deletions

.github/workflows/daily_precommit.yml

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ jobs:
614614
- name: Install tox
615615
run: uv pip install tox --system
616616
# TODO: enable doctest for 3.14
617-
- if: ${{ contains('macos', matrix.os.download_name) && matrix.python-version != '3.14' }}
617+
- if: ${{ contains('macos', matrix.os.download_name) }}
618618
name: Run doctests
619619
run: python -m tox -e "py${PYTHON_VERSION}-doctest-notudf-ci"
620620
env:
@@ -623,9 +623,7 @@ jobs:
623623
PYTEST_ADDOPTS: --color=yes --tb=short --disable_cte_optimization
624624
TOX_PARALLEL_NO_SPINNER: 1
625625
shell: bash
626-
# TODO: enable for 3.14
627-
- if: ${{ matrix.python-version != '3.14' }}
628-
name: Run tests (excluding doctests)
626+
- name: Run tests (excluding doctests)
629627
run: python -m tox -e "py${PYTHON_VERSION/\./}-dailynotdoctest-ci"
630628
env:
631629
PYTHON_VERSION: ${{ matrix.python-version }}
@@ -635,18 +633,6 @@ jobs:
635633
SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }}
636634
SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }}
637635
shell: bash
638-
# TODO: remove the test below and run udf tests for 3.14
639-
- if: ${{ matrix.python-version == '3.14' }}
640-
name: Run tests (excluding udf, doctests)
641-
run: python -m tox -e "py${PYTHON_VERSION/\./}-dailynotdoctestnotudf-ci"
642-
env:
643-
PYTHON_VERSION: ${{ matrix.python-version }}
644-
cloud_provider: ${{ matrix.cloud-provider }}
645-
PYTEST_ADDOPTS: --color=yes --tb=short --disable_cte_optimization
646-
TOX_PARALLEL_NO_SPINNER: 1
647-
SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }}
648-
SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }}
649-
shell: bash
650636
- name: Combine coverages
651637
run: python -m tox -e coverage --skip-missing-interpreters false
652638
shell: bash

.github/workflows/precommit.yml

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,7 @@ jobs:
175175
- name: Install tox
176176
run: uv pip install tox --system
177177
# we only run doctest on macos
178-
# TODO: enable doctest for 3.14
179-
- if: ${{ matrix.os == 'macos-latest' && matrix.python-version != '3.14' }}
178+
- if: ${{ matrix.os == 'macos-latest' }}
180179
name: Run doctests
181180
run: python -m tox -e "py${PYTHON_VERSION}-doctest-notudf-ci"
182181
env:
@@ -188,7 +187,7 @@ jobs:
188187
# For example, see https://github.com/snowflakedb/snowpark-python/pull/681
189188
shell: bash
190189
# do not run other tests for macos
191-
- if: ${{ matrix.os != 'macos-latest' && matrix.python-version != '3.14' }}
190+
- if: ${{ matrix.os != 'macos-latest' }}
192191
name: Run tests (excluding doctests)
193192
run: python -m tox -e "py${PYTHON_VERSION/\./}-notdoctest-ci"
194193
env:
@@ -199,19 +198,6 @@ jobs:
199198
SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }}
200199
SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }}
201200
shell: bash
202-
# TODO: Remove the test below and run udf tests for 3.14
203-
# for 3.14, skip udf, doctest
204-
- if: ${{ matrix.os != 'macos-latest' && matrix.python-version == '3.14' }}
205-
name: Run tests (excluding udf, doctests)
206-
run: python -m tox -e "py${PYTHON_VERSION/\./}-notudfdoctest-ci"
207-
env:
208-
PYTHON_VERSION: ${{ matrix.python-version }}
209-
cloud_provider: ${{ matrix.cloud-provider }}
210-
PYTEST_ADDOPTS: --color=yes --tb=short
211-
TOX_PARALLEL_NO_SPINNER: 1
212-
SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }}
213-
SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }}
214-
shell: bash
215201
- name: Install MS ODBC Driver (Ubuntu only)
216202
if: ${{ contains(matrix.os, 'ubuntu') }}
217203
run: |
@@ -222,8 +208,7 @@ jobs:
222208
shell: bash
223209
- name: Run data source tests
224210
# psycopg2 is not supported on macos 3.9
225-
# TODO: enable datasource tests for 3.14
226-
if: ${{ !(matrix.os == 'macos-latest' && matrix.python-version == '3.9') && !(matrix.python-version == '3.14') }}
211+
if: ${{ !(matrix.os == 'macos-latest' && matrix.python-version == '3.9') }}
227212
run: python -m tox -e datasource
228213
env:
229214
PYTHON_VERSION: ${{ matrix.python-version }}

src/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from snowflake.snowpark import Session
1515
from snowflake.snowpark.functions import to_timestamp
16+
from snowflake.snowpark.context import _DEFAULT_ARTIFACT_REPOSITORY
1617

1718
logging.getLogger("snowflake.connector").setLevel(logging.ERROR)
1819

@@ -72,6 +73,9 @@ def add_snowpark_session(doctest_namespace, pytestconfig):
7273
session.sql(
7374
f"GRANT ALL PRIVILEGES ON SCHEMA {TEST_SCHEMA} TO ROLE PUBLIC"
7475
).collect()
76+
session.sql(
77+
f"ALTER SCHEMA SET DEFAULT_PYTHON_ARTIFACT_REPOSITORY = {_DEFAULT_ARTIFACT_REPOSITORY}"
78+
).collect()
7579
session.use_schema(TEST_SCHEMA)
7680
doctest_namespace["session"] = session
7781
yield

src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,14 @@ def udtf_ingestion(
167167
statement_params: Optional[Dict[str, str]] = None,
168168
_emit_ast: bool = True,
169169
) -> "snowflake.snowpark.DataFrame":
170-
from snowflake.snowpark._internal.data_source.utils import UDTF_PACKAGE_MAP
170+
from snowflake.snowpark._internal.data_source.utils import (
171+
resolve_udtf_packages,
172+
)
173+
174+
resolved_packages = packages or resolve_udtf_packages(
175+
self.dbms_type,
176+
artifact_repository or session._get_default_artifact_repository(),
177+
)
171178

172179
udtf_name = random_name_for_temp_object(TempObjectType.FUNCTION)
173180
with measure_time() as udtf_register_time:
@@ -186,7 +193,7 @@ def udtf_ingestion(
186193
]
187194
),
188195
external_access_integrations=[external_access_integrations],
189-
packages=packages or UDTF_PACKAGE_MAP.get(self.dbms_type),
196+
packages=resolved_packages,
190197
imports=imports,
191198
artifact_repository=artifact_repository,
192199
statement_params=statement_params,

src/snowflake/snowpark/_internal/data_source/utils.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
from snowflake.snowpark._internal.data_source import DataSourceReader
3535
from snowflake.snowpark._internal.type_utils import convert_sp_to_sf_type
3636
from snowflake.snowpark._internal.utils import get_temp_type_for_object
37-
from snowflake.snowpark.exceptions import SnowparkDataframeReaderException
37+
from snowflake.snowpark.context import _PYPI_SHARED_REPOSITORY
38+
from snowflake.snowpark.exceptions import (
39+
SnowparkClientException,
40+
SnowparkDataframeReaderException,
41+
)
3842
from snowflake.snowpark.types import StructType
3943

4044
from typing import TYPE_CHECKING
@@ -98,7 +102,11 @@ class DRIVER_TYPE(str, Enum):
98102
DRIVER_TYPE.PYMYSQL: PymysqlDriver,
99103
}
100104

101-
UDTF_PACKAGE_MAP = {
105+
# Default UDTF package list, suitable for Snowflake's Anaconda shared
106+
# repository. The Snowflake Anaconda channel ships conda builds of these
107+
# packages with the necessary native libraries (e.g., libpq for psycopg2,
108+
# msodbcsql for pyodbc) bundled, so the source distribution names work.
109+
_ANACONDA_UDTF_PACKAGE_MAP = {
102110
DBMS_TYPE.ORACLE_DB: ["oracledb>=2.0.0,<4.0.0", "snowflake-snowpark-python"],
103111
DBMS_TYPE.SQLITE_DB: ["snowflake-snowpark-python"],
104112
DBMS_TYPE.SQL_SERVER_DB: [
@@ -114,6 +122,67 @@ class DRIVER_TYPE(str, Enum):
114122
DBMS_TYPE.MYSQL_DB: ["pymysql>=1.0.0,<2.0.0", "snowflake-snowpark-python"],
115123
}
116124

125+
# UDTF package list when using the PyPI shared repository. The server-side
126+
# UDTF install sandbox refuses to compile source distributions (sdists), so
127+
# every package here must be wheel-installable from PyPI. Differences from
128+
# the Anaconda map:
129+
# - Postgres uses ``psycopg2-binary`` because ``psycopg2`` on PyPI is
130+
# sdist-only; ``psycopg2-binary`` is the wheel-packaged equivalent.
131+
# - SQL Server has no PyPI-installable equivalent of ``msodbcsql`` (it is
132+
# Microsoft's ODBC driver, distributed as a system package), so the
133+
# UDTF path cannot work on PyPI today.
134+
# - Databricks depends on ``databricks-sql-connector``, which transitively
135+
# requires ``thrift``; ``thrift`` on PyPI is sdist-only for every
136+
# version, so the server cannot install it.
137+
# These PyPI gaps are independent of the Python version; they apply to any
138+
# session whose default artifact repository is PyPI (most commonly Python
139+
# 3.14+, where PyPI is the global default).
140+
_PYPI_UDTF_PACKAGE_MAP = {
141+
DBMS_TYPE.ORACLE_DB: ["oracledb>=2.0.0,<4.0.0", "snowflake-snowpark-python"],
142+
DBMS_TYPE.SQLITE_DB: ["snowflake-snowpark-python"],
143+
DBMS_TYPE.POSTGRES_DB: [
144+
"psycopg2-binary>=2.0.0,<3.0.0",
145+
"snowflake-snowpark-python",
146+
],
147+
DBMS_TYPE.MYSQL_DB: ["pymysql>=1.0.0,<2.0.0", "snowflake-snowpark-python"],
148+
# SQL_SERVER_DB and DATABRICKS_DB intentionally omitted - see
149+
# resolve_udtf_packages for the user-facing error.
150+
}
151+
152+
# Backwards-compatible alias for callers (and external code) that imported
153+
# the old map. New code should use :func:`resolve_udtf_packages`.
154+
UDTF_PACKAGE_MAP = _ANACONDA_UDTF_PACKAGE_MAP
155+
156+
157+
def resolve_udtf_packages(
158+
dbms_type: "DBMS_TYPE", artifact_repository: Optional[str]
159+
) -> List[str]:
160+
"""Return the default UDTF package list for ``dbms_type``.
161+
162+
Picks the package list appropriate for ``artifact_repository``. When the
163+
repository is the PyPI shared repository, some DBMSes have no working
164+
package set (their dependencies are not wheel-installable from PyPI, and
165+
the server-side UDTF sandbox refuses to compile sdists); this raises
166+
:class:`SnowparkClientException` with guidance to switch repositories.
167+
"""
168+
if artifact_repository == _PYPI_SHARED_REPOSITORY:
169+
packages = _PYPI_UDTF_PACKAGE_MAP.get(dbms_type)
170+
if packages is None:
171+
raise SnowparkClientException(
172+
f"DataFrameReader.dbapi server-side UDTF ingestion for "
173+
f"{dbms_type.value} is not supported when the session's "
174+
f"default artifact repository is PyPI: the required "
175+
f"packages are not wheel-installable from PyPI, and the "
176+
f"server-side UDTF install sandbox refuses to compile "
177+
f"source distributions. Switch to the Anaconda artifact "
178+
f"repository (on Python 3.14+, PyPI is the client-side "
179+
f"default; on older Python versions, Anaconda is the "
180+
f"default but may have been overridden at the account, "
181+
f"database, or schema level)."
182+
)
183+
return packages
184+
return _ANACONDA_UDTF_PACKAGE_MAP.get(dbms_type)
185+
117186

118187
def get_jdbc_dbms(jdbc_url: str) -> str:
119188
"""

src/snowflake/snowpark/_internal/udf_utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ def resolve_imports_and_packages(
12471247
if artifact_repository != _ANACONDA_SHARED_REPOSITORY:
12481248
# Non-conda artifact repository - skip conda-based package resolution
12491249
resolved_packages = []
1250-
if not packages and session:
1250+
if packages is None and session:
12511251
resolved_packages = list(
12521252
session._resolve_packages(
12531253
[],
@@ -1256,7 +1256,7 @@ def resolve_imports_and_packages(
12561256
include_pandas=is_pandas_udf,
12571257
)
12581258
)
1259-
elif packages:
1259+
elif packages is not None:
12601260
if not all(isinstance(package, str) for package in packages):
12611261
raise TypeError(
12621262
"Non-conda artifact repository requires that all packages be passed as str."
@@ -1615,7 +1615,6 @@ def create_python_udf_or_sp(
16151615
is_ddl_on_temp_object=not is_permanent,
16161616
statement_params=statement_params,
16171617
)
1618-
16191618
if comment is not None:
16201619
object_signature_sql = f"{object_name}({','.join(input_sql_types)})"
16211620
comment = escape_single_quotes(comment)

src/snowflake/snowpark/context.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"""Context module for Snowpark."""
77
import logging
8+
import sys
89
from typing import Callable, Optional
910

1011
import snowflake.snowpark
@@ -168,8 +169,14 @@
168169

169170
# The fully qualified name of the Anaconda shared repository (conda channel).
170171
_ANACONDA_SHARED_REPOSITORY = "snowflake.snowpark.anaconda_shared_repository"
171-
# In case of failures or the current default artifact repository is unset, we fallback to this
172-
_DEFAULT_ARTIFACT_REPOSITORY = _ANACONDA_SHARED_REPOSITORY
172+
# The fully qualified name of the PyPI shared repository (pypi channel).
173+
_PYPI_SHARED_REPOSITORY = "snowflake.snowpark.pypi_shared_repository"
174+
# In case of failures and for routing to the right session package store, we use this
175+
_DEFAULT_ARTIFACT_REPOSITORY = (
176+
_ANACONDA_SHARED_REPOSITORY
177+
if sys.version_info < (3, 14)
178+
else _PYPI_SHARED_REPOSITORY
179+
)
173180

174181

175182
def configure_development_features(

tests/integ/conftest.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
#
55
import os
66
from logging import getLogger
7-
import sys
87
from typing import Dict
98

109
import pytest
1110

1211
import snowflake.connector
1312
from snowflake.snowpark import Session
13+
from snowflake.snowpark.context import _DEFAULT_ARTIFACT_REPOSITORY
1414
from snowflake.snowpark._internal.utils import set_ast_state, AstFlagSource
1515
from snowflake.snowpark.exceptions import SnowparkSQLException
1616
from snowflake.snowpark.mock._connection import MockServerConnection
1717
from tests.ast.ast_test_utils import (
1818
close_full_ast_validation_mode,
1919
setup_full_ast_validation_mode,
2020
)
21+
from tests.integ.session_parameters import set_up_test_session_parameters
2122
from tests.parameters import CONNECTION_PARAMETERS
2223
from tests.utils import (
2324
TEST_SCHEMA,
@@ -286,6 +287,9 @@ def test_schema(connection, local_testing_mode) -> None:
286287
cursor.execute(
287288
f"GRANT ALL PRIVILEGES ON SCHEMA {TEST_SCHEMA} TO ROLE PUBLIC"
288289
)
290+
cursor.execute(
291+
f"ALTER SCHEMA SET DEFAULT_PYTHON_ARTIFACT_REPOSITORY = {_DEFAULT_ARTIFACT_REPOSITORY}"
292+
)
289293
yield
290294
cursor.execute(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA}")
291295

@@ -355,13 +359,7 @@ def session(
355359
)
356360

357361
# TODO: SNOW-2346239: Set parameter on user level instead of in config file
358-
if not local_testing_mode:
359-
session.sql(
360-
"alter session set ENABLE_EXTRACTION_PUSHDOWN_EXTERNAL_PARQUET_FOR_COPY_PHASE_I='Track';"
361-
).collect()
362-
session.sql("alter session set ENABLE_ROW_ACCESS_POLICY=true").collect()
363-
if sys.version_info.major == 3 and sys.version_info.minor == 14:
364-
session.sql("alter session set ENABLE_PYTHON_3_14=true").collect()
362+
set_up_test_session_parameters(session, local_testing_mode)
365363

366364
try:
367365
yield session
@@ -412,6 +410,7 @@ def profiler_session(
412410
integration2,
413411
integration3,
414412
)
413+
set_up_test_session_parameters(session, local_testing_mode)
415414
try:
416415
yield session
417416
finally:
@@ -434,6 +433,9 @@ def temp_schema(connection, session, local_testing_mode) -> None:
434433
cursor.execute(
435434
f"GRANT ALL PRIVILEGES ON SCHEMA {temp_schema_name} TO ROLE PUBLIC"
436435
)
436+
cursor.execute(
437+
f"ALTER SCHEMA SET DEFAULT_PYTHON_ARTIFACT_REPOSITORY = {_DEFAULT_ARTIFACT_REPOSITORY}"
438+
)
437439
yield temp_schema_name
438440
cursor.execute(f"DROP SCHEMA IF EXISTS {temp_schema_name}")
439441

tests/integ/datasource/test_databricks.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
databricks_unicode_schema,
4343
databricks_double_quoted_schema,
4444
)
45-
from tests.utils import IS_IN_STORED_PROC, IS_MACOS, Utils
45+
from tests.utils import IS_IN_STORED_PROC, IS_MACOS, Utils, IS_PY314
4646

4747
DEPENDENCIES_PACKAGE_UNAVAILABLE = True
4848
try:
@@ -177,6 +177,10 @@ def test_double_quoted_column_databricks(session, custom_schema):
177177
[("table", TEST_TABLE_NAME), ("query", f"(SELECT * FROM {TEST_TABLE_NAME})")],
178178
)
179179
@pytest.mark.udf
180+
@pytest.mark.skipif(
181+
IS_PY314,
182+
reason="databricks-sql-connector's thrift dependency has no Python 3.14 wheel on PyPI; server-side UDTF install fails on the default repo.",
183+
)
180184
def test_udtf_ingestion_databricks(session, input_type, input_value, caplog):
181185
# we define here to avoid test_databricks.py to be pickled and unpickled in UDTF
182186
def local_create_databricks_connection():
@@ -286,6 +290,10 @@ def test_session_init(session):
286290
)
287291

288292

293+
@pytest.mark.skipif(
294+
IS_PY314,
295+
reason="databricks-sql-connector's thrift dependency has no Python 3.14 wheel on PyPI; server-side UDTF install fails on the default repo.",
296+
)
289297
def test_session_init_udtf(session):
290298
udtf_configs = {
291299
"external_access_integration": DATABRICKS_TEST_EXTERNAL_ACCESS_INTEGRATION

0 commit comments

Comments
 (0)