33import functools
44import inspect
55from collections .abc import Collection , Sequence
6- from typing import Any , Callable , Literal , Optional
6+ from typing import Any , Callable , Literal , Optional , get_type_hints
77
88import narwhals .stable .v1 as nw
9+ from narwhals .stable .v1 .dependencies import (
10+ get_cudf ,
11+ get_modin ,
12+ get_pandas ,
13+ get_polars ,
14+ get_pyarrow ,
15+ )
916
1017from .selectors import Selector
1118
1219col = nw .col
1320lit = nw .lit
1421
1522
23+ def _is_polars_series (ser : Any ) -> bool :
24+ return (pl := get_polars ()) is not None and issubclass (ser , pl .Series )
25+
26+
27+ def _is_polars_expr (expr : Any ) -> bool :
28+ return (pl := get_polars ()) is not None and issubclass (expr , pl .Expr )
29+
30+
31+ def _is_polars_dataframe (df : Any ) -> bool :
32+ return (pl := get_polars ()) is not None and issubclass (df , pl .DataFrame )
33+
34+
35+ def _is_pandas_series (ser : Any ) -> bool :
36+ return (pd := get_pandas ()) is not None and issubclass (ser , pd .Series )
37+
38+
39+ def _is_pandas_dataframe (df : Any ) -> bool :
40+ return (pd := get_pandas ()) is not None and issubclass (df , pd .DataFrame )
41+
42+
43+ def _is_modin_dataframe (df : Any ) -> bool :
44+ return (mpd := get_modin ()) is not None and issubclass (df , mpd .DataFrame )
45+
46+
47+ def _is_modin_series (ser : Any ) -> bool :
48+ return (mpd := get_modin ()) is not None and issubclass (ser , mpd .Series )
49+
50+
51+ def _is_cudf_dataframe (df : Any ) -> bool :
52+ return (cudf := get_cudf ()) is not None and issubclass (df , cudf .DataFrame )
53+
54+
55+ def _is_cudf_series (ser : Any ) -> bool :
56+ return (cudf := get_cudf ()) is not None and issubclass (ser , cudf .Series )
57+
58+
59+ def _is_pyarrow_chunked_array (ser : Any ) -> bool :
60+ return (pa := get_pyarrow ()) is not None and issubclass (ser , pa .ChunkedArray )
61+
62+
63+ def _is_pyarrow_table (df : Any ) -> bool :
64+ return (pa := get_pyarrow ()) is not None and issubclass (df , pa .Table )
65+
66+
67+ def _is_series (x : Any ) -> bool :
68+ return (
69+ issubclass (x , nw .Series )
70+ or _is_pandas_series (x )
71+ or _is_modin_series (x )
72+ or _is_cudf_series (x )
73+ or _is_polars_series (x )
74+ or _is_pyarrow_chunked_array (x )
75+ )
76+
77+
78+ def _is_expr (x : Any ) -> bool :
79+ return issubclass (x , nw .Expr ) or _is_polars_expr (x )
80+
81+
82+ def _is_dataframe (x : Any ) -> bool :
83+ return (
84+ isinstance (x , nw .DataFrame )
85+ or _is_polars_dataframe (x )
86+ or _is_pandas_dataframe (x )
87+ or _is_modin_dataframe (x )
88+ or _is_cudf_dataframe (x )
89+ or _is_pyarrow_table (x )
90+ )
91+
92+
1693class staticproperty :
1794 """
1895 A decorator that allows defining a read-only, class-level attribute
@@ -38,21 +115,47 @@ def __delete__(self, obj):
38115 raise AttributeError (f"can't delete attribute '{ self .__name__ } '" )
39116
40117
41- def _resolve_return_type_from_annotation (func : Callable ):
118+ def _infer_input_type (
119+ type_hints : dict [str , Any ], signature : inspect .Signature
120+ ) -> CheckInputType :
121+ params = signature .parameters
122+ if len (params ) == 0 :
123+ return None
124+
125+ first_param_name = list (params .keys ())[0 ]
42126 try :
43- dtype = str ( func . __annotations__ [ "return" ])
127+ type_hint = type_hints [ first_param_name ]
44128 except KeyError :
45129 return "auto"
46130
47- if dtype == "bool" :
48- return "bool"
131+ if issubclass (type_hint , str ):
132+ return "str"
133+ elif _is_dataframe (type_hint ):
134+ return "Frame"
135+ elif _is_series (type_hint ):
136+ return "Series"
49137
50- if len (inspect .signature (func ).parameters ) == 0 :
51- return "Expr"
138+ return "auto"
52139
53- if "Series" in dtype :
54- return "Series"
55- elif "Expr" in dtype :
140+
141+ def _infer_return_type (
142+ type_hints : dict [str , Any ], input_type : CheckInputType
143+ ) -> CheckReturnType :
144+ try :
145+ # Try to get it from the type hints first
146+ type_hint = type_hints ["return" ]
147+
148+ if issubclass (type_hint , bool ):
149+ return "bool"
150+ elif _is_expr (type_hint ):
151+ return "Expr"
152+ elif _is_series (type_hint ):
153+ return "Series"
154+ except KeyError :
155+ # If type hints don't exist, we try to infer from the input_type
156+ pass
157+
158+ if input_type == "str" or input_type is None :
56159 return "Expr"
57160
58161 return "auto"
@@ -272,7 +375,7 @@ def contains(pattern: str, literal: bool = False) -> Check:
272375 )
273376
274377
275- CheckInputType = Optional [Literal ["auto" , "Frame" , "Expr " , "Series" ]]
378+ CheckInputType = Optional [Literal ["auto" , "Frame" , "str " , "Series" ]]
276379CheckReturnType = Literal ["auto" , "bool" , "Expr" , "Series" ]
277380
278381
@@ -324,22 +427,21 @@ def __init__(
324427
325428 def _set_params (self ) -> None :
326429 assert self .func is not None
327- self ._func_n_params = len (inspect .signature (self .func ).parameters )
328-
329- if self .input_type == "auto" :
330- if self ._func_n_params == 0 :
331- self .input_type = None
332-
333- if self .return_type == "auto" and self .func is not None :
334- if self .input_type is None :
335- self .return_type = "Expr"
336- else :
337- self .return_type = _resolve_return_type_from_annotation (
338- self .func ,
339- )
340-
341- if self .return_type == "Expr" :
342- self .input_type = None
430+ auto_input_type = self .input_type == "auto"
431+ auto_return_type = self .return_type == "auto"
432+
433+ if auto_input_type or auto_return_type :
434+ signature = inspect .signature (self .func )
435+ type_hints = get_type_hints (self .func )
436+
437+ if auto_input_type :
438+ self .input_type = _infer_input_type (type_hints , signature )
439+
440+ if auto_return_type :
441+ self .return_type = _infer_return_type (
442+ type_hints ,
443+ self .input_type ,
444+ )
343445
344446 if self .name is None :
345447 self .name = None if self .func .__name__ == "<lambda>" else self .func .__name__
0 commit comments