From 31804def3939ac98730f06af8d9eef04669f464d Mon Sep 17 00:00:00 2001 From: galipremsagar Date: Thu, 25 Jun 2026 20:56:14 +0000 Subject: [PATCH 1/3] split up --- python/cudf/cudf/pandas/_wrappers/pandas.py | 7 +- python/cudf/cudf/pandas/fast_slow_proxy.py | 108 ++++++++++++++++++ .../pandas/scripts/pandas-testing-plugin.py | 14 --- .../cudf_pandas_tests/test_fast_slow_proxy.py | 105 +++++++++++++++++ 4 files changed, 218 insertions(+), 16 deletions(-) diff --git a/python/cudf/cudf/pandas/_wrappers/pandas.py b/python/cudf/cudf/pandas/_wrappers/pandas.py index 341c65658e33..e6c8631f3fe4 100644 --- a/python/cudf/cudf/pandas/_wrappers/pandas.py +++ b/python/cudf/cudf/pandas/_wrappers/pandas.py @@ -47,6 +47,7 @@ _FastSlowAttribute, _FunctionProxy, _maybe_wrap_result, + _setattr_fsproxy_no_mirror, _State, _Unusable, is_proxy_object, @@ -1661,8 +1662,10 @@ def _df_query_method(self, *args, local_dict=None, global_dict=None, **kwargs): ) -DataFrame.eval = _df_eval_method -DataFrame.query = _df_query_method +# These custom implementations are installed by cudf.pandas itself and must +# not be mirrored onto (and clobber) the real ``pandas.DataFrame``. +_setattr_fsproxy_no_mirror(DataFrame, "eval", _df_eval_method) +_setattr_fsproxy_no_mirror(DataFrame, "query", _df_query_method) _JsonReader = make_intermediate_proxy_type( "_JsonReader", diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index e228f5fd0589..f9704137c51f 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -362,6 +362,11 @@ def _fsproxy_state(self) -> _State: final_type_map[fast_type] = cls final_type_map[slow_type] = cls + # Proxy type fully constructed: from here on, class-level attribute writes + # are genuine runtime monkeypatches and should be mirrored onto the + # underlying "slow" (real) type. + type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", True) + return cls @@ -511,6 +516,11 @@ def _fsproxy_fast_to_slow(self): intermediate_type_map[fast_type] = cls intermediate_type_map[slow_type] = cls + # Proxy type fully constructed: from here on, class-level attribute writes + # are genuine runtime monkeypatches and should be mirrored onto the + # underlying "slow" (real) type. + type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", True) + return cls @@ -560,6 +570,22 @@ def get_registered_functions(): return dict() +_NO_SLOW_ATTR = object() + + +def _setattr_fsproxy_no_mirror(cls: type, name: str, value: Any) -> None: + """Set ``cls.name = value`` on a proxy type *without* mirroring to slow. + + ``_FastSlowProxyMeta.__setattr__`` mirrors class-level attribute writes + onto the underlying "slow" (real) type so that runtime monkeypatches stay + visible to the pandas fallback path. cudf.pandas itself installs a handful + of custom methods (e.g. ``DataFrame.query``/``DataFrame.eval``) onto the + proxy classes that must *not* clobber pandas' genuine implementations; use + this helper for those (rare) assignments. + """ + type.__setattr__(cls, name, value) + + class _FastSlowProxyMeta(type): """ Metaclass used to dynamically find class attributes and @@ -578,6 +604,88 @@ def _fsproxy_slow(self) -> type: def _fsproxy_fast(self) -> type: return self._fsproxy_fast_type + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + # Per-proxy-type switch controlling whether class-level attribute + # writes are mirrored onto the underlying "slow" (real) type (see + # ``__setattr__``/``__delattr__``). It starts disabled so that the + # attributes installed while the proxy type is being built are not + # forwarded to the real type; ``make_*_proxy_type`` enables it once + # construction is complete. + type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", False) + + def _fsproxy_restore_slow_attr(cls, name): + # Undo a previously-mirrored class-level patch: revert the underlying + # "slow" (real) type to whatever it had before we touched ``name``. + slow = cls.__dict__.get("_fsproxy_slow_type") + stash = cls.__dict__.get("_fsproxy_slow_overrides") + if slow is None or stash is None or name not in stash: + return + original = stash.pop(name) + try: + if original is _NO_SLOW_ATTR: + if name in slow.__dict__: + delattr(slow, name) + else: + setattr(slow, name, original) + except (AttributeError, TypeError): + pass + + def __setattr__(cls, name, value): + # Class-level attribute assignments on a proxy type (e.g. + # ``monkeypatch.setattr(pd.ExcelFile, "parse", fn)``) must also be + # mirrored onto the underlying "slow" (real) type. Code that runs + # under ``disable_module_accelerator()`` (e.g. the pandas fallback + # path of ``pd.read_excel``) resolves attributes from the real + # class, not the proxy, so a patch applied only to the proxy would + # otherwise be invisible to that code. + type.__setattr__(cls, name, value) + if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): + # The proxy type is still being constructed (or this is a + # non-pandas proxy): only mirror user/runtime monkeypatches, + # never the custom methods cudf.pandas installs on the proxy + # classes itself. + return + if name.startswith("_"): + return + slow = cls.__dict__.get("_fsproxy_slow_type") + if slow is None: + return + if isinstance( + value, (_MethodProxy, _FastSlowAttribute, _FastSlowProxy) + ): + # Restoring the proxy's own machinery (e.g. ``monkeypatch`` + # teardown re-setting a previously-unpatched attribute, whose + # saved value is a ``_MethodProxy``/``_FastSlowAttribute``). + # Revert the real type to whatever it had before we touched it + # rather than shadowing its genuine implementation. + cls._fsproxy_restore_slow_attr(name) + return + # Real value being patched in: remember the real type's original + # so it can be restored later, then forward the patch. + stash = cls.__dict__.get("_fsproxy_slow_overrides") + if stash is None: + stash = {} + type.__setattr__(cls, "_fsproxy_slow_overrides", stash) + if name not in stash: + stash[name] = slow.__dict__.get(name, _NO_SLOW_ATTR) + try: + setattr(slow, name, value) + except (AttributeError, TypeError): + pass + + def __delattr__(cls, name): + # Mirror class-level attribute *deletions* onto the underlying "slow" + # (real) type as well. ``monkeypatch`` teardown deletes a proxy + # attribute that did not exist before the patch; restore the real + # type to whatever it had before the matching ``__setattr__``. + type.__delattr__(cls, name) + if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): + return + if name.startswith("_"): + return + cls._fsproxy_restore_slow_attr(name) + def __dir__(self): # Try to return the cached dir of the slow object, but if it # doesn't exist, fall back to the default implementation. diff --git a/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py b/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py index 36bd33ad4e67..7521c00c3db8 100644 --- a/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py +++ b/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py @@ -2720,19 +2720,6 @@ def pytest_unconfigure(config): "tests/io/excel/test_odswriter.py::test_cell_value_type[test string-string-string-value-test string]": "TODO: Add a reason for failure", "tests/io/excel/test_odswriter.py::test_cell_value_type[value4-date-date-value-2010-10-10T10:10:10]": "TODO: Add a reason for failure", "tests/io/excel/test_odswriter.py::test_cell_value_type[value5-date-date-value-2010-10-10]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('calamine', '.ods')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('calamine', '.xls')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('calamine', '.xlsb')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('calamine', '.xlsm')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('calamine', '.xlsx')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('odf', '.ods')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('openpyxl', '.xlsm')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('openpyxl', '.xlsx')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('pyxlsb', '.xlsb')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[('xlrd', '.xls')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[(None, '.xls')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[(None, '.xlsm')]": "TODO: Add a reason for failure", - "tests/io/excel/test_readers.py::TestReaders::test_engine_used[(None, '.xlsx')]": "TODO: Add a reason for failure", "tests/io/excel/test_style.py::test_format_hierarchical_rows_periodindex[False]": "AttributeError: _compute. Did you mean: 'compare'?", "tests/io/excel/test_style.py::test_format_hierarchical_rows_periodindex[True]": "AttributeError: _compute. Did you mean: 'compare'?", "tests/io/excel/test_style.py::test_format_hierarchical_rows_periodindex[columns]": "AttributeError: _compute. Did you mean: 'compare'?", @@ -3919,7 +3906,6 @@ def pytest_unconfigure(config): "tests/test_algos.py::TestValueCounts::test_value_counts_dropna": "pandas keeps bool-with-None data as object dtype; cudf stores it as a masked bool column", "tests/test_algos.py::TestValueCounts::test_value_counts_stability": "asserts that kind='quicksort' produces an unstable order; cudf sorts are always stable", "tests/test_col.py::test_cached_property": "AssertionError: assert False", - "tests/test_col.py::test_custom_accessor": "AttributeError: 'Series' object has no attribute 'xyz'", "tests/test_common.py::test_serializable[obj0]": "TODO: Add a reason for failure", "tests/test_common.py::test_standardize_mapping": "TODO: Add a reason for failure", "tests/test_common.py::test_temp_setattr[False]": "TODO: Add a reason for failure", diff --git a/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py b/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py index 8be51f2b6c90..e3ab3dbbab6e 100644 --- a/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py +++ b/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py @@ -12,7 +12,9 @@ import cudf.pandas.fast_slow_proxy from cudf.pandas.fast_slow_proxy import ( _fast_arg, + _FastSlowAttribute, _FunctionProxy, + _setattr_fsproxy_no_mirror, _slow_arg, _transform_arg, _Unusable, @@ -662,3 +664,106 @@ def test_tuple_with_attrs_transform(): assert b == bprime and b is not bprime assert c == cprime and c is not cprime assert d == dprime and d is not dprime + + +def _make_mirror_proxy(): + class Fast: + pass + + class Slow: + def existing(self): + return "slow original" + + Pxy = make_final_proxy_type( + "Pxy", + Fast, + Slow, + fast_to_slow=lambda fast: Slow(), + slow_to_fast=lambda slow: Fast(), + ) + return Fast, Slow, Pxy + + +def test_class_attr_mirroring_enabled_after_construction(): + # ``make_*_proxy_type`` enables per-type mirroring once the proxy type is + # fully built (it starts disabled so the methods installed during + # construction are not forwarded to the real type). + _, _, Pxy = _make_mirror_proxy() + assert Pxy.__dict__["_fsproxy_mirror_slow_overrides"] is True + + +def test_class_attr_setattr_mirrored_to_slow(): + # A class-level attribute write on the proxy is mirrored onto the + # underlying "slow" (real) type so it is visible to fallback code. + _, Slow, Pxy = _make_mirror_proxy() + + def patched(self): + return "patched" + + Pxy.new_method = patched + assert Slow.__dict__.get("new_method") is patched + + +def test_class_attr_delattr_restores_slow(): + # Deleting a previously-mirrored *new* attribute removes it from the slow + # type as well (the ``__delattr__`` path). + _, Slow, Pxy = _make_mirror_proxy() + + def patched(self): + return "patched" + + Pxy.new_method = patched + assert "new_method" in Slow.__dict__ + + del Pxy.new_method + assert "new_method" not in Pxy.__dict__ + assert "new_method" not in Slow.__dict__ + + +def test_class_attr_restore_existing_slow_attr(): + # Patching an attribute that already exists on the slow type, then + # restoring the proxy's auto-generated ``_FastSlowAttribute`` (as + # ``monkeypatch`` teardown does), reverts the real implementation. + _, Slow, Pxy = _make_mirror_proxy() + saved = Pxy.__dict__["existing"] + assert isinstance(saved, _FastSlowAttribute) + original_slow = Slow.__dict__["existing"] + + def patched(self): + return "patched" + + Pxy.existing = patched + assert Slow.__dict__["existing"] is patched + + Pxy.existing = saved + assert Slow.__dict__["existing"] is original_slow + + +def test_class_attr_monkeypatch_roundtrip(monkeypatch): + # End-to-end: ``monkeypatch`` of a brand-new attribute mirrors onto the + # slow type, and teardown (which deletes it) restores the slow type. + _, Slow, Pxy = _make_mirror_proxy() + + def fake(self): + return "fake" + + monkeypatch.setattr(Pxy, "brand_new", fake, raising=False) + assert Slow.__dict__.get("brand_new") is fake + + monkeypatch.undo() + assert "brand_new" not in Pxy.__dict__ + assert "brand_new" not in Slow.__dict__ + + +def test_setattr_fsproxy_no_mirror_skips_slow(): + # ``_setattr_fsproxy_no_mirror`` sets a class attribute on the proxy + # without forwarding it to the slow type (used for cudf.pandas' own custom + # methods such as ``DataFrame.query``/``eval``). + _, Slow, Pxy = _make_mirror_proxy() + + def custom(self): + return "custom" + + _setattr_fsproxy_no_mirror(Pxy, "custom_method", custom) + assert Pxy.__dict__["custom_method"] is custom + assert "custom_method" not in Slow.__dict__ From 49a88e474e192482d08109dc8387c05a8cc818ed Mon Sep 17 00:00:00 2001 From: galipremsagar Date: Mon, 13 Jul 2026 18:47:32 +0000 Subject: [PATCH 2/3] Replace stash-based restore with pristine-state translation for class-attr mirroring Address review: __delattr__ now mirrors deletion as deletion (no restore), and the runtime patch stash (_fsproxy_slow_overrides/_fsproxy_restore_slow_attr) is gone. Instead, each proxy type snapshots its pristine public class attributes alongside the slow type's pristine class-dict entries when mirroring is enabled; __setattr__ translates assigned values into slow space: re-assigning the proxy's pristine attribute (what monkeypatch and mock.patch save and re-assign on undo) restores the slow type's pristine attribute, and proxy machinery unwraps to the slow object it delegates to. This also fixes real defects in the stash design's coverage: undo through unittest.mock (which saves the raw unresolved descriptor), patches of non-method attributes (properties, accessors, data attrs, cudf-installed attributes like DataFrame.columns/eval/query and Series.str, whose leak caused infinite recursion on the real type), classmethod/staticmethod descriptor preservation, and inherited methods no longer being copied into the slow type's dict on undo. --- python/cudf/cudf/pandas/fast_slow_proxy.py | 188 +++++++++---- .../cudf_pandas_tests/test_cudf_pandas.py | 32 +++ .../cudf_pandas_tests/test_fast_slow_proxy.py | 250 +++++++++++++++++- 3 files changed, 409 insertions(+), 61 deletions(-) diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index f9704137c51f..ee5966e43e56 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -362,10 +362,10 @@ def _fsproxy_state(self) -> _State: final_type_map[fast_type] = cls final_type_map[slow_type] = cls - # Proxy type fully constructed: from here on, class-level attribute writes - # are genuine runtime monkeypatches and should be mirrored onto the - # underlying "slow" (real) type. - type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", True) + # Proxy type fully constructed: snapshot its pristine state and, from + # here on, mirror class-level attribute writes (genuine runtime + # monkeypatches) onto the underlying "slow" (real) type. + _enable_fsproxy_mirroring(cls) return cls @@ -516,10 +516,10 @@ def _fsproxy_fast_to_slow(self): intermediate_type_map[fast_type] = cls intermediate_type_map[slow_type] = cls - # Proxy type fully constructed: from here on, class-level attribute writes - # are genuine runtime monkeypatches and should be mirrored onto the - # underlying "slow" (real) type. - type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", True) + # Proxy type fully constructed: snapshot its pristine state and, from + # here on, mirror class-level attribute writes (genuine runtime + # monkeypatches) onto the underlying "slow" (real) type. + _enable_fsproxy_mirroring(cls) return cls @@ -570,20 +570,105 @@ def get_registered_functions(): return dict() -_NO_SLOW_ATTR = object() +_MIRROR_SKIP = object() +_SLOW_ABSENT = object() + + +def _enable_fsproxy_mirroring(cls: type) -> None: + """Finalize a proxy type for class-level patch mirroring. + + Snapshots the proxy type's pristine public class attributes together + with the slow type's pristine class-dict entries for the same names, + then enables mirroring of class-level attribute writes/deletions onto + the slow type (see ``_FastSlowProxyMeta.__setattr__``/``__delattr__``). + + The snapshot is a fixed translation table, not runtime patch tracking: + re-assigning the proxy's pristine attribute for ``name`` (which is what + ``monkeypatch``/``mock.patch`` save and re-assign on undo) translates + to restoring the slow type's pristine attribute for ``name``. + """ + slow = cls.__dict__.get("_fsproxy_slow_type") + pristine = {} + if slow is not None: + for name, value in cls.__dict__.items(): + if name.startswith("_"): + continue + pristine[name] = (value, slow.__dict__.get(name, _SLOW_ABSENT)) + type.__setattr__(cls, "_fsproxy_pristine_attrs", pristine) + type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", True) def _setattr_fsproxy_no_mirror(cls: type, name: str, value: Any) -> None: - """Set ``cls.name = value`` on a proxy type *without* mirroring to slow. + """Install a cudf.pandas-internal attribute on a proxy type. ``_FastSlowProxyMeta.__setattr__`` mirrors class-level attribute writes onto the underlying "slow" (real) type so that runtime monkeypatches stay visible to the pandas fallback path. cudf.pandas itself installs a handful of custom methods (e.g. ``DataFrame.query``/``DataFrame.eval``) onto the proxy classes that must *not* clobber pandas' genuine implementations; use - this helper for those (rare) assignments. + this helper for those (rare) assignments. The attribute is registered as + part of the proxy's pristine state so that a later save/patch/re-assign + cycle restores the slow type's own attribute rather than forwarding the + cudf-internal object to it. """ type.__setattr__(cls, name, value) + pristine = cls.__dict__.get("_fsproxy_pristine_attrs") + slow = cls.__dict__.get("_fsproxy_slow_type") + if pristine is not None and slow is not None and not name.startswith("_"): + pristine[name] = (value, slow.__dict__.get(name, _SLOW_ABSENT)) + + +def _mirror_value_to_slow( + value: Any, slow: type, name: str, pristine: dict +) -> Any: + """Translate a value assigned on a proxy type into "slow" space. + + A value read off a proxy type (e.g. the original that a caller saves + before patching and re-assigns to undo) is proxy machinery wrapping a + slow-side object; mirroring it verbatim would install that machinery on + the real type. Unwrap it to the slow object it delegates to instead, so + that save/patch/re-assign cycles round-trip on the real type. + + Returns ``_MIRROR_SKIP`` when the value has no determinable slow-side + equivalent, in which case nothing should be mirrored. + """ + if isinstance(value, _FastSlowAttribute): + # The proxy's own delegating descriptor (a *pristine* one is already + # handled by identity in ``__setattr__``; this covers a descriptor + # obtained some other way): its slow equivalent is the method it + # wraps, if it ever resolved one. + attr = value._attr + if not isinstance(attr, _MethodProxy): + return _MIRROR_SKIP + value = attr + if isinstance(value, _FunctionProxy): + unwrapped = value._fsproxy_slow + entry = pristine.get(name) + if entry is not None and entry[1] is not _SLOW_ABSENT: + # Re-assigning a saved ``cls.method`` (a ``_MethodProxy`` over + # the *resolved* slow attribute): if it resolves back to the + # slow type's pristine attribute, restore the pristine + # class-dict entry itself so ``classmethod``/``staticmethod`` + # descriptors are not degraded to their bound/plain-function + # forms. + descriptor = entry[1] + try: + resolved = ( + descriptor.__get__(None, slow) + if hasattr(type(descriptor), "__get__") + else descriptor + ) + if unwrapped is resolved or unwrapped == resolved: + return descriptor + except Exception: + pass + return unwrapped + if isinstance(value, _FastSlowProxy): + return value._fsproxy_slow + if isinstance(value, _FastSlowProxyMeta): + slow_type = getattr(value, "_fsproxy_slow_type", None) + return slow_type if slow_type is not None else _MIRROR_SKIP + return value class _FastSlowProxyMeta(type): @@ -614,23 +699,6 @@ def __init__(cls, *args, **kwargs): # construction is complete. type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", False) - def _fsproxy_restore_slow_attr(cls, name): - # Undo a previously-mirrored class-level patch: revert the underlying - # "slow" (real) type to whatever it had before we touched ``name``. - slow = cls.__dict__.get("_fsproxy_slow_type") - stash = cls.__dict__.get("_fsproxy_slow_overrides") - if slow is None or stash is None or name not in stash: - return - original = stash.pop(name) - try: - if original is _NO_SLOW_ATTR: - if name in slow.__dict__: - delattr(slow, name) - else: - setattr(slow, name, original) - except (AttributeError, TypeError): - pass - def __setattr__(cls, name, value): # Class-level attribute assignments on a proxy type (e.g. # ``monkeypatch.setattr(pd.ExcelFile, "parse", fn)``) must also be @@ -638,7 +706,12 @@ def __setattr__(cls, name, value): # under ``disable_module_accelerator()`` (e.g. the pandas fallback # path of ``pd.read_excel``) resolves attributes from the real # class, not the proxy, so a patch applied only to the proxy would - # otherwise be invisible to that code. + # otherwise be invisible to that code. The assigned value is first + # translated into its slow-space equivalent: re-assigning the + # proxy's pristine attribute translates to the slow type's pristine + # attribute, and proxy machinery is unwrapped to the slow object it + # delegates to, so save/patch/re-assign cycles round-trip on the + # real type as well. type.__setattr__(cls, name, value) if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): # The proxy type is still being constructed (or this is a @@ -651,40 +724,49 @@ def __setattr__(cls, name, value): slow = cls.__dict__.get("_fsproxy_slow_type") if slow is None: return - if isinstance( - value, (_MethodProxy, _FastSlowAttribute, _FastSlowProxy) - ): - # Restoring the proxy's own machinery (e.g. ``monkeypatch`` - # teardown re-setting a previously-unpatched attribute, whose - # saved value is a ``_MethodProxy``/``_FastSlowAttribute``). - # Revert the real type to whatever it had before we touched it - # rather than shadowing its genuine implementation. - cls._fsproxy_restore_slow_attr(name) - return - # Real value being patched in: remember the real type's original - # so it can be restored later, then forward the patch. - stash = cls.__dict__.get("_fsproxy_slow_overrides") - if stash is None: - stash = {} - type.__setattr__(cls, "_fsproxy_slow_overrides", stash) - if name not in stash: - stash[name] = slow.__dict__.get(name, _NO_SLOW_ATTR) try: - setattr(slow, name, value) - except (AttributeError, TypeError): + # Mirroring is best-effort: translating a wrapped proxy instance + # can require a fast-to-slow conversion, which may itself fail; + # never let that escape an otherwise-successful assignment. + pristine = cls.__dict__.get("_fsproxy_pristine_attrs") or {} + entry = pristine.get(name) + if entry is not None and value is entry[0]: + # The proxy's pristine attribute for ``name`` is being + # re-assigned (e.g. ``monkeypatch``/``mock.patch`` undo + # re-setting the saved class-dict entry). Its slow-space + # equivalent is the slow type's pristine attribute: restore + # it, or remove the mirrored entry if the slow type had + # none of its own (leaving any inherited one visible). + if entry[1] is _SLOW_ABSENT: + if name in slow.__dict__: + delattr(slow, name) + else: + setattr(slow, name, entry[1]) + return + slow_value = _mirror_value_to_slow(value, slow, name, pristine) + if slow_value is _MIRROR_SKIP: + return + setattr(slow, name, slow_value) + except Exception: pass def __delattr__(cls, name): # Mirror class-level attribute *deletions* onto the underlying "slow" - # (real) type as well. ``monkeypatch`` teardown deletes a proxy - # attribute that did not exist before the patch; restore the real - # type to whatever it had before the matching ``__setattr__``. + # (real) type as well, for the same reason as ``__setattr__``: after + # ``del cls.name`` the attribute is gone from the proxy, so it must + # also be gone from the real type seen by fallback code. type.__delattr__(cls, name) if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): return if name.startswith("_"): return - cls._fsproxy_restore_slow_attr(name) + slow = cls.__dict__.get("_fsproxy_slow_type") + if slow is None: + return + try: + delattr(slow, name) + except (AttributeError, TypeError): + pass def __dir__(self): # Try to return the cached dir of the slow object, but if it diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index cbf3e17bfdc4..792e4f2cf314 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -2186,6 +2186,38 @@ def test_module_proxy_write_through_config(monkeypatch): cf.register_option("foo", 1) +def test_class_monkeypatch_roundtrip_restores_real_pandas(monkeypatch): + # Class-level patches on proxy types are mirrored onto the real pandas + # type (so they stay visible to fallback code running under + # ``disable_module_accelerator``); undoing them must restore the real + # type's own attributes — including for attributes that cudf.pandas + # replaces on the proxy, like ``columns``/``eval``/``str``. + real_df = xpd.DataFrame._fsproxy_slow + real_series = xpd.Series._fsproxy_slow + orig_columns = real_df.__dict__["columns"] + orig_eval = real_df.__dict__["eval"] + orig_str = real_series.__dict__["str"] + + def fake_eval(self, *args, **kwargs): + return "patched" + + monkeypatch.setattr(xpd.DataFrame, "eval", fake_eval) + assert real_df.__dict__["eval"] is fake_eval + monkeypatch.setattr( + xpd.DataFrame, "columns", property(lambda self: "patched") + ) + monkeypatch.setattr(xpd.Series, "str", property(lambda self: "patched")) + monkeypatch.undo() + + assert real_df.__dict__["columns"] is orig_columns + assert real_df.__dict__["eval"] is orig_eval + assert real_series.__dict__["str"] is orig_str + # The real type must remain fully functional on the fallback path. + df = real_df({"a": [1, 2]}) + assert list(df.columns) == ["a"] + assert list(df.eval("b = a + 1").columns) == ["a", "b"] + + @pytest.mark.parametrize("box", ["Series", "array"]) @pytest.mark.parametrize("na_value", [pd.NA, np.nan], ids=["NA", "NaN"]) @pytest.mark.parametrize("storage", ["python", "pyarrow"]) diff --git a/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py b/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py index e3ab3dbbab6e..8484bc7ebfa7 100644 --- a/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py +++ b/python/cudf/cudf_pandas_tests/test_fast_slow_proxy.py @@ -670,10 +670,28 @@ def _make_mirror_proxy(): class Fast: pass - class Slow: + class SlowBase: + def inherited(self): + return "base" + + class Slow(SlowBase): + const = 42 + def existing(self): return "slow original" + @property + def prop(self): + return "slow prop" + + @staticmethod + def smethod(x): + return x + 1 + + @classmethod + def cmethod(cls): + return cls.__name__ + Pxy = make_final_proxy_type( "Pxy", Fast, @@ -704,9 +722,9 @@ def patched(self): assert Slow.__dict__.get("new_method") is patched -def test_class_attr_delattr_restores_slow(): - # Deleting a previously-mirrored *new* attribute removes it from the slow - # type as well (the ``__delattr__`` path). +def test_class_attr_delattr_mirrored_to_slow(): + # Deleting a class-level attribute on the proxy mirrors the deletion + # onto the slow type (the ``__delattr__`` path). _, Slow, Pxy = _make_mirror_proxy() def patched(self): @@ -720,10 +738,28 @@ def patched(self): assert "new_method" not in Slow.__dict__ +def test_class_attr_delattr_existing_removes_from_slow(): + # Deleting is deleting, not restoring: patching an existing attribute + # and then deleting it removes it from both the proxy and the slow type, + # exactly as the same sequence would on a plain Python class. + _, Slow, Pxy = _make_mirror_proxy() + + def patched(self): + return "patched" + + Pxy.existing = patched + assert Slow.__dict__["existing"] is patched + + del Pxy.existing + assert "existing" not in Pxy.__dict__ + assert "existing" not in Slow.__dict__ + + def test_class_attr_restore_existing_slow_attr(): - # Patching an attribute that already exists on the slow type, then - # restoring the proxy's auto-generated ``_FastSlowAttribute`` (as - # ``monkeypatch`` teardown does), reverts the real implementation. + # Re-assigning the proxy's pristine class-dict entry (as ``monkeypatch`` + # and ``mock.patch`` teardown do) restores the slow type's pristine + # attribute. Note: no prior class-level getattr — the saved descriptor + # is unresolved, as in the ``mock.patch.object`` flow. _, Slow, Pxy = _make_mirror_proxy() saved = Pxy.__dict__["existing"] assert isinstance(saved, _FastSlowAttribute) @@ -739,9 +775,27 @@ def patched(self): assert Slow.__dict__["existing"] is original_slow +def test_class_attr_restore_via_saved_method_proxy(): + # Re-assigning a saved ``Pxy.method`` (a ``_MethodProxy`` over the + # resolved slow attribute, not the class-dict descriptor) also restores + # the slow type's pristine attribute. + _, Slow, Pxy = _make_mirror_proxy() + saved = getattr(Pxy, "existing") + original_slow = Slow.__dict__["existing"] + + def patched(self): + return "patched" + + Pxy.existing = patched + assert Slow.__dict__["existing"] is patched + + Pxy.existing = saved + assert Slow.__dict__["existing"] is original_slow + + def test_class_attr_monkeypatch_roundtrip(monkeypatch): # End-to-end: ``monkeypatch`` of a brand-new attribute mirrors onto the - # slow type, and teardown (which deletes it) restores the slow type. + # slow type, and teardown (which deletes it) mirrors the deletion. _, Slow, Pxy = _make_mirror_proxy() def fake(self): @@ -755,6 +809,160 @@ def fake(self): assert "brand_new" not in Slow.__dict__ +def test_class_attr_monkeypatch_existing_roundtrip(monkeypatch): + # End-to-end: ``monkeypatch`` of a pre-existing method mirrors the patch + # onto the slow type, and teardown (which re-assigns the saved proxy + # descriptor) restores the slow type's original implementation. + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["existing"] + + def fake(self): + return "fake" + + monkeypatch.setattr(Pxy, "existing", fake) + assert Slow.__dict__["existing"] is fake + + monkeypatch.undo() + assert isinstance(Pxy.__dict__["existing"], _FastSlowAttribute) + assert Slow.__dict__["existing"] is original_slow + + +def test_class_attr_nested_monkeypatch_existing_roundtrip(): + # Nested patches of the same pre-existing method unwind in order, + # each level restoring the slow type to the previous state. + from _pytest.monkeypatch import MonkeyPatch + + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["existing"] + + def fake1(self): + return "fake1" + + def fake2(self): + return "fake2" + + mp1, mp2 = MonkeyPatch(), MonkeyPatch() + mp1.setattr(Pxy, "existing", fake1) + assert Slow.__dict__["existing"] is fake1 + mp2.setattr(Pxy, "existing", fake2) + assert Slow.__dict__["existing"] is fake2 + + mp2.undo() + assert Slow.__dict__["existing"] is fake1 + mp1.undo() + assert Slow.__dict__["existing"] is original_slow + + +def test_class_attr_mock_patch_object_roundtrip(): + # ``unittest.mock.patch.object`` saves the raw class-dict entry without + # a prior getattr (so the saved descriptor is never resolved); undo must + # still restore the slow type's pristine attribute. + from unittest import mock + + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["existing"] + + def fake(self): + return "fake" + + with mock.patch.object(Pxy, "existing", fake): + assert Slow.__dict__["existing"] is fake + assert Slow.__dict__["existing"] is original_slow + + +def test_class_attr_property_monkeypatch_roundtrip(monkeypatch): + # Patching a property mirrors it onto the slow type; undo restores the + # slow type's pristine property object. + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["prop"] + + fake = property(lambda self: "fake") + monkeypatch.setattr(Pxy, "prop", fake) + assert Slow.__dict__["prop"] is fake + assert Slow().prop == "fake" + + monkeypatch.undo() + assert Slow.__dict__["prop"] is original_slow + assert Slow().prop == "slow prop" + + +def test_class_attr_data_attr_monkeypatch_roundtrip(monkeypatch): + # Patching a plain class data attribute round-trips on the slow type. + _, Slow, Pxy = _make_mirror_proxy() + + monkeypatch.setattr(Pxy, "const", 99) + assert Slow.const == 99 + + monkeypatch.undo() + assert Slow.const == 42 + + +def test_class_attr_staticmethod_monkeypatch_roundtrip(monkeypatch): + # Undo restores the slow type's pristine ``staticmethod`` descriptor, + # not the plain function it resolves to (which would break instance + # calls by receiving ``self``). + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["smethod"] + assert isinstance(original_slow, staticmethod) + + monkeypatch.setattr(Pxy, "smethod", staticmethod(lambda x: x - 1)) + assert Slow.smethod(1) == 0 + + monkeypatch.undo() + assert Slow.__dict__["smethod"] is original_slow + assert Slow().smethod(1) == 2 + + +def test_class_attr_classmethod_monkeypatch_roundtrip(monkeypatch): + # Undo restores the slow type's pristine ``classmethod`` descriptor, + # not the class-bound method it resolves to (which would pin ``cls`` + # for subclasses). + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["cmethod"] + assert isinstance(original_slow, classmethod) + + monkeypatch.setattr(Pxy, "cmethod", classmethod(lambda cls: "fake")) + assert Slow.cmethod() == "fake" + + monkeypatch.undo() + assert Slow.__dict__["cmethod"] is original_slow + assert Slow.cmethod() == "Slow" + + +def test_class_attr_inherited_method_monkeypatch_roundtrip(monkeypatch): + # Patching a method the slow type only inherits mirrors it into the slow + # type's own dict; undo removes that entry again (rather than copying + # the base-class implementation into the subclass), leaving the + # inherited implementation visible. + _, Slow, Pxy = _make_mirror_proxy() + assert "inherited" not in Slow.__dict__ + + def fake(self): + return "fake" + + monkeypatch.setattr(Pxy, "inherited", fake) + assert Slow.__dict__["inherited"] is fake + + monkeypatch.undo() + assert "inherited" not in Slow.__dict__ + assert Slow().inherited() == "base" + + +def test_class_attr_monkeypatch_delattr_roundtrip(monkeypatch): + # ``monkeypatch.delattr`` mirrors the deletion onto the slow type, and + # undo (which re-assigns the saved pristine descriptor) restores the + # slow type's pristine attribute. + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["existing"] + + monkeypatch.delattr(Pxy, "existing") + assert "existing" not in Pxy.__dict__ + assert "existing" not in Slow.__dict__ + + monkeypatch.undo() + assert Slow.__dict__["existing"] is original_slow + + def test_setattr_fsproxy_no_mirror_skips_slow(): # ``_setattr_fsproxy_no_mirror`` sets a class attribute on the proxy # without forwarding it to the slow type (used for cudf.pandas' own custom @@ -767,3 +975,29 @@ def custom(self): _setattr_fsproxy_no_mirror(Pxy, "custom_method", custom) assert Pxy.__dict__["custom_method"] is custom assert "custom_method" not in Slow.__dict__ + + +def test_setattr_fsproxy_no_mirror_monkeypatch_roundtrip(monkeypatch): + # A cudf-installed custom attribute (e.g. ``DataFrame.eval``/``query``) + # participates in the pristine state: monkeypatching it and undoing + # restores the proxy's custom object AND the slow type's own genuine + # implementation — the cudf-internal object is never forwarded to the + # slow type. + _, Slow, Pxy = _make_mirror_proxy() + original_slow = Slow.__dict__["existing"] + + def custom(self): + return "cudf custom" + + _setattr_fsproxy_no_mirror(Pxy, "existing", custom) + assert Slow.__dict__["existing"] is original_slow + + def fake(self): + return "fake" + + monkeypatch.setattr(Pxy, "existing", fake) + assert Slow.__dict__["existing"] is fake + + monkeypatch.undo() + assert Pxy.__dict__["existing"] is custom + assert Slow.__dict__["existing"] is original_slow From ec3a6a3aec7cea51d0e808028d3e8a97761ce39e Mon Sep 17 00:00:00 2001 From: galipremsagar Date: Tue, 21 Jul 2026 13:23:55 +0000 Subject: [PATCH 3/3] Address review: inline mirror translation, validate flag/pristine invariants Initialize _fsproxy_mirror_slow_overrides in the metaclass __new__ rather than __init__: cooperating metaclasses can perform class-level attribute writes from their own __new__ (ABCMeta.__new__ assigns __abstractmethods__ for the ExcelFile/ExcelWriter proxies), dispatching to the mirroring __setattr__ before __init__ runs. With the flag guaranteed to exist, access it (and _fsproxy_slow_type/_fsproxy_pristine_attrs, both guaranteed once the flag is set by _enable_fsproxy_mirroring) unconditionally instead of via defensive cls.__dict__.get lookups. Also inline _mirror_value_to_slow into __setattr__ so its early returns read directly at the call site, dropping the _MIRROR_SKIP sentinel, and expand the comment on the _SLOW_ABSENT undo branch explaining why a mirrored patch for a slow-inherited attribute must be deleted on restore. --- python/cudf/cudf/pandas/fast_slow_proxy.py | 171 ++++++++++----------- 1 file changed, 85 insertions(+), 86 deletions(-) diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index 0aa501a13cec..60fa07dc37b4 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -570,7 +570,6 @@ def get_registered_functions(): return dict() -_MIRROR_SKIP = object() _SLOW_ABSENT = object() @@ -587,13 +586,12 @@ def _enable_fsproxy_mirroring(cls: type) -> None: ``monkeypatch``/``mock.patch`` save and re-assign on undo) translates to restoring the slow type's pristine attribute for ``name``. """ - slow = cls.__dict__.get("_fsproxy_slow_type") - pristine = {} - if slow is not None: - for name, value in cls.__dict__.items(): - if name.startswith("_"): - continue - pristine[name] = (value, slow.__dict__.get(name, _SLOW_ABSENT)) + slow = cls._fsproxy_slow_type # type: ignore[attr-defined] + pristine = { + name: (value, slow.__dict__.get(name, _SLOW_ABSENT)) + for name, value in cls.__dict__.items() + if not name.startswith("_") + } type.__setattr__(cls, "_fsproxy_pristine_attrs", pristine) type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", True) @@ -606,69 +604,19 @@ def _setattr_fsproxy_no_mirror(cls: type, name: str, value: Any) -> None: visible to the pandas fallback path. cudf.pandas itself installs a handful of custom methods (e.g. ``DataFrame.query``/``DataFrame.eval``) onto the proxy classes that must *not* clobber pandas' genuine implementations; use - this helper for those (rare) assignments. The attribute is registered as + this helper for those (rare) assignments, after the proxy type has been + fully constructed by ``make_*_proxy_type``. The attribute is registered as part of the proxy's pristine state so that a later save/patch/re-assign cycle restores the slow type's own attribute rather than forwarding the cudf-internal object to it. """ type.__setattr__(cls, name, value) - pristine = cls.__dict__.get("_fsproxy_pristine_attrs") - slow = cls.__dict__.get("_fsproxy_slow_type") - if pristine is not None and slow is not None and not name.startswith("_"): - pristine[name] = (value, slow.__dict__.get(name, _SLOW_ABSENT)) - - -def _mirror_value_to_slow( - value: Any, slow: type, name: str, pristine: dict -) -> Any: - """Translate a value assigned on a proxy type into "slow" space. - - A value read off a proxy type (e.g. the original that a caller saves - before patching and re-assigns to undo) is proxy machinery wrapping a - slow-side object; mirroring it verbatim would install that machinery on - the real type. Unwrap it to the slow object it delegates to instead, so - that save/patch/re-assign cycles round-trip on the real type. - - Returns ``_MIRROR_SKIP`` when the value has no determinable slow-side - equivalent, in which case nothing should be mirrored. - """ - if isinstance(value, _FastSlowAttribute): - # The proxy's own delegating descriptor (a *pristine* one is already - # handled by identity in ``__setattr__``; this covers a descriptor - # obtained some other way): its slow equivalent is the method it - # wraps, if it ever resolved one. - attr = value._attr - if not isinstance(attr, _MethodProxy): - return _MIRROR_SKIP - value = attr - if isinstance(value, _FunctionProxy): - unwrapped = value._fsproxy_slow - entry = pristine.get(name) - if entry is not None and entry[1] is not _SLOW_ABSENT: - # Re-assigning a saved ``cls.method`` (a ``_MethodProxy`` over - # the *resolved* slow attribute): if it resolves back to the - # slow type's pristine attribute, restore the pristine - # class-dict entry itself so ``classmethod``/``staticmethod`` - # descriptors are not degraded to their bound/plain-function - # forms. - descriptor = entry[1] - try: - resolved = ( - descriptor.__get__(None, slow) - if hasattr(type(descriptor), "__get__") - else descriptor - ) - if unwrapped is resolved or unwrapped == resolved: - return descriptor - except Exception: - pass - return unwrapped - if isinstance(value, _FastSlowProxy): - return value._fsproxy_slow - if isinstance(value, _FastSlowProxyMeta): - slow_type = getattr(value, "_fsproxy_slow_type", None) - return slow_type if slow_type is not None else _MIRROR_SKIP - return value + if not name.startswith("_"): + slow = cls._fsproxy_slow_type # type: ignore[attr-defined] + cls._fsproxy_pristine_attrs[name] = ( # type: ignore[attr-defined] + value, + slow.__dict__.get(name, _SLOW_ABSENT), + ) class _FastSlowProxyMeta(type): @@ -689,15 +637,20 @@ def _fsproxy_slow(self) -> type: def _fsproxy_fast(self) -> type: return self._fsproxy_fast_type - def __init__(cls, *args, **kwargs): - super().__init__(*args, **kwargs) + def __new__(mcls, *args, **kwargs): + cls = super().__new__(mcls, *args, **kwargs) # Per-proxy-type switch controlling whether class-level attribute # writes are mirrored onto the underlying "slow" (real) type (see # ``__setattr__``/``__delattr__``). It starts disabled so that the # attributes installed while the proxy type is being built are not # forwarded to the real type; ``make_*_proxy_type`` enables it once - # construction is complete. + # construction is complete. Initialized in ``__new__`` rather than + # ``__init__`` because cooperating metaclasses may perform + # class-level attribute writes from their own ``__new__`` — e.g. + # ``ABCMeta.__new__`` assigns ``__abstractmethods__``, dispatching + # to ``__setattr__`` below before ``__init__`` ever runs. type.__setattr__(cls, "_fsproxy_mirror_slow_overrides", False) + return cls def __setattr__(cls, name, value): # Class-level attribute assignments on a proxy type (e.g. @@ -713,7 +666,7 @@ def __setattr__(cls, name, value): # delegates to, so save/patch/re-assign cycles round-trip on the # real type as well. type.__setattr__(cls, name, value) - if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): + if not cls._fsproxy_mirror_slow_overrides: # The proxy type is still being constructed (or this is a # non-pandas proxy): only mirror user/runtime monkeypatches, # never the custom methods cudf.pandas installs on the proxy @@ -721,32 +674,81 @@ def __setattr__(cls, name, value): return if name.startswith("_"): return - slow = cls.__dict__.get("_fsproxy_slow_type") - if slow is None: - return + slow = cls._fsproxy_slow_type try: # Mirroring is best-effort: translating a wrapped proxy instance # can require a fast-to-slow conversion, which may itself fail; # never let that escape an otherwise-successful assignment. - pristine = cls.__dict__.get("_fsproxy_pristine_attrs") or {} + pristine = cls._fsproxy_pristine_attrs entry = pristine.get(name) if entry is not None and value is entry[0]: # The proxy's pristine attribute for ``name`` is being # re-assigned (e.g. ``monkeypatch``/``mock.patch`` undo # re-setting the saved class-dict entry). Its slow-space - # equivalent is the slow type's pristine attribute: restore - # it, or remove the mirrored entry if the slow type had - # none of its own (leaving any inherited one visible). + # equivalent is the slow type's pristine attribute. if entry[1] is _SLOW_ABSENT: + # The slow type never defined ``name`` itself: the proxy + # mirrors the slow type's *dir*, so it has pristine + # attributes for methods the slow type only inherits + # (e.g. ``DataFrame.head`` lives on ``NDFrame``). + # Mirroring a patch for such a name added a shadowing + # entry to the slow type's dict; undoing the patch must + # remove that entry again so the inherited + # implementation becomes visible. It may legitimately be + # missing (the mirror is best-effort), hence the guard. if name in slow.__dict__: delattr(slow, name) else: setattr(slow, name, entry[1]) return - slow_value = _mirror_value_to_slow(value, slow, name, pristine) - if slow_value is _MIRROR_SKIP: - return - setattr(slow, name, slow_value) + # Otherwise translate the assigned value into "slow" space + # before mirroring it: a value read off a proxy type (e.g. the + # original that a caller saves before patching and re-assigns + # to undo) is proxy machinery wrapping a slow-side object, and + # mirroring it verbatim would install that machinery on the + # real type. Unwrap it to the slow object it delegates to, so + # save/patch/re-assign cycles round-trip on the real type; + # values with no determinable slow-side equivalent are not + # mirrored at all. + if isinstance(value, _FastSlowAttribute): + # The proxy's own delegating descriptor (a *pristine* one is + # already handled by identity above; this covers a + # descriptor obtained some other way): its slow equivalent + # is the method it wraps, if it ever resolved one. + attr = value._attr + if not isinstance(attr, _MethodProxy): + return + value = attr + if isinstance(value, _FunctionProxy): + unwrapped = value._fsproxy_slow + if entry is not None and entry[1] is not _SLOW_ABSENT: + # Re-assigning a saved ``cls.method`` (a ``_MethodProxy`` + # over the *resolved* slow attribute): if it resolves + # back to the slow type's pristine attribute, restore + # the pristine class-dict entry itself so + # ``classmethod``/``staticmethod`` descriptors are not + # degraded to their bound/plain-function forms. + descriptor = entry[1] + try: + resolved = ( + descriptor.__get__(None, slow) + if hasattr(type(descriptor), "__get__") + else descriptor + ) + if unwrapped is resolved or unwrapped == resolved: + setattr(slow, name, descriptor) + return + except Exception: + pass + setattr(slow, name, unwrapped) + elif isinstance(value, _FastSlowProxy): + setattr(slow, name, value._fsproxy_slow) + elif isinstance(value, _FastSlowProxyMeta): + slow_type = getattr(value, "_fsproxy_slow_type", None) + if slow_type is not None: + setattr(slow, name, slow_type) + else: + setattr(slow, name, value) except Exception: pass @@ -756,15 +758,12 @@ def __delattr__(cls, name): # ``del cls.name`` the attribute is gone from the proxy, so it must # also be gone from the real type seen by fallback code. type.__delattr__(cls, name) - if not cls.__dict__.get("_fsproxy_mirror_slow_overrides", False): + if not cls._fsproxy_mirror_slow_overrides: return if name.startswith("_"): return - slow = cls.__dict__.get("_fsproxy_slow_type") - if slow is None: - return try: - delattr(slow, name) + delattr(cls._fsproxy_slow_type, name) except (AttributeError, TypeError): pass