Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-rope for openSUSE:Factory 
checked in at 2021-04-19 21:05:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-rope (Old)
 and      /work/SRC/openSUSE:Factory/.python-rope.new.12324 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-rope"

Mon Apr 19 21:05:50 2021 rev:24 rq:886499 version:0.19.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-rope/python-rope.changes  2021-04-17 
00:01:52.825604976 +0200
+++ /work/SRC/openSUSE:Factory/.python-rope.new.12324/python-rope.changes       
2021-04-19 21:06:08.084041550 +0200
@@ -1,0 +2,33 @@
+Sun Apr 18 20:56:15 UTC 2021 - Matej Cepl <mc...@suse.com>
+
+- Update to 0.19.0:
+  - fixes #337
+  - Fix AttributeError lineno
+  - Python 3.9 ast changes
+  - create_generate with goal_resource param
+  - Fix relative import offset calculation
+  - Fix missinge lineno attribute for AssignedName ast node
+  - Added _NamedExpr into `patchedast.py`
+  - Add support for the walrus operator.
+  - fix test case name for `test_ann_assign_node_without_target`
+  - Returned _AnnAssign and checked for support assignment without value
+  - fixed version restriction in tests for NamedExpr
+  - Removed AnnAssign, added NeamedExpr, testa are made
+  - Added _AnnAsign into `patchedast.py`
+  - Extract augmented assignment
+  - Fix handling of dict rename in Python 2.x
+  - Improve handling of generalized dict unpacking during dict rename
+  - Add expected failure test for comprehension variable scopes
+  - Implement basic scoping and rename for set and dict comprehension
+  - Visit subexpressions of comprehensions to collect names for scopes
+  - Implement rename of inline assignment expression
+  - Implement basic scoping and renaming of list and generator
+    comprehension loop variables
+  - Implement f-string extract refactoring
+  - Refactor consume_joined_string and also fix missing
+    ast.JoinedStr/FormattedValue in older python
+  - Fix some f-string corner cases
+  - Implement PEP-448 generalized dict-unpacking
+- Removed upstreamed rope-pr333-py39.patch.
+
+-------------------------------------------------------------------

Old:
----
  rope-0.18.0.tar.gz
  rope-pr333-py39.patch

New:
----
  rope-0.19.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-rope.spec ++++++
--- /var/tmp/diff_new_pack.SDAZjv/_old  2021-04-19 21:06:08.580042293 +0200
+++ /var/tmp/diff_new_pack.SDAZjv/_new  2021-04-19 21:06:08.584042299 +0200
@@ -19,15 +19,13 @@
 %define upname rope
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-rope
-Version:        0.18.0
+Version:        0.19.0
 Release:        0
 Summary:        A python refactoring library
 License:        LGPL-3.0-or-later
 Group:          Development/Languages/Python
 URL:            https://github.com/python-rope/rope
 Source:         
https://files.pythonhosted.org/packages/source/r/rope/rope-%{version}.tar.gz
-# PATCH-FIX-UPSTREAM rope-pr333-py39.patch gh#python-rope/rope#333
-Patch1:         rope-pr333-py39.patch
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  fdupes

++++++ rope-0.18.0.tar.gz -> rope-0.19.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/PKG-INFO new/rope-0.19.0/PKG-INFO
--- old/rope-0.18.0/PKG-INFO    2020-10-07 18:08:40.468835800 +0200
+++ new/rope-0.19.0/PKG-INFO    2021-04-18 22:17:27.277263200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: rope
-Version: 0.18.0
+Version: 0.19.0
 Summary: a python refactoring library...
 Home-page: https://github.com/python-rope/rope
 Author: Ali Gholami Rudi
@@ -51,5 +51,6 @@
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Software Development
 Provides-Extra: dev
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/__init__.py 
new/rope-0.19.0/rope/__init__.py
--- old/rope-0.18.0/rope/__init__.py    2020-10-07 18:08:26.000000000 +0200
+++ new/rope-0.19.0/rope/__init__.py    2021-04-18 22:17:05.000000000 +0200
@@ -1,9 +1,9 @@
 """rope, a python refactoring library"""
 
 INFO = __doc__
-VERSION = '0.18.0'
+VERSION = '0.19.0'
 COPYRIGHT = """\
-Copyright (C) 2019-2020 Matej Cepl
+Copyright (C) 2019-2021 Matej Cepl
 Copyright (C) 2015-2018 Nicholas Smith
 Copyright (C) 2014-2015 Matej Cepl
 Copyright (C) 2006-2012 Ali Gholami Rudi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/base/codeanalyze.py 
new/rope-0.19.0/rope/base/codeanalyze.py
--- old/rope-0.18.0/rope/base/codeanalyze.py    2020-10-06 19:52:52.000000000 
+0200
+++ new/rope-0.19.0/rope/base/codeanalyze.py    2021-02-03 12:02:18.000000000 
+0100
@@ -143,7 +143,7 @@
             if token in ["'''", '"""', "'", '"']:
                 if not self.in_string:
                     self.in_string = token
-                elif self.in_string == token:
+                elif self.in_string == token or (self.in_string in ['"', "'"] 
and token == 3*self.in_string):
                     self.in_string = ''
             if self.in_string:
                 continue
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/base/evaluate.py 
new/rope-0.19.0/rope/base/evaluate.py
--- old/rope-0.18.0/rope/base/evaluate.py       2020-04-27 11:29:13.000000000 
+0200
+++ new/rope-0.19.0/rope/base/evaluate.py       2021-04-18 22:14:45.000000000 
+0200
@@ -1,3 +1,5 @@
+from operator import itemgetter
+
 import rope.base.builtins
 import rope.base.pynames
 import rope.base.pyobjects
@@ -232,9 +234,12 @@
     def _Dict(self, node):
         keys = None
         values = None
-        if node.keys:
-            keys = self._get_object_for_node(node.keys[0])
-            values = self._get_object_for_node(node.values[0])
+        if node.keys and node.keys[0]:
+            keys, values = next(iter(filter(itemgetter(0), zip(node.keys, 
node.values))), (None, None))
+            if keys:
+                keys = self._get_object_for_node(keys)
+            if values:
+                values = self._get_object_for_node(values)
         self.result = rope.base.pynames.UnboundName(
             pyobject=rope.base.builtins.get_dict(keys, values))
 
@@ -302,6 +307,9 @@
         elif isinstance(node.slice, ast.Slice):
             self._call_function(node.value, '__getitem__',
                                 [node.slice])
+        elif isinstance(node.slice, ast.expr):
+            self._call_function(node.value, '__getitem__',
+                                [node.value])
 
     def _Slice(self, node):
         self.result = self._get_builtin_name('slice')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/base/oi/soa.py 
new/rope-0.19.0/rope/base/oi/soa.py
--- old/rope-0.18.0/rope/base/oi/soa.py 2020-01-30 17:02:09.000000000 +0100
+++ new/rope-0.19.0/rope/base/oi/soa.py 2021-04-18 22:14:45.000000000 +0200
@@ -126,7 +126,7 @@
         for subscript, levels in nodes:
             instance = evaluate.eval_node(self.scope, subscript.value)
             args_pynames = [evaluate.eval_node(self.scope,
-                                               subscript.slice.value)]
+                                               subscript.slice)]
             value = rope.base.oi.soi._infer_assignment(
                 rope.base.pynames.AssignmentValue(node.value, levels,
                                                   type_hint=type_hint),
@@ -149,5 +149,5 @@
 
     def _added(self, node, levels):
         if isinstance(node, rope.base.ast.Subscript) and \
-           isinstance(node.slice, rope.base.ast.Index):
+           isinstance(node.slice, (rope.base.ast.Index, rope.base.ast.expr)):
             self.nodes.append((node, levels))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/base/pynamesdef.py 
new/rope-0.19.0/rope/base/pynamesdef.py
--- old/rope-0.18.0/rope/base/pynamesdef.py     2016-02-17 18:18:23.000000000 
+0100
+++ new/rope-0.19.0/rope/base/pynamesdef.py     2021-04-18 22:14:45.000000000 
+0200
@@ -24,7 +24,10 @@
     def get_definition_location(self):
         """Returns a (module, lineno) tuple"""
         if self.lineno is None and self.assignments:
-            self.lineno = self.assignments[0].get_lineno()
+            try:
+                self.lineno = self.assignments[0].get_lineno()
+            except AttributeError:
+                pass
         return (self.module, self.lineno)
 
     def invalidate(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/base/pyobjectsdef.py 
new/rope-0.19.0/rope/base/pyobjectsdef.py
--- old/rope-0.18.0/rope/base/pyobjectsdef.py   2020-10-06 19:52:52.000000000 
+0200
+++ new/rope-0.19.0/rope/base/pyobjectsdef.py   2021-02-03 12:02:18.000000000 
+0100
@@ -321,6 +321,37 @@
         pass
 
 
+class _ExpressionVisitor(object):
+    def __init__(self, scope_visitor):
+        self.scope_visitor = scope_visitor
+
+    def _assigned(self, name, assignment=None):
+        self.scope_visitor._assigned(name, assignment)
+
+    def _GeneratorExp(self, node):
+        for child in ['elt', 'key', 'value']:
+            if hasattr(node, child):
+                ast.walk(getattr(node, child), self)
+        for comp in node.generators:
+            ast.walk(comp.target, _AssignVisitor(self))
+            ast.walk(comp, self)
+            for if_ in comp.ifs:
+                ast.walk(if_, self)
+
+    def _ListComp(self, node):
+        self._GeneratorExp(node)
+
+    def _SetComp(self, node):
+        self._GeneratorExp(node)
+
+    def _DictComp(self, node):
+        self._GeneratorExp(node)
+
+    def _NamedExpr(self, node):
+        ast.walk(node.target, _AssignVisitor(self))
+        ast.walk(node.value, self)
+
+
 class _AssignVisitor(object):
 
     def __init__(self, scope_visitor):
@@ -331,6 +362,7 @@
         self.assigned_ast = node.value
         for child_node in node.targets:
             ast.walk(child_node, self)
+        ast.walk(node.value, _ExpressionVisitor(self.scope_visitor))
 
     def _assigned(self, name, assignment=None):
         self.scope_visitor._assigned(name, assignment)
@@ -359,7 +391,7 @@
         pass
 
 
-class _ScopeVisitor(object):
+class _ScopeVisitor(_ExpressionVisitor):
 
     def __init__(self, pycore, owner_object):
         self.pycore = pycore
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/base/worder.py 
new/rope-0.19.0/rope/base/worder.py
--- old/rope-0.18.0/rope/base/worder.py 2020-10-07 17:19:26.000000000 +0200
+++ new/rope-0.19.0/rope/base/worder.py 2021-04-18 22:14:45.000000000 +0200
@@ -225,6 +225,14 @@
             prev = self._find_last_non_space_char(offset - 1)
             if offset <= 0 or self.code[prev] != '.':
                 break
+
+            # Check if relative import
+            # XXX: Looks like a hack...
+            prev_word_end = self._find_last_non_space_char(prev - 1)
+            if self.code[prev_word_end-3:prev_word_end+1] == "from":
+                offset = prev
+                break
+
             offset = self._find_primary_without_dot_start(prev - 1)
             if not self._is_id_char(offset):
                 break
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/contrib/generate.py 
new/rope-0.19.0/rope/contrib/generate.py
--- old/rope-0.18.0/rope/contrib/generate.py    2020-01-30 17:02:09.000000000 
+0100
+++ new/rope-0.19.0/rope/contrib/generate.py    2021-04-18 22:14:45.000000000 
+0200
@@ -5,7 +5,7 @@
 from rope.refactor import sourceutils, importutils, functionutils, suites
 
 
-def create_generate(kind, project, resource, offset):
+def create_generate(kind, project, resource, offset, goal_resource=None):
     """A factory for creating `Generate` objects
 
     `kind` can be 'variable', 'function', 'class', 'module' or
@@ -13,7 +13,7 @@
 
     """
     generate = eval('Generate' + kind.title())
-    return generate(project, resource, offset)
+    return generate(project, resource, offset, goal_resource=goal_resource)
 
 
 def create_module(project, name, sourcefolder=None):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/refactor/extract.py 
new/rope-0.19.0/rope/refactor/extract.py
--- old/rope-0.18.0/rope/refactor/extract.py    2020-01-30 17:02:09.000000000 
+0100
+++ new/rope-0.19.0/rope/refactor/extract.py    2021-02-03 12:02:18.000000000 
+0100
@@ -547,7 +547,7 @@
         # if not make_global, do not pass any global names; they are
         # all visible.
         if self.info.global_ and not self.info.make_global:
-            return ()
+            return list(self.info_collector.read & 
self.info_collector.postread & self.info_collector.written)
         if not self.info.one_line:
             result = (self.info_collector.prewritten &
                       self.info_collector.read)
@@ -662,6 +662,11 @@
         for child in node.targets:
             ast.walk(child, self)
 
+    def _AugAssign(self, node):
+        ast.walk(node.value, self)
+        self._read_variable(node.target.id, node.target.lineno)
+        self._written_variable(node.target.id, node.target.lineno)
+
     def _ClassDef(self, node):
         self._written_variable(node.name, node.lineno)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope/refactor/patchedast.py 
new/rope-0.19.0/rope/refactor/patchedast.py
--- old/rope-0.18.0/rope/refactor/patchedast.py 2020-10-06 19:52:52.000000000 
+0200
+++ new/rope-0.19.0/rope/refactor/patchedast.py 2021-04-18 22:15:06.000000000 
+0200
@@ -125,7 +125,10 @@
                     # semicolon in except
                     region = self.source.consume_except_as_or_semicolon()
                 else:
-                    region = self.source.consume(child)
+                    if hasattr(ast, 'JoinedStr') and isinstance(node, 
(ast.JoinedStr, ast.FormattedValue)):
+                        region = self.source.consume_joined_string(child)
+                    else:
+                        region = self.source.consume(child)
                 child = self.source[region[0]:region[1]]
                 token_start = region[0]
             if not first_token:
@@ -255,6 +258,13 @@
         children.extend(['=', node.value])
         self._handle(node, children)
 
+    def _AnnAssign(self, node):
+        children = [node.target, ':', node.annotation]
+        if node.value is not None:
+            children.append('=')
+            children.append(node.value)
+        self._handle(node, children)
+
     def _Repr(self, node):
         self._handle(node, ['`', node.value, '`'])
 
@@ -361,6 +371,46 @@
     def _Bytes(self, node):
         self._handle(node, [self.String])
 
+    def _JoinedStr(self, node):
+        def start_quote_char():
+            possible_quotes = [(self.source.source.find(q, start, end), q) for 
q in QUOTE_CHARS]
+            quote_pos, quote_char = min((pos, q) for pos, q in possible_quotes 
if pos != -1)
+            return self.source[start:quote_pos + len(quote_char)]
+
+        def end_quote_char():
+            possible_quotes = [(self.source.source.rfind(q, start, end), q) 
for q in reversed(QUOTE_CHARS)]
+            _, quote_pos, quote_char = max((len(q), pos, q) for pos, q in 
possible_quotes if pos != -1)
+            return self.source[end - len(quote_char):end]
+
+        QUOTE_CHARS = ['"""', "'''", '"', "'"]
+        offset = self.source.offset
+        start, end = self.source.consume_string(
+            end=self._find_next_statement_start(),
+        )
+        self.source.offset = offset
+
+        children = []
+        children.append(start_quote_char())
+        for part in node.values:
+            if isinstance(part, ast.FormattedValue):
+                children.append(part)
+        children.append(end_quote_char())
+        self._handle(node, children)
+
+    def _FormattedValue(self, node):
+        children = []
+        children.append('{')
+        children.append(node.value)
+        if node.format_spec:
+            children.append(':')
+            for val in node.format_spec.values:
+                if isinstance(val, ast.FormattedValue):
+                    children.append(val.value)
+                else:
+                    children.append(val.s)
+        children.append('}')
+        self._handle(node, children)
+
     def _Continue(self, node):
         self._handle(node, ['continue'])
 
@@ -369,7 +419,11 @@
         children.append('{')
         if node.keys:
             for index, (key, value) in enumerate(zip(node.keys, node.values)):
-                children.extend([key, ':', value])
+                if key is None:
+                    # PEP-448 dict unpacking: {a: b, **unpack}
+                    children.extend(['**', value])
+                else:
+                    children.extend([key, ':', value])
                 if index < len(node.keys) - 1:
                     children.append(',')
         children.append('}')
@@ -381,6 +435,10 @@
     def _Expr(self, node):
         self._handle(node, [node.value])
 
+    def _NamedExpr(self, node):
+        children = [node.target, ':=', node.value]
+        self._handle(node, children)
+
     def _Exec(self, node):
         children = []
         children.extend(['exec', node.body])
@@ -764,11 +822,11 @@
         self.source = source
         self.offset = 0
 
-    def consume(self, token):
+    def consume(self, token, skip_comment=True):
         try:
             while True:
                 new_offset = self.source.index(token, self.offset)
-                if self._good_token(token, new_offset):
+                if self._good_token(token, new_offset) or not skip_comment:
                     break
                 else:
                     self._skip_comment()
@@ -779,9 +837,16 @@
         self.offset = new_offset + len(token)
         return (new_offset, self.offset)
 
+    def consume_joined_string(self, token):
+        new_offset = self.source.index(token, self.offset)
+        self.offset = new_offset + len(token)
+        return (new_offset, self.offset)
+
     def consume_string(self, end=None):
         if _Source._string_pattern is None:
-            original = codeanalyze.get_string_pattern()
+            string_pattern = codeanalyze.get_string_pattern()
+            formatted_string_pattern = 
codeanalyze.get_formatted_string_pattern()
+            original = r'(?:%s)|(?:%s)' % (string_pattern, 
formatted_string_pattern)
             pattern = r'(%s)((\s|\\\n|#[^\n]*\n)*(%s))*' % \
                       (original, original)
             _Source._string_pattern = re.compile(pattern)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/rope.egg-info/PKG-INFO 
new/rope-0.19.0/rope.egg-info/PKG-INFO
--- old/rope-0.18.0/rope.egg-info/PKG-INFO      2020-10-07 18:08:40.000000000 
+0200
+++ new/rope-0.19.0/rope.egg-info/PKG-INFO      2021-04-18 22:17:27.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: rope
-Version: 0.18.0
+Version: 0.19.0
 Summary: a python refactoring library...
 Home-page: https://github.com/python-rope/rope
 Author: Ali Gholami Rudi
@@ -51,5 +51,6 @@
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Software Development
 Provides-Extra: dev
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/codeanalyzetest.py 
new/rope-0.19.0/ropetest/codeanalyzetest.py
--- old/rope-0.18.0/ropetest/codeanalyzetest.py 2020-10-07 17:19:26.000000000 
+0200
+++ new/rope-0.19.0/ropetest/codeanalyzetest.py 2021-04-18 22:14:45.000000000 
+0200
@@ -1,3 +1,4 @@
+from textwrap import dedent
 try:
     import unittest2 as unittest
 except ImportError:
@@ -156,6 +157,11 @@
         self.assertEqual('a_func  (  "(" ) .   an_attr',
                          self._find_primary(code, 26))
 
+    def test_relative_import(self):
+        code = "from .module import smt"
+        self.assertEqual('.module',
+                         self._find_primary(code, 5))
+
     def test_functions_on_ending_parens(self):
         code = 'A()'
         self.assertEqual('A()', self._find_primary(code, 2))
@@ -600,12 +606,12 @@
 
     def test_generating_line_starts2(self):
         code = 'a = 1\na = 2\n\na = \\ 3\n'
-        line_finder = LogicalLineFinder(SourceLinesAdapter(code))
+        line_finder = self._logical_finder(code)
         self.assertEqual([2, 4], list(line_finder.generate_starts(2)))
 
     def test_generating_line_starts3(self):
         code = 'a = 1\na = 2\n\na = \\ 3\n'
-        line_finder = LogicalLineFinder(SourceLinesAdapter(code))
+        line_finder = self._logical_finder(code)
         self.assertEqual([2], list(line_finder.generate_starts(2, 3)))
 
     def test_generating_line_starts_for_multi_line_statements(self):
@@ -619,6 +625,21 @@
         line_finder = self._logical_finder(code)
         self.assertEqual([4, 5], list(line_finder.generate_starts(4)))
 
+    def test_false_triple_quoted_string(self):
+        code = dedent("""\
+            def foo():
+                a = 0
+                p = 'foo'''
+
+            def bar():
+                a = 1
+                a += 1
+        """)
+        line_finder = self._logical_finder(code)
+        self.assertEqual([1, 2, 3, 5, 6, 7], 
list(line_finder.generate_starts()))
+        self.assertEqual((3, 3), line_finder.logical_line_in(3))
+        self.assertEqual([5, 6, 7], list(line_finder.generate_starts(4)))
+
 
 class TokenizerLogicalLineFinderTest(LogicalLineFinderTest):
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/contrib/codeassisttest.py 
new/rope-0.19.0/ropetest/contrib/codeassisttest.py
--- old/rope-0.18.0/ropetest/contrib/codeassisttest.py  2020-10-06 
19:52:52.000000000 +0200
+++ new/rope-0.19.0/ropetest/contrib/codeassisttest.py  2021-02-03 
12:02:18.000000000 +0100
@@ -1,6 +1,7 @@
 # coding: utf-8
 
 import os.path
+from textwrap import dedent
 try:
     import unittest2 as unittest
 except ImportError:
@@ -472,6 +473,19 @@
         result = get_definition_location(self.project, code, len(code) - 3)
         self.assertEqual((None, 3), result)
 
+    def test_get_definition_location_false_triple_quoted_string(self):
+        code = dedent('''\
+            def foo():
+                a = 0
+                p = "foo"""
+
+            def bar():
+                a = 1
+                a += 1
+        ''')
+        result = get_definition_location(self.project, code, code.index("a += 
1"))
+        self.assertEqual((None, 6), result)
+
     def test_code_assists_in_parens(self):
         code = 'def a_func(a_var):\n    pass\na_var = 10\na_func(a_'
         result = self._assist(code)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/contrib/generatetest.py 
new/rope-0.19.0/ropetest/contrib/generatetest.py
--- old/rope-0.18.0/ropetest/contrib/generatetest.py    2020-01-30 
17:02:09.000000000 +0100
+++ new/rope-0.19.0/ropetest/contrib/generatetest.py    2021-04-18 
22:14:45.000000000 +0200
@@ -284,6 +284,32 @@
             '    if 1:\n        g()\n',
             self.mod.read())
 
+    def test_create_generate_class_with_goal_resource(self):
+        code = 'c = C()\n'
+        self.mod.write(code)
+
+        result = generate.create_generate(
+            "class",
+            self.project,
+            self.mod,
+            code.index("C"),
+            goal_resource=self.mod2)
+
+        self.assertTrue(isinstance(result, generate.GenerateClass))
+        self.assertEqual(result.goal_resource, self.mod2)
+
+    def test_create_generate_class_without_goal_resource(self):
+        code = 'c = C()\n'
+        self.mod.write(code)
+
+        result = generate.create_generate(
+            "class",
+            self.project,
+            self.mod,
+            code.index("C"))
+
+        self.assertTrue(isinstance(result, generate.GenerateClass))
+        self.assertIsNone(result.goal_resource)
 
 if __name__ == '__main__':
     unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/pyscopestest.py 
new/rope-0.19.0/ropetest/pyscopestest.py
--- old/rope-0.18.0/ropetest/pyscopestest.py    2020-01-30 17:02:09.000000000 
+0100
+++ new/rope-0.19.0/ropetest/pyscopestest.py    2021-02-03 12:02:18.000000000 
+0100
@@ -44,6 +44,71 @@
                           scope.get_scopes()[0]['SampleClass'].
                           get_object().get_type())
 
+    def test_list_comprehension_scope_inside_assignment(self):
+        scope = libutils.get_string_scope(
+            self.project, 'a_var = [b_var + d_var for b_var, c_var in 
e_var]\n')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['a_var', 'b_var', 'c_var'],
+        )
+
+    def test_list_comprehension_scope(self):
+        scope = libutils.get_string_scope(
+            self.project, '[b_var + d_var for b_var, c_var in e_var]\n')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['b_var', 'c_var'],
+        )
+
+    def test_set_comprehension_scope(self):
+        scope = libutils.get_string_scope(
+            self.project, '{b_var + d_var for b_var, c_var in e_var}\n')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['b_var', 'c_var'],
+        )
+
+    def test_generator_comprehension_scope(self):
+        scope = libutils.get_string_scope(
+            self.project, '(b_var + d_var for b_var, c_var in e_var)\n')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['b_var', 'c_var'],
+        )
+
+    def test_dict_comprehension_scope(self):
+        scope = libutils.get_string_scope(
+            self.project, '{b_var: d_var for b_var, c_var in e_var}\n')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['b_var', 'c_var'],
+        )
+
+    @testutils.only_for_versions_higher('3.8')
+    def test_inline_assignment_in_comprehensions(self):
+        scope = libutils.get_string_scope(
+            self.project, '''[
+                (a_var := b_var + (f_var := g_var))
+                for b_var in [(j_var := i_var)
+                for i_var in c_var] if a_var + (h_var := d_var)
+            ]''')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['a_var', 'b_var', 'f_var', 'h_var', 'i_var', 'j_var'],
+        )
+
+    def test_nested_comprehension(self):
+        scope = libutils.get_string_scope(
+            self.project, '''[
+                b_var + d_var for b_var, c_var in [
+                    e_var for e_var in f_var
+                ]
+            ]\n''')
+        self.assertEqual(
+            list(sorted(scope.get_defined_names())),
+            ['b_var', 'c_var', 'e_var'],
+        )
+
     def test_simple_class_scope(self):
         scope = libutils.get_string_scope(
             self.project,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/refactor/extracttest.py 
new/rope-0.19.0/ropetest/refactor/extracttest.py
--- old/rope-0.18.0/ropetest/refactor/extracttest.py    2020-01-30 
17:02:09.000000000 +0100
+++ new/rope-0.19.0/ropetest/refactor/extracttest.py    2021-02-03 
12:02:18.000000000 +0100
@@ -1,3 +1,4 @@
+from textwrap import dedent
 try:
     import unittest2 as unittest
 except ImportError:
@@ -65,6 +66,26 @@
                    "def extracted():\n    print('one')\n\nprint('hey')\n"
         self.assertEqual(expected, refactored)
 
+    @testutils.only_for('3.5')
+    def test_extract_function_containing_dict_generalized_unpacking(self):
+        code = dedent('''\
+            def a_func(dict1):
+                dict2 = {}
+                a_var = {a: b, **dict1, **dict2}
+        ''')
+        start = code.index('{a')
+        end = code.index('2}') + len('2}')
+        refactored = self.do_extract_method(code, start, end, 'extracted')
+        expected = dedent('''\
+            def a_func(dict1):
+                dict2 = {}
+                a_var = extracted(dict1, dict2)
+
+            def extracted(dict1, dict2):
+                return {a: b, **dict1, **dict2}
+        ''')
+        self.assertEqual(expected, refactored)
+
     def test_simple_extract_function_with_parameter(self):
         code = "def a_func():\n    a_var = 10\n    print(a_var)\n"
         start, end = self._convert_line_range_to_offset(code, 3, 3)
@@ -409,6 +430,20 @@
         expected = 'new_var = 10 + 20\na_var = new_var\n'
         self.assertEqual(expected, refactored)
 
+    @testutils.only_for_versions_higher('3.6')
+    def test_extract_variable_f_string(self):
+        code = dedent('''\
+            foo(f"abc {a_var} def", 10)
+        ''')
+        start = code.index('f"')
+        end = code.index('def"') + 4
+        refactored = self.do_extract_variable(code, start, end, 'new_var')
+        expected = dedent('''\
+            new_var = f"abc {a_var} def"
+            foo(new_var, 10)
+        ''')
+        self.assertEqual(expected, refactored)
+
     def test_extract_variable_multiple_lines(self):
         code = 'a = 1\nb = 2\n'
         start = code.index('1')
@@ -544,6 +579,87 @@
         expected = '\n\ndef new_f():\n    print(1)\n\nnew_f()\n'
         self.assertEqual(expected, refactored)
 
+    @testutils.only_for_versions_higher('3.6')
+    def test_extract_method_f_string_extract_method(self):
+        code = dedent('''\
+            def func(a_var):
+                foo(f"abc {a_var}", 10)
+        ''')
+        start = code.index('f"')
+        end = code.index('}"') + 2
+        refactored = self.do_extract_method(code, start, end, 'new_func')
+        expected = dedent('''\
+            def func(a_var):
+                foo(new_func(a_var), 10)
+
+            def new_func(a_var):
+                return f"abc {a_var}"
+        ''')
+        self.assertEqual(expected, refactored)
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_extract_method_f_string_extract_method_complex_expression(self):
+        code = dedent('''\
+            def func(a_var):
+                b_var = int
+                c_var = 10
+                fill = 10
+                foo(f"abc {a_var + f'{b_var(a_var)}':{fill}16}" f"{c_var}", 10)
+        ''')
+        start = code.index('f"')
+        end = code.index('c_var}"') + 7
+        refactored = self.do_extract_method(code, start, end, 'new_func')
+        expected = dedent('''\
+            def func(a_var):
+                b_var = int
+                c_var = 10
+                fill = 10
+                foo(new_func(a_var, b_var, c_var, fill), 10)
+
+            def new_func(a_var, b_var, c_var, fill):
+                return f"abc {a_var + f'{b_var(a_var)}':{fill}16}" f"{c_var}"
+        ''')
+        self.assertEqual(expected, refactored)
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_extract_method_f_string_false_comment(self):
+        code = dedent('''\
+            def func(a_var):
+                foo(f"abc {a_var} # ", 10)
+        ''')
+        start = code.index('f"')
+        end = code.index('# "') + 3
+        refactored = self.do_extract_method(code, start, end, 'new_func')
+        expected = dedent('''\
+            def func(a_var):
+                foo(new_func(a_var), 10)
+
+            def new_func(a_var):
+                return f"abc {a_var} # "
+        ''')
+        self.assertEqual(expected, refactored)
+
+    @unittest.expectedFailure
+    @testutils.only_for_versions_higher('3.6')
+    def 
test_extract_method_f_string_false_format_value_in_regular_string(self):
+        code = dedent('''\
+            def func(a_var):
+                b_var = 1
+                foo(f"abc {a_var} " "{b_var}" f"{b_var} def", 10)
+        ''')
+        start = code.index('f"')
+        end = code.index('def"') + 4
+        refactored = self.do_extract_method(code, start, end, 'new_func')
+        expected = dedent('''\
+            def func(a_var):
+                b_var = 1
+                foo(new_func(a_var, b_var), 10)
+
+            def new_func(a_var, b_var):
+                return f"abc {a_var} " "{b_var}" f"{b_var} def"
+        ''')
+        self.assertEqual(expected, refactored)
+
     def test_variable_writes_in_the_same_line_as_variable_read(self):
         code = 'a = 1\na = 1 + a\n'
         start = code.index('\n') + 1
@@ -554,12 +670,60 @@
         self.assertEqual(expected, refactored)
 
     def test_variable_writes_in_the_same_line_as_variable_read2(self):
-        code = 'a = 1\na += 1\n'
+        code = dedent('''\
+            a = 1
+            a += 1
+        ''')
         start = code.index('\n') + 1
         end = len(code)
         refactored = self.do_extract_method(code, start, end, 'new_f',
                                             global_=True)
-        expected = 'a = 1\n\ndef new_f():\n    a += 1\n\nnew_f()\n'
+        expected = dedent('''\
+            a = 1
+
+            def new_f(a):
+                a += 1
+
+            new_f(a)
+        ''')
+        self.assertEqual(expected, refactored)
+
+    def test_variable_writes_in_the_same_line_as_variable_read3(self):
+        code = dedent('''\
+            a = 1
+            a += 1
+            print(a)
+        ''')
+        start, end = self._convert_line_range_to_offset(code, 2, 2)
+        refactored = self.do_extract_method(code, start, end, 'new_f')
+        expected = dedent('''\
+            a = 1
+
+            def new_f(a):
+                a += 1
+                return a
+
+            a = new_f(a)
+            print(a)
+        ''')
+        self.assertEqual(expected, refactored)
+
+    def test_variable_writes_only(self):
+        code = dedent('''\
+            i = 1
+            print(i)
+        ''')
+        start, end = self._convert_line_range_to_offset(code, 1, 1)
+        refactored = self.do_extract_method(code, start, end, 'new_f')
+        expected = dedent('''\
+
+            def new_f():
+                i = 1
+                return i
+
+            i = new_f()
+            print(i)
+        ''')
         self.assertEqual(expected, refactored)
 
     def test_variable_and_similar_expressions(self):
@@ -1022,5 +1186,50 @@
         end = len(code) - 1
         with self.assertRaises(rope.base.exceptions.RefactoringError):
             self.do_extract_method(code, start, end, 'new_func')
+
+    def test_extract_function_with_inline_assignment_in_method(self):
+        code = dedent('''\
+            def foo():
+                i = 1
+                i += 1
+                print(i)
+        ''')
+        start, end = self._convert_line_range_to_offset(code, 3, 3)
+        refactored = self.do_extract_method(code, start, end, 'new_func')
+        expected = dedent('''\
+            def foo():
+                i = 1
+                i = new_func(i)
+                print(i)
+
+            def new_func(i):
+                i += 1
+                return i
+        ''')
+        self.assertEqual(expected, refactored)
+
+    @testutils.only_for_versions_higher('3.8')
+    def test_extract_function_with_inline_assignment_in_condition(self):
+        code = dedent('''\
+            def foo(a):
+                if i := a == 5:
+                    i += 1
+                print(i)
+        ''')
+        start, end = self._convert_line_range_to_offset(code, 2, 3)
+        refactored = self.do_extract_method(code, start, end, 'new_func')
+        expected = dedent('''\
+            def foo(a):
+                i = new_func(a)
+                print(i)
+
+            def new_func(a):
+                if i := a == 5:
+                    i += 1
+                return i
+        ''')
+        self.assertEqual(expected, refactored)
+
+
 if __name__ == '__main__':
     unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/refactor/patchedasttest.py 
new/rope-0.19.0/ropetest/refactor/patchedasttest.py
--- old/rope-0.18.0/ropetest/refactor/patchedasttest.py 2020-10-06 
19:52:52.000000000 +0200
+++ new/rope-0.19.0/ropetest/refactor/patchedasttest.py 2021-04-18 
22:14:45.000000000 +0200
@@ -120,6 +120,26 @@
         checker.check_children(
             'Assign', ['Name', ' ', '=', ' ', 'Num'])
 
+    @testutils.only_for_versions_higher('3.6')
+    def test_ann_assign_node_without_target(self):
+        source = 'a: List[int]\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        start = source.index('a')  # noqa
+        checker.check_region('AnnAssign', 0, len(source) - 1)
+        checker.check_children(
+            'AnnAssign', ['Name', '', ':', ' ', 'Subscript'])
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_ann_assign_node_with_target(self):
+        source = 'a: int = 10\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        start = source.index('a')  # noqa
+        checker.check_region('AnnAssign', 0, len(source) - 1)
+        checker.check_children(
+            'AnnAssign', ['Name', '', ':', ' ', 'Name', ' ', '=', ' ', 'Num'])
+
     def test_add_node(self):
         source = '1 + 2\n'
         ast_frag = patchedast.get_patched_ast(source, True)
@@ -227,6 +247,63 @@
         checker = _ResultChecker(self, ast_frag)
         checker.check_children('Module', ['', 'Expr', '\n', 'Expr', '\n'])
 
+    def test_handling_raw_strings(self):
+        source = 'r"abc"\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_children(
+            'Str', ['r"abc"'])
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_handling_format_strings_basic(self):
+        source = '1 + f"abc{a}"\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_children(
+            'JoinedStr', ['f"', 'abc', 'FormattedValue', '', '"'])
+        checker.check_children(
+            'FormattedValue', ['{', '', 'Name', '', '}'])
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_handling_format_strings_with_implicit_join(self):
+        source = '''"1" + rf'abc{a}' f"""xxx{b} """\n'''
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_children(
+            'JoinedStr', ["rf'", 'abc', 'FormattedValue', '\' f"""xxx', 
'FormattedValue', ' ', '"""'])
+        checker.check_children(
+            'FormattedValue', ['{', '', 'Name', '', '}'])
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_handling_format_strings_with_format_spec(self):
+        source = 'f"abc{a:01}"\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_children(
+            'JoinedStr', ['f"', 'abc', 'FormattedValue', '', '"'])
+        checker.check_children(
+            'FormattedValue', ['{', '', 'Name', '', ':', '', '01', '', '}'])
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_handling_format_strings_with_inner_format_spec(self):
+        source = 'f"abc{a:{length}01}"\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_children(
+            'JoinedStr', ['f"', 'abc', 'FormattedValue', '', '"'])
+        checker.check_children(
+            'FormattedValue', ['{', '', 'Name', '', ':', '{', 'Name', '}', 
'01', '', '}'])
+
+    @testutils.only_for_versions_higher('3.6')
+    def test_handling_format_strings_with_expression(self):
+        source = 'f"abc{a + b}"\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_children(
+            'JoinedStr', ['f"', 'abc', 'FormattedValue', '', '"'])
+        checker.check_children(
+            'FormattedValue', ['{', '', 'BinOp', '', '}'])
+
     @testutils.only_for_versions_lower('3')
     def test_long_integer_literals(self):
         source = "0x1L + a"
@@ -499,6 +576,16 @@
             'Dict', ['{', '', 'Num', '', ':', ' ', 'Num', '', ',',
                      ' ', 'Num', '', ':', ' ', 'Num', '', '}'])
 
+    @testutils.only_for('3.5')
+    def test_dict_node_with_unpacking(self):
+        source = '{**dict1, **dict2}\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        checker.check_region('Dict', 0, len(source) - 1)
+        checker.check_children(
+            'Dict', ['{', '', '**', '', 'Name', '', ',',
+                     ' ', '**', '', 'Name', '', '}'])
+
     def test_div_node(self):
         source = '1 / 2\n'
         ast_frag = patchedast.get_patched_ast(source, True)
@@ -534,6 +621,15 @@
                     ':', '\n    ', 'Pass', '\n',
                     'else', '', ':', '\n    ', 'Pass'])
 
+    @testutils.only_for_versions_higher('3.8')
+    def test_named_expr_node(self):
+        source = 'if a := 10 == 10:\n    pass\n'
+        ast_frag = patchedast.get_patched_ast(source, True)
+        checker = _ResultChecker(self, ast_frag)
+        start = source.index('a')
+        checker.check_region('NamedExpr', start, start + 13)
+        checker.check_children('NamedExpr', ['Name', ' ', ':=', ' ', 
'Compare'])
+
     def test_normal_from_node(self):
         source = 'from x import y\n'
         ast_frag = patchedast.get_patched_ast(source, True)
@@ -742,8 +838,12 @@
         source = 'x = xs[0,:]\n'
         ast_frag = patchedast.get_patched_ast(source, True)
         checker = _ResultChecker(self, ast_frag)
-        checker.check_region('ExtSlice', 7, len(source) - 2)
-        checker.check_children('ExtSlice', ['Index', '', ',', '', 'Slice'])
+        if sys.version_info >= (3, 9):
+            checker.check_region('Tuple', 7, len(source) - 2)
+            checker.check_children('Tuple', ['Num', '', ',', '', 'Slice'])
+        else:
+            checker.check_region('ExtSlice', 7, len(source) - 2)
+            checker.check_children('ExtSlice', ['Index', '', ',', '', 'Slice'])
 
     def test_simple_module_node(self):
         source = 'pass\n'
@@ -837,9 +937,13 @@
         source = 'a[1]\n'
         ast_frag = patchedast.get_patched_ast(source, True)
         checker = _ResultChecker(self, ast_frag)
-        checker.check_children(
-            'Subscript', ['Name', '', '[', '', 'Index', '', ']'])
-        checker.check_children('Index', ['Num'])
+        if sys.version_info >= (3, 9):
+            checker.check_children(
+                'Subscript', ['Name', '', '[', '', 'Num', '', ']'])
+        else:
+            checker.check_children(
+                'Subscript', ['Name', '', '[', '', 'Index', '', ']'])
+            checker.check_children('Index', ['Num'])
 
     def test_tuple_node(self):
         source = '(1, 2)\n'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/ropetest/refactor/renametest.py 
new/rope-0.19.0/ropetest/refactor/renametest.py
--- old/rope-0.18.0/ropetest/refactor/renametest.py     2020-01-30 
17:02:09.000000000 +0100
+++ new/rope-0.19.0/ropetest/refactor/renametest.py     2021-02-03 
12:02:18.000000000 +0100
@@ -1,4 +1,5 @@
 import sys
+from textwrap import dedent
 try:
     import unittest2 as unittest
 except ImportError:
@@ -88,6 +89,60 @@
             'def a_func(new_param):\n    a = new_param\na_func(1)\n',
             refactored)
 
+    def test_renaming_comprehension_loop_variables(self):
+        code = '[b_var for b_var, c_var in d_var if b_var == c_var]'
+        refactored = self._local_rename(code, code.index('b_var') + 1,
+                                        'new_var')
+        self.assertEqual(
+            '[new_var for new_var, c_var in d_var if new_var == c_var]',
+            refactored)
+
+    def test_renaming_list_comprehension_loop_variables_in_assignment(self):
+        code = 'a_var = [b_var for b_var, c_var in d_var if b_var == c_var]'
+        refactored = self._local_rename(code, code.index('b_var') + 1,
+                                        'new_var')
+        self.assertEqual(
+            'a_var = [new_var for new_var, c_var in d_var if new_var == 
c_var]',
+            refactored)
+
+    def test_renaming_generator_comprehension_loop_variables(self):
+        code = 'a_var = (b_var for b_var, c_var in d_var if b_var == c_var)'
+        refactored = self._local_rename(code, code.index('b_var') + 1,
+                                        'new_var')
+        self.assertEqual(
+            'a_var = (new_var for new_var, c_var in d_var if new_var == 
c_var)',
+            refactored)
+
+    @unittest.expectedFailure
+    def test_renaming_comprehension_loop_variables_scope(self):
+        # FIXME: variable scoping for comprehensions is incorrect, we currently
+        #        don't create a scope for comprehension
+        code = dedent('''\
+            [b_var for b_var, c_var in d_var if b_var == c_var]
+            b_var = 10
+        ''')
+        refactored = self._local_rename(code, code.index('b_var') + 1,
+                                        'new_var')
+        self.assertEqual(
+            '[new_var for new_var, c_var in d_var if new_var == c_var]\nb_var 
= 10\n',
+            refactored)
+
+    @testutils.only_for_versions_higher('3.8')
+    def test_renaming_inline_assignment(self):
+        code = dedent('''\
+            while a_var := next(foo):
+                print(a_var)
+        ''')
+        refactored = self._local_rename(code, code.index('a_var') + 1,
+                                        'new_var')
+        self.assertEqual(
+            dedent('''\
+                while new_var := next(foo):
+                    print(new_var)
+            '''),
+            refactored,
+        )
+
     def test_renaming_arguments_for_normal_args_changing_calls(self):
         code = 'def a_func(p1=None, p2=None):\n    pass\na_func(p2=1)\n'
         refactored = self._local_rename(code, code.index('p2') + 1, 'p3')
@@ -627,6 +682,26 @@
                                         'new_var')
         self.assertEqual(code.replace('a_var', 'new_var', 2), refactored)
 
+    @testutils.only_for_versions_higher('3.5')
+    def test_renaming_in_generalized_dict_unpacking(self):
+        code = dedent('''\
+            a_var = {**{'stuff': 'can'}, **{'stuff': 'crayon'}}
+
+            if "stuff" in a_var:
+                print("ya")
+        ''')
+        mod1 = testutils.create_module(self.project, 'mod1')
+        mod1.write(code)
+        refactored = self._local_rename(code, code.index('a_var') + 1,
+                                        'new_var')
+        expected = dedent('''\
+            new_var = {**{'stuff': 'can'}, **{'stuff': 'crayon'}}
+
+            if "stuff" in new_var:
+                print("ya")
+        ''')
+        self.assertEqual(expected, refactored)
+
     def test_dos_line_ending_and_renaming(self):
         code = '\r\na = 1\r\n\r\nprint(2 + a + 2)\r\n'
         offset = code.replace('\r\n', '\n').rindex('a')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rope-0.18.0/setup.py new/rope-0.19.0/setup.py
--- old/rope-0.18.0/setup.py    2020-10-07 18:08:16.000000000 +0200
+++ new/rope-0.19.0/setup.py    2021-04-18 22:15:53.000000000 +0200
@@ -25,6 +25,7 @@
     'Programming Language :: Python :: 3.6',
     'Programming Language :: Python :: 3.7',
     'Programming Language :: Python :: 3.8',
+    'Programming Language :: Python :: 3.9',
     'Topic :: Software Development']
 
 

Reply via email to