Skip to content

Bind receivers for ordinary functions stored on classes - #904

Open
yangfan-yf-yf wants to merge 1 commit into
python-rope:masterfrom
yangfan-yf-yf:fix/class-stored-function-receiver
Open

yangfan-yf-yf wants to merge 1 commit into
python-rope:masterfrom
yangfan-yf-yf:fix/class-stored-function-receiver

Conversation

@yangfan-yf-yf

Copy link
Copy Markdown
Contributor

Description

I found that field Rename can infer the wrong receiver when a plain module function is stored on a class:

def module_callback(self, item):
    return self

class Owner:
    def __init__(self):
        self.field = 1
    callback = module_callback

class Other:
    def __init__(self):
        self.field = 2

result = Owner().callback(Other())
print(result.field)

Python returns an Owner, but Rope can infer an Other. Renaming Owner.field then misses result.field, while renaming Other.field changes a reference that belongs to Owner. Both edits can break execution. Saving the access as saved = Owner().callback also loses the receiver before saved(Other()) is analyzed.

I retain the receiver in a bound access value while preserving the original function, class slot, and parameter source identities. Direct and saved instance calls use that receiver once; class access remains unbound. Inherited slots and imported module functions keep their original storage and lexical modules. Direct functions placed on the instance itself retain their unbound behavior, including writes through an alias of that instance. Overriding a different instance does not change the first instance's access.

I also pass the bound receiver to the original function during call analysis and preserve the callable and receiver in saved object records. The execution checks cover field and source-name Rename, imported parameter keyword Rename, ordinary module analysis, and reading saved call values after closing and reopening the project.

The supported storage is an unambiguous class-body name assignment of an undecorated module function with an ordinary positional parameter for the receiver and no positional-only or keyword-only parameters. Override checks examine the storage, receiver, and access modules. Arbitrary writes in other modules, decorated functions or storage classes, metaclasses, and multiple storage assignments remain outside this inference. The existing ordinary argument protocol is retained: #898 covers the separate parameter/default/staticmethod changes, #900 covers lambda source values, and #901 covers external class data writes. This change does not require those PRs or introduce a general descriptor model.

Validation

  • All 34 regression tests pass on Python 3.12 and 3.14, including actual refactoring and execution, imported keyword renames, and bound callable values after normal analysis and project reopening.
  • Full suite: Python 3.12 — 2183 passed, 12 skipped, 5 xfailed; Python 3.14 — 2187 passed, 8 skipped, 5 xfailed. Both collect 2200 tests with no failures or errors.
  • Black, isort, and the repository's pre-commit checks pass for the changed files. I reviewed the function, class-slot, and parameter identities separately from the inferred access value.
  • The separate staticmethod/default inference failures and standalone instance-only attribute lookup limitation remain outside this fix; I retain their original diagnostics.

Checklist

  • I have added tests that prove the fix is effective.
  • I have updated CHANGELOG.md.
  • I have updated the library documentation.

Retain the receiver in direct and saved class-slot access values without
changing the original function's lexical parent or source bindings.
Preserve inherited and imported functions and direct instance overrides.

Record canonical calls during analysis and persist bound callable values.
Add execution and source-identity regressions for these consumers.
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.99571% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.44%. Comparing base (22f5e50) to head (0054264).

Files with missing lines Patch % Lines
rope/base/classfunctions.py 84.44% 14 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #904      +/-   ##
==========================================
+ Coverage   95.34%   95.44%   +0.10%     
==========================================
  Files         134      138       +4     
  Lines       26762    27224     +462     
==========================================
+ Hits        25516    25985     +469     
+ Misses       1246     1239       -7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant