diff --git a/CHANGELOG.md b/CHANGELOG.md index d2b64528e..a8bddc012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - #391, #396 Extract method similar no longer replace the left-hand side of assignment (@climbus) - #303 Fix inlining into f-string containing quote characters (@lieryan) - Added scopes for comprehension expressions as part of #293 (@climbus) +- Added support for checking scopes by offset as part of #293 (@climbus) - #423 Fix `AttributeError: '_ExpressionVisitor' object has no attribute 'defineds'` (@lieryan) - #398, #104 Fix parsing of nested `with` statement/context manager (@climbus) - #391, #376 Fix improper replacement when extracting attribute access expression with `similar=True` (@climbus) diff --git a/rope/base/pyscopes.py b/rope/base/pyscopes.py index 83c644743..387f53648 100644 --- a/rope/base/pyscopes.py +++ b/rope/base/pyscopes.py @@ -2,6 +2,7 @@ import rope.base.codeanalyze import rope.base.pynames from rope.base import ast, exceptions, utils +from rope.refactor import patchedast class Scope(object): @@ -100,6 +101,20 @@ def get_logical_end(self): def get_kind(self): pass + @utils.saveit + def get_region(self): + node = patchedast.patch_ast( + self.pyobject.get_ast(), self.pyobject.get_module().source_code + ) + region = patchedast.node_region(node) + return region + + def in_region(self, offset): + "Checks if offset is in scope region" + + region = self.get_region() + return region[0] < offset < region[1] + class GlobalScope(Scope): def __init__(self, pycore, module): @@ -289,8 +304,14 @@ def _is_empty_line(self, lineno): def _get_body_indents(self, scope): return self.get_indents(scope.get_body_start()) - def get_holding_scope_for_offset(self, scope, offset): - return self.get_holding_scope(scope, self.lines.get_line_number(offset)) + @staticmethod + def get_holding_scope_for_offset(scope, offset): + for inner_scope in scope.get_scopes(): + if inner_scope.in_region(offset): + return _HoldingScopeFinder.get_holding_scope_for_offset( + inner_scope, offset + ) + return scope def find_scope_end(self, scope): if not scope.parent: diff --git a/ropetest/pyscopestest.py b/ropetest/pyscopestest.py index 2e76ad287..0003be260 100644 --- a/ropetest/pyscopestest.py +++ b/ropetest/pyscopestest.py @@ -310,6 +310,24 @@ def test_get_inner_scope_for_staticmethods(self): f_in_c = c_scope.get_scopes()[0] self.assertEqual(f_in_c, scope.get_inner_scope_for_line(4)) + def test_get_scope_for_offset_for_comprehension(self): + scope = libutils.get_string_scope(self.project, "a = [i for i in range(10)]\n") + c_scope = scope.get_scopes()[0] + self.assertEqual(c_scope, scope.get_inner_scope_for_offset(10)) + self.assertEqual(scope, scope.get_inner_scope_for_offset(1)) + + def test_get_scope_for_offset_for_in_nested_comprehension(self): + scope = libutils.get_string_scope(self.project, "[i for i in [j for j in k]]\n") + c_scope = scope.get_scopes()[0] + self.assertEqual(c_scope, scope.get_inner_scope_for_offset(5)) + inner_scope = c_scope.get_scopes()[0] + self.assertEqual(inner_scope, scope.get_inner_scope_for_offset(15)) + + def test_get_scope_for_offset_for_scope_with_indent(self): + scope = libutils.get_string_scope(self.project, "def f(a):\n" " print(a)\n") + inner_scope = scope.get_scopes()[0] + self.assertEqual(inner_scope, scope.get_inner_scope_for_offset(10)) + def test_getting_overwritten_scopes(self): scope = libutils.get_string_scope( self.project, "def f():\n pass\ndef f():\n pass\n"