• 
      

    Fix unspy replacing staticmethods and classmethods with plain functions.

    Review Request #15147 — Created July 2, 2026 and submitted — Latest diff uploaded

    Information

    kgb
    master

    Reviewers

    kgb

    When spying on a staticmethod or classmethod defined directly on a
    class (passing the method plus owner=), unspy() replaced the
    descriptor on the class with a plain function, instead of restoring the
    original staticmethod or classmethod.

    The cause was in how the spy records the value to restore. When the
    owner does not need patching (a class that owns the attribute directly,
    not slippery, not inherited), the spy stored self.orig_func as the
    restore value. For a staticmethod or classmethod, orig_func is the
    function obtained through attribute access, which has already been
    unwrapped by the descriptor. On unspy(), that unwrapped function was
    assigned back onto the class, discarding the descriptor.

    While spied, everything still worked, because the spy swaps the code
    object in place and the descriptor keeps pointing at the same function.
    The corruption only appeared after unspy(): the attribute was now a
    plain function, so a later call through an instance bound the instance
    as the first positional argument. For a staticmethod this raised a
    TypeError. For a classmethod the instance was silently passed where
    the class was expected.

    We now store the attribute value exactly as defined in the owner
    class dict for this case, matching what the owner-patching path
    already does, so the descriptor is preserved across unspy().

    This also removes a workaround in the test base class that manually
    restored MathClass.class_do_math after each test. That workaround
    existed only to paper over this bug, and it corrupted the classmethod
    descriptor for later tests. One test
    (test_construction_with_call_fake_and_classmethod) depended on that
    corruption, asserting object identity across two classmethod accesses;
    it now compares by equality, which is correct for a classmethod that
    returns a fresh bound method each time.

    • Commented out the fix and saw the new unit tests fail.
    • Ran the full test suite on Python 3.14.
    • Verified all staticmethod and classmethod tests pass in isolation,
      confirming they no longer depend on inter-test state.

    Commits

    Files