From 8f7f4d47d286f52d2b642b388334204bb3bf3e6b Mon Sep 17 00:00:00 2001 From: Felix Zumstein Date: Tue, 29 Oct 2024 17:23:14 -0300 Subject: [PATCH] migrate custom function tests from xlwings repo --- app/config.py | 1 + app/custom_functions/examples.py | 10 +- tests/e2e_custom_functions.py | 946 +++++++++++++++++++++++++++++++ tests/e2e_custom_functions.xlsx | Bin 0 -> 38908 bytes 4 files changed, 956 insertions(+), 1 deletion(-) create mode 100644 tests/e2e_custom_functions.py create mode 100644 tests/e2e_custom_functions.xlsx diff --git a/app/config.py b/app/config.py index 237f214..6a6f08a 100644 --- a/app/config.py +++ b/app/config.py @@ -32,6 +32,7 @@ class Settings(BaseSettings): enable_excel_online: bool = True enable_htmx: bool = True enable_socketio: bool = True + enable_tests: bool = False environment: Literal["dev", "qa", "uat", "staging", "prod"] = "prod" functions_namespace: str = "XLWINGS" hostname: Optional[str] = None diff --git a/app/custom_functions/examples.py b/app/custom_functions/examples.py index 6c2e6f7..e79fcf6 100644 --- a/app/custom_functions/examples.py +++ b/app/custom_functions/examples.py @@ -4,6 +4,8 @@ """ import asyncio +import sys +from pathlib import Path from typing import Annotated import numpy as np @@ -12,7 +14,7 @@ from xlwings.ext.sql import _sql from xlwings.server import arg, func, ret -from .. import custom_scripts, utils +from .. import custom_scripts, settings, utils from ..models import CurrentUser @@ -159,3 +161,9 @@ async def hello_with_script(name): """This function triggers a custom script, requires XLWINGS_ENABLE_SOCKETIO=true""" await utils.trigger_script(custom_scripts.hello_world, exclude="MySheet") return f"Hello {name}!" + + +# Unit tests +if settings.enable_tests: + sys.path.append(str(Path(__file__).parent.parent.resolve())) + from tests.e2e_custom_functions import * diff --git a/tests/e2e_custom_functions.py b/tests/e2e_custom_functions.py new file mode 100644 index 0000000..cb402db --- /dev/null +++ b/tests/e2e_custom_functions.py @@ -0,0 +1,946 @@ +""" +Key differences with COM UDFs: +* respects ints (COM always returns floats) +* returns 0 for empty cells. To get None like in COM, you need to set the formula to: ="" +* caller range object not supported (caller address would easy to get though) +* reading datetime must be explicitly converted via dt.date / dt.datetime or parse_dates (pandas) +* writing datetime is now automatically formatting it as date in Excel +* categories aren't supported: replaced by namespaces +""" + +import datetime as dt +from datetime import date, datetime +from typing import Annotated + +import xlwings as xw +from xlwings.server import arg, func, ret + +try: + import numpy as np + from numpy.testing import assert_array_equal + + def nparray_equal(a, b): + try: + assert_array_equal(a, b) + except AssertionError: + return False + return True + +except ImportError: + np = None +try: + import pandas as pd + from pandas.testing import assert_frame_equal, assert_series_equal + + def frame_equal(a, b): + try: + assert_frame_equal(a, b) + except AssertionError: + return False + return True + + def series_equal(a, b): + try: + assert_series_equal(a, b) + except AssertionError: + return False + return True + +except ImportError: + pd = None + + +# Defaults +@func +def read_float(x): + return x == 2 + + +@func +def write_float(): + return 2 + + +@func +def read_string(x): + return x == "xlwings" + + +@func +def write_string(): + return "xlwings" + + +@func +def read_empty(x): + return x is None + + +@func +@arg("x", dt.datetime) +def read_date(x): + print(x) + return x == datetime(2015, 1, 15) + + +@func +def write_date(): + return datetime(1969, 12, 31) + + +@func +@arg("x", dt.datetime) +def read_datetime(x): + return x == datetime(1976, 2, 15, 13, 6, 22) + + +@func +def write_datetime(): + return datetime(1976, 2, 15, 13, 6, 23) + + +@func +def read_horizontal_list(x): + return x == [1, 2] + + +@func +def write_horizontal_list(): + return [1, 2] + + +@func +def read_vertical_list(x): + return x == [1, 2] + + +@func +def write_vertical_list(): + return [[1], [2]] + + +@func +def read_2dlist(x): + return x == [[1, 2], [3, 4]] + + +@func +def write_2dlist(): + return [[1, 2], [3, 4]] + + +# Keyword args on default converters + + +@func +@arg("x", ndim=1) +def read_ndim1(x): + return x == [2] + + +@func +@arg("x", ndim=2) +def read_ndim2(x): + return x == [[2]] + + +@func +@arg("x", transpose=True) +def read_transpose(x): + return x == [[1, 3], [2, 4]] + + +@func +@ret(transpose=True) +def write_transpose(): + return [[1, 2], [3, 4]] + + +@func +def read_dates_as1(x): + x[0][1] = xw.to_datetime(x[0][1]).date() + x[1][0] = xw.to_datetime(x[1][0]).date() + return x == [[1, date(2015, 1, 13)], [date(2000, 12, 1), 4]] + + +@func +@arg("x", dt.date) +def read_dates_as2(x): + return x == date(2005, 1, 15) + + +@func +def read_dates_as3(x): + x[0][1] = xw.to_datetime(x[0][1]) + x[1][0] = xw.to_datetime(x[1][0]) + return x == [[1, datetime(2015, 1, 13)], [datetime(2000, 12, 1), 4]] + + +@func +@arg("x", empty="empty") +def read_empty_as(x): + return x == [[1, "empty"], ["empty", 4]] + + +# Dicts +@func +@arg("x", dict) +def read_dict(x): + return x == {"a": 1, "b": "c"} + + +@func +@arg("x", dict, transpose=True) +def read_dict_transpose(x): + return x == {1: "c", "a": "b"} + + +@func +def write_dict(): + return {"a": 1, "b": "c"} + + +# Numpy Array +if np: + + @func + @arg("x", np.array) + def read_scalar_nparray(x): + return nparray_equal(x, np.array(1)) + + @func + @arg("x", np.array) + def read_empty_nparray(x): + return nparray_equal(x, np.array(np.nan)) + + @func + @arg("x", np.array) + def read_horizontal_nparray(x): + return nparray_equal(x, np.array([1, 2])) + + @func + @arg("x", np.array) + def read_vertical_nparray(x): + return nparray_equal(x, np.array([1, 2])) + + @func + @arg("x", dt.datetime) + def read_date_nparray(x): + x = np.array(x) + return nparray_equal(x, np.array(datetime(2000, 12, 20))) + + # Keyword args on Numpy arrays + + @func + @arg("x", np.array, ndim=1) + def read_ndim1_nparray(x): + return nparray_equal(x, np.array([2])) + + @func + @arg("x", np.array, ndim=2) + def read_ndim2_nparray(x): + return nparray_equal(x, np.array([[2]])) + + @func + @arg("x", np.array, transpose=True) + def read_transpose_nparray(x): + return nparray_equal(x, np.array([[1, 3], [2, 4]])) + + @func + @ret(transpose=True) + def write_transpose_nparray(): + return np.array([[1, 2], [3, 4]]) + + @func + @arg("x", dt.date) + def read_dates_as_nparray(x): + x = np.array(x) + return nparray_equal(x, np.array(date(2000, 12, 20))) + + @func + @arg("x", np.array, empty="empty") + def read_empty_as_nparray(x): + return nparray_equal(x, np.array("empty")) + + @func + def write_np_scalar(): + return np.float64(2) + + +# Pandas Series + +if pd: + + @func + @arg("x", pd.Series, header=False, index=False) + def read_series_noheader_noindex(x): + return series_equal(x, pd.Series([1, 2])) + + @func + @arg("x", pd.Series, header=False, index=True) + def read_series_noheader_index(x): + return series_equal(x, pd.Series([1, 2], index=[10, 20])) + + @func + @arg("x", pd.Series, header=True, index=False) + def read_series_header_noindex(x): + return series_equal(x, pd.Series([1, 2], name="name")) + + @func + @arg("x", pd.Series, header=True, index=True) + def read_series_header_named_index(x): + return series_equal( + x, + pd.Series([1, 2], name="name", index=pd.Index([10, 20], name="ix")), + ) + + @func + @arg("x", pd.Series, header=True, index=True) + def read_series_header_nameless_index(x): + print(x) + return series_equal(x, pd.Series([1, 2], name="name", index=[10, 20])) + + @func + @arg("x", pd.Series, header=True, index=2) + def read_series_header_nameless_2index(x): + ix = pd.MultiIndex.from_arrays([["a", "a"], [10, 20]]) + return series_equal(x, pd.Series([1, 2], name="name", index=ix)) + + @func + @arg("x", pd.Series, header=True, index=2) + def read_series_header_named_2index(x): + ix = pd.MultiIndex.from_arrays([["a", "a"], [10, 20]], names=["ix1", "ix2"]) + return series_equal(x, pd.Series([1, 2], name="name", index=ix)) + + @func + @arg("x", pd.Series, header=False, index=2) + def read_series_noheader_2index(x): + ix = pd.MultiIndex.from_arrays([["a", "a"], [10, 20]]) + return series_equal(x, pd.Series([1, 2], index=ix)) + + @func + @ret(pd.Series, index=False) + def write_series_noheader_noindex(): + return pd.Series([1, 2]) + + @func + @ret(pd.Series, index=True) + def write_series_noheader_index(): + return pd.Series([1, 2], index=[10, 20]) + + @func + @ret(pd.Series, index=False) + def write_series_header_noindex(): + return pd.Series([1, 2], name="name") + + @func + def write_series_header_named_index(): + return pd.Series([1, 2], name="name", index=pd.Index([10, 20], name="ix")) + + @func + @ret(pd.Series, index=True, header=True) + def write_series_header_nameless_index(): + return pd.Series([1, 2], name="name", index=[10, 20]) + + @func + @ret(pd.Series, header=True, index=2) + def write_series_header_nameless_2index(): + ix = pd.MultiIndex.from_arrays([["a", "a"], [10, 20]]) + return pd.Series([1, 2], name="name", index=ix) + + @func + @ret(pd.Series, header=True, index=2) + def write_series_header_named_2index(): + ix = pd.MultiIndex.from_arrays([["a", "a"], [10, 20]], names=["ix1", "ix2"]) + return pd.Series([1, 2], name="name", index=ix) + + @func + @ret(pd.Series, header=False, index=2) + def write_series_noheader_2index(): + ix = pd.MultiIndex.from_arrays([["a", "a"], [10, 20]]) + return pd.Series([1, 2], index=ix) + + @func + @arg("x", pd.Series, parse_dates=True) + def read_timeseries(x): + return series_equal( + x, + pd.Series( + [1.5, 2.5], + name="ts", + index=[datetime(2000, 12, 20), datetime(2000, 12, 21)], + ), + ) + + @func + @ret(pd.Series) + def write_timeseries(): + return pd.Series( + [1.5, 2.5], + name="ts", + index=[datetime(2000, 12, 20), datetime(2000, 12, 21)], + ) + + @func + @ret(pd.Series, index=False) + def write_series_nan(): + return pd.Series([1, np.nan, 3]) + + +# Pandas DataFrame + +if pd: + + @func + @arg("x", pd.DataFrame, index=False, header=False) + def read_df_0header_0index(x): + return frame_equal(x, pd.DataFrame([[1, 2], [3, 4]])) + + @func + @ret(pd.DataFrame, index=False, header=False) + def write_df_0header_0index(): + return pd.DataFrame([[1, 2], [3, 4]]) + + @func + @arg("x", pd.DataFrame, index=False, header=True) + def read_df_1header_0index(x): + return frame_equal(x, pd.DataFrame([[1, 2], [3, 4]], columns=["a", "b"])) + + @func + @ret(pd.DataFrame, index=False, header=True) + def write_df_1header_0index(): + return pd.DataFrame([[1, 2], [3, 4]], columns=["a", "b"]) + + @func + @arg("x", pd.DataFrame, index=True, header=False) + def read_df_0header_1index(x): + return frame_equal(x, pd.DataFrame([[1, 2], [3, 4]], index=[10, 20])) + + @func + @ret(pd.DataFrame, index=True, header=False) + def write_df_0header_1index(): + return pd.DataFrame([[1, 2], [3, 4]], index=[10, 20]) + + @func + @arg("x", pd.DataFrame, index=2, header=False) + def read_df_0header_2index(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays([["a", "a", "b"], [1, 2, 1]]), + ) + return frame_equal(x, df) + + @func + @ret(pd.DataFrame, index=2, header=False) + def write_df_0header_2index(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays([["a", "a", "b"], [1, 2, 1]]), + ) + return df + + @func + @arg("x", pd.DataFrame, index=1, header=1) + def read_df_1header_1namedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=["c", "d", "c"], + ) + df.index.name = "ix1" + return frame_equal(x, df) + + @func + def write_df_1header_1namedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=["c", "d", "c"], + ) + df.index.name = "ix1" + return df + + @func + @arg("x", pd.DataFrame, index=1, header=1) + def read_df_1header_1unnamedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=["c", "d", "c"], + ) + return frame_equal(x, df) + + @func + def write_df_1header_1unnamedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=["c", "d", "c"], + ) + return df + + @func + @arg("x", pd.DataFrame, index=False, header=2) + def read_df_2header_0index(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return frame_equal(x, df) + + @func + @ret(pd.DataFrame, index=False, header=2) + def write_df_2header_0index(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return df + + @func + @arg("x", pd.DataFrame, index=1, header=2) + def read_df_2header_1namedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + df.index.name = "ix1" + return frame_equal(x, df) + + @func + def write_df_2header_1namedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + df.index.name = "ix1" + return df + + @func + @arg("x", pd.DataFrame, index=1, header=2) + def read_df_2header_1unnamedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return frame_equal(x, df) + + @func + def write_df_2header_1unnamedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[1, 2], + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return df + + @func + @arg("x", pd.DataFrame, index=2, header=2) + def read_df_2header_2namedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays( + [["a", "a", "b"], [1, 2, 1]], names=["x1", "x2"] + ), + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return frame_equal(x, df) + + @func + def write_df_2header_2namedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays( + [["a", "a", "b"], [1, 2, 1]], names=["x1", "x2"] + ), + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return df + + @func + @arg("x", pd.DataFrame, index=2, header=2) + def read_df_2header_2unnamedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays([["a", "a", "b"], [1, 2, 1]]), + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return frame_equal(x, df) + + @func + def write_df_2header_2unnamedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays([["a", "a", "b"], [1, 2, 1]]), + columns=pd.MultiIndex.from_arrays([["a", "a", "b"], ["c", "d", "c"]]), + ) + return df + + @func + @arg("x", pd.DataFrame, index=2, header=1) + def read_df_1header_2namedindex(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays( + [["a", "a", "b"], [1, 2, 1]], names=["x1", "x2"] + ), + columns=["a", "d", "c"], + ) + return frame_equal(x, df) + + @func + def write_df_1header_2namedindex(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=pd.MultiIndex.from_arrays( + [["a", "a", "b"], [1, 2, 1]], names=["x1", "x2"] + ), + columns=["a", "d", "c"], + ) + return df + + @func + @arg("x", pd.DataFrame, parse_dates=True) + def read_df_date_index(x): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[datetime(1999, 12, 13), datetime(1999, 12, 14)], + columns=["c", "d", "c"], + ) + return frame_equal(x, df) + + @func + def write_df_date_index(): + df = pd.DataFrame( + [[1, 2, 3], [4, 5, 6]], + index=[datetime(1999, 12, 13), datetime(1999, 12, 14)], + columns=["c", "d", "c"], + ) + return df + + @func + def read_workbook_caller(): + wb = xw.Book.caller() + return wb.sheets.active["E277"].value == 1 + + +@func +def default_args(x, y="hello", z=20): + return 2 * x + 3 * len(y) + 7 * z + + +@func +def variable_args(x, *z): + return 2 * x + 3 * len(z) + 7 * z[0] + + +@func +def optional_args(x, y=None): + if y is None: + y = 10 + return x * y + + +@func +def write_none(): + return None + + +@func +def method_signature_with_less_than_1024_characters( + very_long_parameter_name_1=None, + very_long_parameter_name_2=None, + very_long_parameter_name_3=None, + very_long_parameter_name_4=None, + very_long_parameter_name_5=None, + very_long_parameter_name_6=None, + very_long_parameter_name_7=None, + very_long_parameter_name_8=None, + very_long_parameter_name_9=None, + very_long_parameter_name_10=None, + very_long_parameter_name_11=None, + very_long_parameter_name_12=None, + very_long_parameter_name_13=None, + very_long_parameter_name_14=None, + very_long_parameter_name_15=None, + very_long_parameter_name_16=None, + very_long_parameter_name_17=None, + very_long_parameter_name_18=None, + very_long_parameter_name_19=None, + very_long_parameter_name_20=None, + very_long_parameter_name_21=None, + very_long_parameter_name_22=None, + very_long_parameter_name_23=None, + very_long_parameter_name_24=None, + very_long_parameter_name_25=None, + paramet_name_26=None, +): + return "non splitted signature" + + +@func +def method_signature_with_more_than_1024_characters( + very_long_parameter_name_1=None, + very_long_parameter_name_2=None, + very_long_parameter_name_3=None, + very_long_parameter_name_4=None, + very_long_parameter_name_5=None, + very_long_parameter_name_6=None, + very_long_parameter_name_7=None, + very_long_parameter_name_8=None, + very_long_parameter_name_9=None, + very_long_parameter_name_10=None, + very_long_parameter_name_11=None, + very_long_parameter_name_12=None, + very_long_parameter_name_13=None, + very_long_parameter_name_14=None, + very_long_parameter_name_15=None, + very_long_parameter_name_16=None, + very_long_parameter_name_17=None, + very_long_parameter_name_18=None, + very_long_parameter_name_19=None, + very_long_parameter_name_20=None, + very_long_parameter_name_21=None, + very_long_parameter_name_22=None, + very_long_parameter_name_23=None, + very_long_parameter_name_24=None, + very_long_parameter_name_25=None, + very_long_parameter_name_26=None, +): + return "splitted signature" + + +@func +def return_pd_nat(): + return pd.DataFrame(data=[pd.NaT], columns=[1], index=[1]) + + +@func +@arg("df", pd.DataFrame, parse_dates=[0, 2]) +def parse_dates_index(df): + expected = pd.DataFrame( + [ + [1, dt.datetime(2021, 1, 1, 11, 11, 11), 4], + [2, dt.datetime(2021, 1, 2, 22, 22, 22), 5], + [3, dt.datetime(2021, 1, 3), 6], + ], + columns=["one", "two", "three"], + index=[ + dt.datetime(2021, 1, 1, 11, 11, 11), + dt.datetime(2021, 1, 2, 22, 22, 22), + dt.datetime(2021, 1, 3), + ], + ) + assert_frame_equal(df, expected) + return True + + +@func +@arg("df", pd.DataFrame, parse_dates=["ix", "two"]) +def parse_dates_names(df): + expected = pd.DataFrame( + [ + [1, dt.datetime(2021, 1, 1, 11, 11, 11), 4], + [2, dt.datetime(2021, 1, 2, 22, 22, 22), 5], + [3, dt.datetime(2021, 1, 3), 6], + ], + columns=["one", "two", "three"], + index=[ + dt.datetime(2021, 1, 1, 11, 11, 11), + dt.datetime(2021, 1, 2, 22, 22, 22), + dt.datetime(2021, 1, 3), + ], + ) + expected.index.name = "ix" + assert_frame_equal(df, expected) + return True + + +@func +@arg("df", pd.DataFrame, parse_dates=True) +def parse_dates_true(df): + expected = pd.DataFrame( + [[1], [2], [3]], + columns=["one"], + index=[ + dt.datetime(2021, 1, 1, 11, 11, 11), + dt.datetime(2021, 1, 2, 22, 22, 22), + dt.datetime(2021, 1, 3), + ], + ) + assert_frame_equal(df, expected) + return True + + +@func +@ret(transpose=True) +def write_error_cells(): + return ["#DIV/0!", "#N/A", "#NAME?", "#NULL!", "#NUM!", "#REF!", "#VALUE!"] + + +@func +def read_error_cells(errors): + assert [None] * 7 == errors + return True + + +@func +@arg("errors", err_to_str=True) +def read_error_cells_str(errors): + assert [ + "#DIV/0!", + "#N/A", + "#NAME?", + "#NULL!", + "#NUM!", + "#REF!", + "#VALUE!", + ] == errors + return True + + +@func +@ret(date_format="yyyy-m-d") +def explicit_date_format(): + return dt.datetime(2022, 1, 13) + + +@func(namespace="subname") +def namespace(): + return True + + +@func(volatile=True) +def volatile(): + return True + + +@func +@arg("x", pd.DataFrame, index=False) +@arg("*params", pd.DataFrame, index=False) +def varargs_arg_decorator(x, *params): + return pd.concat(params + (x,)) + + +# Type hints notation +@func +def type_hints_arg_int(x: int) -> bool: + return isinstance(x, int) and x == 2 + + +@func +def type_hints_arg_float(x: float): + return isinstance(x, float) and x == 2.2 + + +@func +def type_hints_arg_str(x: str): + return x == "xlwings" + + +@func +def type_hints_arg_bool(x: bool): + return x is True + + +@func +def type_hints_arg_datetime(x: dt.datetime): + return x == dt.datetime(2020, 12, 20) + + +@func +def type_hints_arg_list(x: list): + return x == [1, 2] + + +@func +def type_hints_arg_list_int(x: list[int]): + return x == [1, 2] + + +@func +def type_hints_arg_list_list_int(x: list[list[int]]): + return x == [[1, 2], [3, 4]] + + +@func +def type_hints_arg_dict(x: dict): + return x == {"a": 1} + + +@func +def type_hints_arg_array(x: np.array): + try: + assert_array_equal(x, np.array([[1, 2], [3, 4]])) + except AssertionError: + return False + return True + + +@func +def type_hints_arg_ndarray(x: np.ndarray): + try: + assert_array_equal(x, np.array([[1, 2], [3, 4]])) + except AssertionError: + return False + return True + + +@func +def type_hints_arg_df(x: pd.DataFrame): + return frame_equal( + x, + pd.DataFrame([[1, 2], [3, 4]], columns=["one", "two"], index=[0, 1]), + ) + + +@func +def type_hints_arg_df_annotated(x: Annotated[pd.DataFrame, {"index": False}]): + return frame_equal( + x, + pd.DataFrame( + [[0, 1, 2], [1, 3, 4]], + columns=[None, "one", "two"], + index=[0, 1], + ), + ) + + +@func +def type_hints_ret_df_annotated() -> Annotated[pd.DataFrame, {"index": False}]: + return pd.DataFrame([[1, 2], [3, 4]], columns=["one", "two"]) + + +@func +@ret(index=False) +def type_hints_ret_df_decorator_override() -> Annotated[pd.DataFrame, {"index": True}]: + return pd.DataFrame([[1, 2], [3, 4]], columns=["one", "two"]) + + +@func +@arg("x", index=False) +def type_hints_arg_df_decorator_coexistence(x: pd.DataFrame): + print(x) + return frame_equal( + x, + pd.DataFrame( + [[0, 1, 2], [1, 3, 4]], + columns=[None, "one", "two"], + index=[0, 1], + ), + ) + + +@func +def varargs_with_object_handles(x, *args: object): + df = pd.DataFrame( + {"A": [1, 2, 3, 4, 5], "B": [10, 8, 6, 4, 2], "C": [10, 9, 8, 7, 6]} + ) + r1 = x == 10 + r2 = len(args) == 2 + r3 = frame_equal(args[0], df) + r4 = frame_equal(args[1], df) + return r1 and r2 and r3 and r4 diff --git a/tests/e2e_custom_functions.xlsx b/tests/e2e_custom_functions.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..31bfa3535f08550c3a150248fd78ed8546bec45d GIT binary patch literal 38908 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

h|ei;g0$gtP$ zC&Ebs_XIeL32-M|4pxe^K%R*{(7o_TB>0BGs)N}d)8YBdj?bBgfWCtd=#cEnLY*s;ruHEc=eMdp3?$T zt`e$uax-nG^)ewW>Xw4-$5{p8$WrBq}ykLjJH5XTl c!~L=sg}+{E1(Gq^s&m$YPaF8QK38@02lbS@dH?_b literal 0 HcmV?d00001