This is an automated email from the ASF dual-hosted git repository.
HyukjinKwon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark-connect-rust.git
The following commit(s) were added to refs/heads/master by this push:
new 254dd50 [SPARK-59143] Close remaining PySpark 4.2.0 Spark Connect
public-API parity gaps
254dd50 is described below
commit 254dd506ad732ee83f7b1d720fcca0678be0dd33
Author: Hyukjin Kwon <[email protected]>
AuthorDate: Tue Sep 1 15:35:52 2026 +0900
[SPARK-59143] Close remaining PySpark 4.2.0 Spark Connect public-API parity
gaps
## What changes were proposed in this pull request?
Closes the remaining PySpark 4.2.0 Spark Connect public-API parity gaps
found by a
close `inspect.signature` diff of the drop-in against the reference
`pyspark==4.2.0`
Connect client.
**Import-path gaps** — thin re-export shims, same pattern as the existing
`connect/dataframe.py`:
- New `pyspark.sql.connect.*` submodules: `catalog`, `conf`, `group`,
`window`,
`readwriter`, `tvf`, `udf`, `udtf`, `observation`, `merge`,
`streaming.readwriter`, `streaming.query`. The reference client and the
official
Connect test suite import from these paths, which previously raised
`ImportError`.
- New public `pyspark.sql.conf` (exposing `RuntimeConfig`, aliasing the
Connect
`RuntimeConf`) and `pyspark.sql.tvf` modules.
**Signature gaps** vs the reference Connect client:
- `StructType.treeString` gains the `maxDepth` parameter (default
`2147483647`),
wired through a new `DataType::tree_string_with_depth` in the core;
`tree_string()` is preserved for existing callers.
- `StructType.fromJson` / `StructField.fromJson` parameter renamed `data` →
`json`.
- `TableValuedFunction.json_tuple` / `stack` now take varargs `*fields`.
- `VariantType.toInternal` parameter renamed `obj` → `variant`.
The `pyspark.sql.functions` surface was already at full parity (all 529
reference
functions, no signature mismatches) and is unchanged.
## Why are the changes needed?
The package advertises drop-in parity with the PySpark 4.2.0 Spark Connect
client.
These were the residual deviations where reference-valid imports and calls
could
break.
## Does this PR introduce any user-facing change?
Yes, additive: previously-failing imports now resolve, and the four methods
above
accept the reference parameter names/forms.
## How was this patch tested?
New offline tests in `python/tests/test_dropin_offline.py`:
- `test_connect_submodule_import_paths` (14 paths) — passes against the
built extension.
- Behavior tests for `treeString(maxDepth)`, `fromJson(json=...)`, and
`VariantType.toInternal(variant=...)` — validated against reference
`pyspark==4.2.0` to confirm they encode the correct behavior; they
exercise the
rebuilt extension under CI.
The Rust changes were authored but not compiled on the dev box (offline);
the
build + official Connect suite in CI is the build/behavior gate.
This pull request and its description were written by Isaac.
Closes #95 from HyukjinKwon/SPARK-59143-close-connect-parity-gaps.
Authored-by: Hyukjin Kwon <[email protected]>
Signed-off-by: Hyukjin Kwon <[email protected]>
---
crates/pyspark-rs/src/tvf.rs | 3 +-
crates/pyspark-rs/src/types.rs | 21 +++----
crates/spark-connect/src/types.rs | 37 +++++++++---
crates/spark-connect/tests/types_behavior.rs | 28 ++++++++++
python/pyspark/sql/conf.py | 23 ++++++++
python/pyspark/sql/connect/catalog.py | 28 ++++++++++
python/pyspark/sql/connect/conf.py | 20 +++++++
python/pyspark/sql/connect/group.py | 21 +++++++
python/pyspark/sql/connect/merge.py | 20 +++++++
python/pyspark/sql/connect/observation.py | 20 +++++++
python/pyspark/sql/connect/readwriter.py | 24 ++++++++
python/pyspark/sql/connect/streaming/query.py | 20 +++++++
python/pyspark/sql/connect/streaming/readwriter.py | 20 +++++++
python/pyspark/sql/connect/tvf.py | 20 +++++++
python/pyspark/sql/connect/udf.py | 20 +++++++
python/pyspark/sql/connect/udtf.py | 25 +++++++++
python/pyspark/sql/connect/window.py | 20 +++++++
python/pyspark/sql/tvf.py | 20 +++++++
python/tests/test_dropin_offline.py | 65 ++++++++++++++++++++++
19 files changed, 435 insertions(+), 20 deletions(-)
diff --git a/crates/pyspark-rs/src/tvf.rs b/crates/pyspark-rs/src/tvf.rs
index 22cf760..523e4bd 100644
--- a/crates/pyspark-rs/src/tvf.rs
+++ b/crates/pyspark-rs/src/tvf.rs
@@ -75,7 +75,7 @@ impl PyTableValuedFunction {
))
}
- #[pyo3(name = "json_tuple")]
+ #[pyo3(name = "json_tuple", signature = (input, *fields))]
fn json_tuple(
&self,
py: Python<'_>,
@@ -108,6 +108,7 @@ impl PyTableValuedFunction {
))
}
+ #[pyo3(signature = (n, *fields))]
fn stack(
&self,
py: Python<'_>,
diff --git a/crates/pyspark-rs/src/types.rs b/crates/pyspark-rs/src/types.rs
index ad89a8b..b58db7c 100644
--- a/crates/pyspark-rs/src/types.rs
+++ b/crates/pyspark-rs/src/types.rs
@@ -1513,12 +1513,12 @@ impl PyStructField {
fn __sf_from_json(
_cls: &Bound<'_, pyo3::types::PyType>,
py: Python<'_>,
- data: &Bound<'_, PyAny>,
+ json: &Bound<'_, PyAny>,
) -> PyResult<PyStructField> {
let s: String = py
.import("json")?
.getattr("dumps")?
- .call1((data,))?
+ .call1((json,))?
.extract()?;
let field = StructField::from_json_str(&s)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError,
_>(e.to_string()))?;
@@ -1808,13 +1808,14 @@ impl PyStructType {
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError,
_>(e.to_string()))
}
- /// Tree string. Mirrors `StructType.treeString()`.
- #[pyo3(name = "treeString")]
- fn __st_tree_string(&self) -> PyResult<String> {
+ /// Tree string. Mirrors `StructType.treeString(maxDepth)`.
+ #[allow(non_snake_case)]
+ #[pyo3(name = "treeString", signature = (maxDepth = 2147483647))]
+ fn __st_tree_string(&self, maxDepth: i32) -> PyResult<String> {
DataType::Struct {
fields: self.fields.clone(),
}
- .tree_string()
+ .tree_string_with_depth(maxDepth)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError,
_>(e.to_string()))
}
@@ -1838,12 +1839,12 @@ impl PyStructType {
fn __st_from_json(
_cls: &Bound<'_, pyo3::types::PyType>,
py: Python<'_>,
- data: &Bound<'_, PyAny>,
+ json: &Bound<'_, PyAny>,
) -> PyResult<Py<PyStructType>> {
let s: String = py
.import("json")?
.getattr("dumps")?
- .call1((data,))?
+ .call1((json,))?
.extract()?;
match DataType::from_json_str(&s)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError,
_>(e.to_string()))?
@@ -2319,8 +2320,8 @@ impl PyVariantType {
obj
}
#[pyo3(name = "toInternal")]
- fn __obj_to_internal<'py>(&self, obj: Bound<'py, PyAny>) -> Bound<'py,
PyAny> {
- obj
+ fn __obj_to_internal<'py>(&self, variant: Bound<'py, PyAny>) -> Bound<'py,
PyAny> {
+ variant
}
#[classmethod]
#[pyo3(name = "fromDDL")]
diff --git a/crates/spark-connect/src/types.rs
b/crates/spark-connect/src/types.rs
index aa8a42d..1f06c73 100644
--- a/crates/spark-connect/src/types.rs
+++ b/crates/spark-connect/src/types.rs
@@ -855,10 +855,17 @@ impl DataType {
/// Tree-string for a StructType, mirroring `StructType.treeString()`.
pub fn tree_string(&self) -> Result<String> {
+ self.tree_string_with_depth(i32::MAX)
+ }
+
+ /// Like [`DataType::tree_string`], but stops recursing into nested structs
+ /// once `max_depth` nesting levels have been printed (top-level fields are
+ /// depth 1). Mirrors `StructType.treeString(maxDepth)`.
+ pub fn tree_string_with_depth(&self, max_depth: i32) -> Result<String> {
match self {
DataType::Struct { .. } => {
let mut out = String::from("root\n");
- self.append_tree(&mut out, " |");
+ self.append_tree(&mut out, " |", 1, max_depth);
Ok(out)
}
_ => Err(SparkError::value(
@@ -868,19 +875,31 @@ impl DataType {
}
}
- fn append_tree(&self, out: &mut String, prefix: &str) {
+ fn append_tree(&self, out: &mut String, prefix: &str, depth: i32,
max_depth: i32) {
if let DataType::Struct { fields } = self {
for f in fields {
+ // A nested struct is rendered as the bare type name
("struct") and its
+ // fields are shown by recursing below, so `max_depth` can
truncate them.
+ // Using `simple_string()` here would inline the children (e.g.
+ // "struct<inner:int>"), leaking them past `max_depth`.
Non-struct types
+ // have no depth-controlled children, so their simple string
is used.
+ let is_struct = matches!(f.data_type, DataType::Struct { .. });
+ let type_repr = if is_struct {
+ f.data_type.type_name()
+ } else {
+ f.data_type.simple_string()
+ };
out.push_str(&format!(
"{}-- {}: {} (nullable = {})\n",
- prefix,
- f.name,
- f.data_type.simple_string(),
- f.nullable
+ prefix, f.name, type_repr, f.nullable
));
- if matches!(f.data_type, DataType::Struct { .. }) {
- f.data_type
- .append_tree(out, &format!("{} |",
prefix.trim_end_matches('|')));
+ if is_struct && depth < max_depth {
+ f.data_type.append_tree(
+ out,
+ &format!("{} |", prefix.trim_end_matches('|')),
+ depth + 1,
+ max_depth,
+ );
}
}
}
diff --git a/crates/spark-connect/tests/types_behavior.rs
b/crates/spark-connect/tests/types_behavior.rs
index 95cfa1d..8f32b79 100644
--- a/crates/spark-connect/tests/types_behavior.rs
+++ b/crates/spark-connect/tests/types_behavior.rs
@@ -293,6 +293,34 @@ fn struct_to_ddl_tree_and_nullable() {
assert!(DataType::Integer.tree_string().is_err());
}
+#[test]
+fn tree_string_with_depth_truncates_nested_structs() {
+ use spark_connect::types::StructField;
+ let nested = DataType::Struct {
+ fields: vec![StructField {
+ name: "outer".to_string(),
+ data_type: DataType::Struct {
+ fields: vec![StructField {
+ name: "inner".to_string(),
+ data_type: DataType::Integer,
+ nullable: true,
+ metadata: Default::default(),
+ }],
+ },
+ nullable: true,
+ metadata: Default::default(),
+ }],
+ };
+ // Full depth shows the nested field, rendering the struct as the bare
type name.
+ let full = nested.tree_string().unwrap();
+ assert!(full.contains("outer: struct (nullable = true)"));
+ assert!(full.contains("inner: int (nullable = true)"));
+ // max_depth = 1 prints only the top level; the nested field is omitted.
+ let shallow = nested.tree_string_with_depth(1).unwrap();
+ assert!(shallow.contains("outer: struct (nullable = true)"));
+ assert!(!shallow.contains("inner"));
+}
+
#[test]
fn from_json_str_roundtrips() {
use spark_connect::types::StructField;
diff --git a/python/pyspark/sql/conf.py b/python/pyspark/sql/conf.py
new file mode 100644
index 0000000..c873ce6
--- /dev/null
+++ b/python/pyspark/sql/conf.py
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Runtime configuration. The connect runtime-config object is the Rust-backed
RuntimeConf; RuntimeConfig is the public reference name and aliases it."""
+from pyspark._pyspark import RuntimeConf
+
+# Public reference name; the Spark Connect runtime config is RuntimeConf.
+RuntimeConfig = RuntimeConf
+
+__all__ = ["RuntimeConfig", "RuntimeConf"]
diff --git a/python/pyspark/sql/connect/catalog.py
b/python/pyspark/sql/connect/catalog.py
new file mode 100644
index 0000000..2646122
--- /dev/null
+++ b/python/pyspark/sql/connect/catalog.py
@@ -0,0 +1,28 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Catalog under the connect import path (mirrors
pyspark.sql.connect.catalog)."""
+from pyspark.sql.catalog import (
+ Catalog,
+ CatalogMetadata,
+ Column,
+ Database,
+ Function,
+ Table,
+ TablePartition,
+)
+
+__all__ = ["Catalog", "CatalogMetadata", "Column", "Database", "Function",
"Table", "TablePartition"]
diff --git a/python/pyspark/sql/connect/conf.py
b/python/pyspark/sql/connect/conf.py
new file mode 100644
index 0000000..0ba8a10
--- /dev/null
+++ b/python/pyspark/sql/connect/conf.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""RuntimeConf under the connect import path (mirrors
pyspark.sql.connect.conf; the connect runtime-config class is named RuntimeConf
upstream)."""
+from pyspark._pyspark import RuntimeConf
+
+__all__ = ["RuntimeConf"]
diff --git a/python/pyspark/sql/connect/group.py
b/python/pyspark/sql/connect/group.py
new file mode 100644
index 0000000..f866e1f
--- /dev/null
+++ b/python/pyspark/sql/connect/group.py
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""GroupedData / PandasCogroupedOps under the connect import path."""
+from pyspark.sql.group import GroupedData
+from pyspark.sql.pandas.group_ops import PandasCogroupedOps
+
+__all__ = ["GroupedData", "PandasCogroupedOps"]
diff --git a/python/pyspark/sql/connect/merge.py
b/python/pyspark/sql/connect/merge.py
new file mode 100644
index 0000000..e509995
--- /dev/null
+++ b/python/pyspark/sql/connect/merge.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""MergeIntoWriter under the connect import path."""
+from pyspark.sql.merge import MergeIntoWriter
+
+__all__ = ["MergeIntoWriter"]
diff --git a/python/pyspark/sql/connect/observation.py
b/python/pyspark/sql/connect/observation.py
new file mode 100644
index 0000000..066b195
--- /dev/null
+++ b/python/pyspark/sql/connect/observation.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Observation under the connect import path."""
+from pyspark.sql.observation import Observation
+
+__all__ = ["Observation"]
diff --git a/python/pyspark/sql/connect/readwriter.py
b/python/pyspark/sql/connect/readwriter.py
new file mode 100644
index 0000000..35ab99d
--- /dev/null
+++ b/python/pyspark/sql/connect/readwriter.py
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""DataFrameReader / DataFrameWriter(V2) under the connect import path."""
+from pyspark.sql.readwriter import (
+ DataFrameReader,
+ DataFrameWriter,
+ DataFrameWriterV2,
+)
+
+__all__ = ["DataFrameReader", "DataFrameWriter", "DataFrameWriterV2"]
diff --git a/python/pyspark/sql/connect/streaming/query.py
b/python/pyspark/sql/connect/streaming/query.py
new file mode 100644
index 0000000..0bda959
--- /dev/null
+++ b/python/pyspark/sql/connect/streaming/query.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""StreamingQuery / StreamingQueryManager under the connect import path."""
+from pyspark.sql.streaming.query import StreamingQuery, StreamingQueryManager
+
+__all__ = ["StreamingQuery", "StreamingQueryManager"]
diff --git a/python/pyspark/sql/connect/streaming/readwriter.py
b/python/pyspark/sql/connect/streaming/readwriter.py
new file mode 100644
index 0000000..61cc912
--- /dev/null
+++ b/python/pyspark/sql/connect/streaming/readwriter.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""DataStreamReader / DataStreamWriter under the connect import path."""
+from pyspark.sql.streaming.readwriter import DataStreamReader, DataStreamWriter
+
+__all__ = ["DataStreamReader", "DataStreamWriter"]
diff --git a/python/pyspark/sql/connect/tvf.py
b/python/pyspark/sql/connect/tvf.py
new file mode 100644
index 0000000..24d81d9
--- /dev/null
+++ b/python/pyspark/sql/connect/tvf.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""TableValuedFunction under the connect import path (mirrors
pyspark.sql.connect.tvf)."""
+from pyspark._pyspark import TableValuedFunction
+
+__all__ = ["TableValuedFunction"]
diff --git a/python/pyspark/sql/connect/udf.py
b/python/pyspark/sql/connect/udf.py
new file mode 100644
index 0000000..3b7daaa
--- /dev/null
+++ b/python/pyspark/sql/connect/udf.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""UserDefinedFunction / UDFRegistration under the connect import path."""
+from pyspark.sql.udf import UDFRegistration, UserDefinedFunction
+
+__all__ = ["UDFRegistration", "UserDefinedFunction"]
diff --git a/python/pyspark/sql/connect/udtf.py
b/python/pyspark/sql/connect/udtf.py
new file mode 100644
index 0000000..a7034ca
--- /dev/null
+++ b/python/pyspark/sql/connect/udtf.py
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""User-defined table function classes under the connect import path."""
+from pyspark.sql.udtf import (
+ AnalyzeArgument,
+ AnalyzeResult,
+ UDTFRegistration,
+ UserDefinedTableFunction,
+)
+
+__all__ = ["AnalyzeArgument", "AnalyzeResult", "UDTFRegistration",
"UserDefinedTableFunction"]
diff --git a/python/pyspark/sql/connect/window.py
b/python/pyspark/sql/connect/window.py
new file mode 100644
index 0000000..5bfb3aa
--- /dev/null
+++ b/python/pyspark/sql/connect/window.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Window / WindowSpec under the connect import path."""
+from pyspark.sql.window import Window, WindowSpec
+
+__all__ = ["Window", "WindowSpec"]
diff --git a/python/pyspark/sql/tvf.py b/python/pyspark/sql/tvf.py
new file mode 100644
index 0000000..e7c84d3
--- /dev/null
+++ b/python/pyspark/sql/tvf.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Table-valued functions (spark.tvf.*)."""
+from pyspark._pyspark import TableValuedFunction
+
+__all__ = ["TableValuedFunction"]
diff --git a/python/tests/test_dropin_offline.py
b/python/tests/test_dropin_offline.py
index dc01a95..6c74762 100644
--- a/python/tests/test_dropin_offline.py
+++ b/python/tests/test_dropin_offline.py
@@ -2664,3 +2664,68 @@ def test_no_pyclass_reports_builtins_module():
if isinstance(obj, type) and obj.__module__ == "builtins"
)
assert offenders == [], f"missing module=: {offenders}"
+
+
+# ------------------------------------------------- connect.* import-path
parity
+
+# The reference Spark Connect client exposes these classes under the
+# pyspark.sql.connect.* package (and pyspark.sql.conf / pyspark.sql.tvf);
+# existing code and the official test suite import from these paths.
[email protected](
+ "module, names",
+ [
+ ("pyspark.sql.connect.catalog",
+ ["Catalog", "Database", "Table", "Function", "CatalogMetadata",
"TablePartition"]),
+ ("pyspark.sql.connect.conf", ["RuntimeConf"]),
+ ("pyspark.sql.connect.group", ["GroupedData", "PandasCogroupedOps"]),
+ ("pyspark.sql.connect.window", ["Window", "WindowSpec"]),
+ ("pyspark.sql.connect.readwriter",
+ ["DataFrameReader", "DataFrameWriter", "DataFrameWriterV2"]),
+ ("pyspark.sql.connect.tvf", ["TableValuedFunction"]),
+ ("pyspark.sql.connect.udf", ["UserDefinedFunction",
"UDFRegistration"]),
+ ("pyspark.sql.connect.udtf",
+ ["UserDefinedTableFunction", "UDTFRegistration", "AnalyzeArgument",
"AnalyzeResult"]),
+ ("pyspark.sql.connect.observation", ["Observation"]),
+ ("pyspark.sql.connect.merge", ["MergeIntoWriter"]),
+ ("pyspark.sql.connect.streaming.readwriter", ["DataStreamReader",
"DataStreamWriter"]),
+ ("pyspark.sql.connect.streaming.query", ["StreamingQuery",
"StreamingQueryManager"]),
+ ("pyspark.sql.conf", ["RuntimeConfig", "RuntimeConf"]),
+ ("pyspark.sql.tvf", ["TableValuedFunction"]),
+ ],
+)
+def test_connect_submodule_import_paths(module, names):
+ import importlib
+
+ mod = importlib.import_module(module)
+ missing = [n for n in names if not hasattr(mod, n)]
+ assert missing == [], f"{module} is missing {missing}"
+
+
+# --------------------------------------------- v4.2.0 signature-parity fixes
+
+def test_structtype_from_json_accepts_json_kwarg():
+ # Reference names the fromJson parameter `json`, not `data`.
+ import json as _json
+
+ st = T.StructType([T.StructField("a", T.IntegerType()), T.StructField("b",
T.StringType())])
+ payload = _json.loads(st.json())
+ assert T.StructType.fromJson(json=payload).simpleString() ==
st.simpleString()
+
+ sf = T.StructField("a", T.IntegerType())
+ assert T.StructField.fromJson(json=_json.loads(sf.json())).name == "a"
+
+
+def test_structtype_tree_string_max_depth():
+ nested = T.StructType([
+ T.StructField("outer", T.StructType([T.StructField("inner",
T.IntegerType())])),
+ ])
+ full = nested.treeString()
+ assert "inner" in full
+ # maxDepth=1 prints only the top level; the nested field is omitted.
+ assert "inner" not in nested.treeString(1)
+ assert "outer" in nested.treeString(1)
+
+
+def test_variant_type_to_internal_accepts_variant_kwarg():
+ # Reference names the VariantType.toInternal parameter `variant`, not
`obj`.
+ assert T.VariantType().toInternal(variant=None) is None
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]