Repository: modin-project/modin

Issue:
Properties that default to pandas have unclear error messages
When a DataFrame/Series property defaults to pandas by registering an operator in `modin.core.dataframe.algebra.default2pandas`, the raised warning message does not properly render the name of the property. For example, commenting out the `PandasQueryCompiler` assignment of `dt_date` and instead using the `BaseQueryCompiler` implementation (which uses `DateTimeDefault`) yields the following:
```python
>>> import modin.pandas as pd
>>> pd.Series([pd.Timestamp("2020-01-01 00:01:01")]).dt.date
UserWarning: Distributing <class 'list'> object. This may take some time.
UserWarning: <function Series.<property object at 0x11fcdf790>> is not currently supported by PandasOnRay, defaulting to pandas implementation.
Please refer to https://modin.readthedocs.io/en/stable/supported_apis/defaulting_to_pandas.html for explanation.
0    2020-01-01
dtype: object
```
This occurs because the method is registered with `DateTimeDefault.register(pandas.Series.dt.date)`, and since `dt.date` is a property, it has no `__name__` field. To resolve this, the below line of `DefaultMethod.register` should be changed to access `func.fget.__name__` if `func` is a Python `property` object.

https://github.com/modin-project/modin/blob/9fa326f6fd53eb0e3192b3eca72382a6388117fe/modin/core/dataframe/algebra/default2pandas/default.py#L97

As far as I can tell, this does not affect any of the first-class Modin backends. Properties like `DataFrame.attrs` use `BasePandasDataset._default_to_pandas`, which explicitly requires a name to be passed in. `dt`/`str` accessor properties use `StrDefault` and `DatetimeDefault` in `BaseQueryCompiler`, but `PandasQueryCompiler` uses the `Map` operator to avoid this default. However, downstream libraries that implement a custom subclass of `BaseQueryCompiler` are affected.


Discussion:

Resolving patch:
diff --git a/modin/core/dataframe/algebra/default2pandas/default.py b/modin/core/dataframe/algebra/default2pandas/default.py
--- a/modin/core/dataframe/algebra/default2pandas/default.py
+++ b/modin/core/dataframe/algebra/default2pandas/default.py
@@ -94,8 +94,6 @@ def register(cls, func, obj_type=None, inplace=None, fn_name=None):
             Function that takes query compiler, does fallback to pandas and applies `func`
             to the casted to pandas frame or its property accesed by ``cls.frame_wrapper``.
         """
-        fn_name = getattr(func, "__name__", str(func)) if fn_name is None else fn_name
-
         if isinstance(func, str):
             if obj_type is None:
                 obj_type = cls.DEFAULT_OBJECT_TYPE
@@ -104,7 +102,16 @@ def register(cls, func, obj_type=None, inplace=None, fn_name=None):
             fn = func
 
         if type(fn) is property:
+            if fn_name is None and hasattr(fn, "fget"):
+                # When `fn` is a property, `str(fn)` will be something like
+                # "<property object at 0x7f8671e09d10>". We instead check its `fget` method to get
+                # the name of the property.
+                # Note that this method is still imperfect because we cannot get the class name
+                # of the property. For example, we can only get "hour" from `Series.dt.hour`.
+                fn_name = f"<property fget:{getattr(fn.fget, '__name__', 'noname')}>"
             fn = cls.build_property_wrapper(fn)
+        else:
+            fn_name = getattr(fn, "__name__", str(fn)) if fn_name is None else fn_name
 
         def applyier(df, *args, **kwargs):
             """


Test patch:
diff --git a/modin/tests/core/storage_formats/pandas/test_internals.py b/modin/tests/core/storage_formats/pandas/test_internals.py
--- a/modin/tests/core/storage_formats/pandas/test_internals.py
+++ b/modin/tests/core/storage_formats/pandas/test_internals.py
@@ -30,6 +30,7 @@
     context,
 )
 from modin.core.dataframe.algebra import Fold
+from modin.core.dataframe.algebra.default2pandas import DataFrameDefault
 from modin.core.dataframe.pandas.dataframe.dataframe import PandasDataframe
 from modin.core.dataframe.pandas.dataframe.utils import ColumnInfo, ShuffleSortFunctions
 from modin.core.dataframe.pandas.metadata import (
@@ -2757,3 +2758,30 @@ def filter_modin_dataframe2(df):
     filtered_df = modin_df.filter_dataframe2()
 
     df_equals(filtered_df, expected_df)
+
+
+def test_default_property_warning_name():
+    # Test that when a property defaults to pandas, the raised warning mentions the full name of
+    # the pandas property rather than a hex address
+
+    @property
+    def _test_default_property(df):
+        return "suspicious sentinel value"
+
+    @property
+    def qc_test_default_property(qc):
+        return DataFrameDefault.register(_test_default_property)(qc)
+
+    PandasQueryCompiler.qc_test_default_property = qc_test_default_property
+
+    @property
+    def dataframe_test_default_property(df):
+        return df._query_compiler.qc_test_default_property
+
+    pd.DataFrame.dataframe_test_default_property = dataframe_test_default_property
+
+    with pytest.warns(
+        UserWarning,
+        match="<function DataFrame.<property fget:_test_default_property>> is not currently supported",
+    ):
+        pd.DataFrame([[1]]).dataframe_test_default_property