From 4d38705f3921dfdf2994b1f6db1f680bd0d927d2 Mon Sep 17 00:00:00 2001 From: Tishj Date: Mon, 16 Oct 2023 12:30:51 +0200 Subject: [PATCH 1/2] enable eval_str in python >= 3.10 --- tools/pythonpkg/src/python_udf.cpp | 15 +++++++++-- .../tests/fast/test_string_annotation.py | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 tools/pythonpkg/tests/fast/test_string_annotation.py diff --git a/tools/pythonpkg/src/python_udf.cpp b/tools/pythonpkg/src/python_udf.cpp index 66ae25e01da2..54494aeb39af 100644 --- a/tools/pythonpkg/src/python_udf.cpp +++ b/tools/pythonpkg/src/python_udf.cpp @@ -279,9 +279,20 @@ struct PythonUDFData { } } - void AnalyzeSignature(const py::object &udf) { + py::object GetSignature(const py::object &udf) { + const int32_t PYTHON_3_10_HEX = 0x030a00f0; + auto python_version = PY_VERSION_HEX; + auto signature_func = py::module_::import("inspect").attr("signature"); - auto signature = signature_func(udf); + if (python_version >= PYTHON_3_10_HEX) { + return signature_func(udf, py::arg("eval_str") = true); + } else { + return signature_func(udf); + } + } + + void AnalyzeSignature(const py::object &udf) { + auto signature = GetSignature(udf); auto sig_params = signature.attr("parameters"); auto return_annotation = signature.attr("return_annotation"); if (!py::none().is(return_annotation)) { diff --git a/tools/pythonpkg/tests/fast/test_string_annotation.py b/tools/pythonpkg/tests/fast/test_string_annotation.py new file mode 100644 index 000000000000..44de86a27e02 --- /dev/null +++ b/tools/pythonpkg/tests/fast/test_string_annotation.py @@ -0,0 +1,27 @@ +import duckdb +import os +import pandas as pd +import pytest +from typing import Union + +# This is used to opt in to PEP 563 (string annotations) +from __future__ import annotations + + +def make_annotated_function(type): + # Create a function that returns its input + def test_base(x): + return x + + import types + + test_function = types.FunctionType( + test_base.__code__, test_base.__globals__, test_base.__name__, test_base.__defaults__, test_base.__closure__ + ) + # Add annotations for the return type and 'x' + test_function.__annotations__ = {'return': type, 'x': type} + return test_function + +class TestStringAnnotation(object): + def test_string_annotations(self): + pass From 8ce3f48b06327dec40f1ad9a77782af8e03ab4d6 Mon Sep 17 00:00:00 2001 From: Tishj Date: Mon, 16 Oct 2023 12:59:43 +0200 Subject: [PATCH 2/2] we cant make use of __future__ annotations feature in generated functions - just use strings instead --- .../tests/fast/test_string_annotation.py | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/tools/pythonpkg/tests/fast/test_string_annotation.py b/tools/pythonpkg/tests/fast/test_string_annotation.py index 44de86a27e02..c5500c663ee1 100644 --- a/tools/pythonpkg/tests/fast/test_string_annotation.py +++ b/tools/pythonpkg/tests/fast/test_string_annotation.py @@ -1,27 +1,53 @@ import duckdb -import os -import pandas as pd import pytest +import sys from typing import Union -# This is used to opt in to PEP 563 (string annotations) -from __future__ import annotations - -def make_annotated_function(type): - # Create a function that returns its input - def test_base(x): - return x +def make_annotated_function(type: str): + def test_base(): + return None import types test_function = types.FunctionType( test_base.__code__, test_base.__globals__, test_base.__name__, test_base.__defaults__, test_base.__closure__ ) - # Add annotations for the return type and 'x' - test_function.__annotations__ = {'return': type, 'x': type} + # Add the 'type' string as return_annotation + test_function.__annotations__ = {'return': type} return test_function + +def python_version_lower_than_3_10(): + import sys + + if sys.version_info[0] < 3: + return True + if sys.version_info[1] < 10: + return True + return False + + class TestStringAnnotation(object): - def test_string_annotations(self): - pass + @pytest.mark.skipif( + python_version_lower_than_3_10(), reason="inspect.signature(eval_str=True) only supported since 3.10 and higher" + ) + @pytest.mark.parametrize( + ['input', 'expected'], + [ + ('str', 'VARCHAR'), + ('list[str]', 'VARCHAR[]'), + ('dict[str, str]', 'MAP(VARCHAR, VARCHAR)'), + ('dict[Union[str, bool], str]', 'MAP(UNION(u1 VARCHAR, u2 BOOLEAN), VARCHAR)'), + ], + ) + def test_string_annotations(self, duckdb_cursor, input, expected): + from inspect import signature + + func = make_annotated_function(input) + sig = signature(func) + assert sig.return_annotation.__class__ == str + + duckdb_cursor.create_function("foo", func) + rel = duckdb_cursor.sql("select foo()") + assert rel.types == [expected]