Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# **Upcoming release**

- Preserve receivers and source identities for ordinary functions stored on classes (@yangfan-yf-yf)
- #895 patchedast Starred and keyword now consumes their syntactically expected and ** (@lieryan)
- #896 patchedast cleanup and refactoring (@lieryan)

Expand Down
14 changes: 14 additions & 0 deletions docs/library.rst
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,20 @@ Also ``Pycore.analyze_module()`` collects object information for a
module. The collected information can be used to enhance rope's
static object inference.

For an undecorated module function stored by a single class-body name
assignment, direct and saved instance accesses retain the receiver. Inherited
slots and imported functions keep the original function, class slot, and
parameter definitions, so field and keyword renames use those definitions.
Call analysis and saved object records also preserve the bound callable and
receiver across closing and reopening a project.

This inference uses the existing ordinary argument protocol and requires a
positional parameter for the receiver, without positional-only or keyword-only
parameters. Recognized function writes to an instance, including aliases,
remain unbound. Override checks cover the storage, receiver, and access
modules; arbitrary writes in other modules, decorated storage classes,
metaclasses, and multiple storage assignments are outside this inference.


`rope.base.taskhandle.TaskHandle`
=================================
Expand Down
134 changes: 134 additions & 0 deletions rope/base/classfunctions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Bound access values for ordinary functions stored in a class body."""

from rope.base import arguments, ast, pynames, pyobjects


class BoundFunction(pyobjects.AbstractFunction):
def __init__(self, function, receiver):
super().__init__()
self.function = function
self.receiver = receiver

def get_name(self):
return self.function.get_name()

def get_doc(self):
return self.function.get_doc()

def get_param_names(self, special_args=True):
return self.function.get_param_names(special_args)

def bind_arguments(self, args):
return arguments.MixedArguments(self.receiver, args, None)

def get_returned_object(self, args):
return self.function.get_returned_object(self.bind_arguments(args))


class BoundAccessName(pynames.PyName):
def __init__(self, canonical, function, receiver):
self.canonical = canonical
self.value = BoundFunction(function, receiver)

def get_object(self):
return self.value

def get_definition_location(self):
return self.canonical.get_definition_location()

def __eq__(self, other):
if isinstance(other, BoundAccessName):
other = other.canonical
return self.canonical == other

def __hash__(self):
return hash(self.canonical)


def canonical_function(value):
if isinstance(value, BoundFunction):
return value.function
return value


def instance_access(primary, attribute, pyname, access_scope):
if not isinstance(pyname, pynames.AssignedName):
return pyname
instance = primary.get_object()
if isinstance(instance, pyobjects.AbstractClass):
return pyname
owner = instance.get_type()
if not isinstance(owner, pyobjects.PyClass):
return pyname
function = pyname.get_object()
if not isinstance(function, pyobjects.PyFunction):
return pyname
if not isinstance(function.parent, pyobjects.PyModule):
return pyname
node = function.get_ast()
if (
not node.args.args
or node.args.posonlyargs
or node.args.kwonlyargs
or node.decorator_list
or getattr(node, "type_params", ())
):
return pyname
assignments = pyname.assignments
if len(assignments) != 1 or not isinstance(assignments[0].ast_node, ast.Name):
return pyname
assignment = assignments[0]
if assignment.levels or assignment.evaluation or assignment.assign_type:
return pyname
module, _ = pyname.get_definition_location()
if module is None:
return pyname
storage_scope = module.get_scope().get_inner_scope_for_line(assignment.get_lineno())
if storage_scope.get_kind() != "Class":
return pyname
storage = storage_scope.pyobject
storage_node = storage.get_ast()
if storage_node.decorator_list or storage_node.keywords:
return pyname
if not any(
isinstance(statement, ast.Assign)
and len(statement.targets) == 1
and isinstance(statement.targets[0], ast.Name)
and statement.targets[0].id == attribute
and statement.value is assignment.ast_node
for statement in storage_node.body
):
return pyname
modules = (module, owner.get_module(), access_scope.pyobject.get_module())
for current in dict.fromkeys(modules):
if _has_override(current, primary, owner, storage, attribute):
return pyname
return BoundAccessName(pyname, function, primary)


def _has_override(module, primary, owner, storage, attribute):
# Instance dictionary functions remain ordinary unbound function values.
from rope.base import evaluate

receiver = primary.get_object()
for target in ast.walk(module.get_ast()):
if (
isinstance(target, ast.Attribute)
and target.attr == attribute
and isinstance(target.ctx, (ast.Store, ast.Del))
):
if not isinstance(target.value, ast.Name):
return True
scope = module.get_scope().get_inner_scope_for_line(target.lineno)
target_name = evaluate.eval_node(scope, target.value)
if target_name == primary:
return True
if target_name is not None:
target_object = target_name.get_object()
if (
target_object is receiver
or target_object is owner
or target_object is storage
):
return True
return False
7 changes: 5 additions & 2 deletions rope/base/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rope.base import (
arguments,
ast,
classfunctions,
exceptions,
nameanalyze,
pyobjects,
Expand Down Expand Up @@ -137,7 +138,7 @@ def get_enclosing_function(self, offset):
except BadIdentifierError:
function_pyname = None
if function_pyname is not None:
pyobject = function_pyname.get_object()
pyobject = classfunctions.canonical_function(function_pyname.get_object())
if isinstance(pyobject, pyobjects.AbstractFunction):
return pyobject
elif (
Expand Down Expand Up @@ -173,7 +174,9 @@ def _Attribute(self, node):
self.old_result = pyname
if pyname.get_object() != rope.base.pyobjects.get_unknown():
try:
self.result = pyname.get_object()[node.attr]
self.result = classfunctions.instance_access(
pyname, node.attr, pyname.get_object()[node.attr], self.scope
)
except exceptions.AttributeNotFoundError:
self.result = None

Expand Down
5 changes: 4 additions & 1 deletion rope/base/oi/soa.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import rope.base.ast
import rope.base.oi.soi
import rope.base.pynames
from rope.base import arguments, evaluate, nameanalyze, pyobjects
from rope.base import arguments, classfunctions, evaluate, nameanalyze, pyobjects


def analyze_module(pycore, pymodule, should_analyze, search_subscopes, followed_calls):
Expand Down Expand Up @@ -75,6 +75,9 @@ def _args_with_self(self, primary, self_pyname, pyfunction, node):
return arguments.MixedArguments(self_pyname, base_args, self.scope)

def _call(self, pyfunction, args):
if isinstance(pyfunction, classfunctions.BoundFunction):
args = pyfunction.bind_arguments(args)
pyfunction = pyfunction.function
if isinstance(pyfunction, pyobjects.PyFunction):
if self.follow is not None:
before = self._parameter_objects(pyfunction)
Expand Down
15 changes: 14 additions & 1 deletion rope/base/oi/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re

import rope.base.builtins # Use full qualification for clarity.
from rope.base import exceptions
from rope.base import classfunctions, exceptions, pynames, pyobjects


class PyObjectToTextual:
Expand Down Expand Up @@ -43,6 +43,13 @@ def PyObject_to_textual(self, pyobject):
def PyFunction_to_textual(self, pyobject):
return self._defined_to_textual(pyobject)

def BoundFunction_to_textual(self, pyobject):
return (
"bound_function",
self.transform(pyobject.function),
self.transform(pyobject.receiver.get_object()),
)

def PyClass_to_textual(self, pyobject):
return self._defined_to_textual(pyobject)

Expand Down Expand Up @@ -206,6 +213,12 @@ def instance_to_pyobject(self, textual):
if type is not None:
return rope.base.pyobjects.PyObject(type)

def bound_function_to_pyobject(self, textual):
function = self.transform(textual[1])
receiver = self.transform(textual[2])
if isinstance(function, pyobjects.PyFunction) and receiver is not None:
return classfunctions.BoundFunction(function, pynames.UnboundName(receiver))

def _get_pymodule(self, path):
resource = self.path_to_resource(path)
if resource is not None:
Expand Down
Loading
Loading