Hello community,

here is the log from the commit of package python-bowler for openSUSE:Factory 
checked in at 2020-10-29 09:47:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-bowler (Old)
 and      /work/SRC/openSUSE:Factory/.python-bowler.new.3463 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-bowler"

Thu Oct 29 09:47:49 2020 rev:3 rq:841490 version:0.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-bowler/python-bowler.changes      
2020-06-10 00:42:08.269985423 +0200
+++ /work/SRC/openSUSE:Factory/.python-bowler.new.3463/python-bowler.changes    
2020-10-29 09:47:52.760144428 +0100
@@ -1,0 +2,13 @@
+Sun Oct 11 07:34:01 UTC 2020 - John Vandenberg <jay...@gmail.com>
+
+- Update to v0.9.0
+  * Added bowler test command for testing codemod scripts
+  * Added python_version option to load files with Python 2 print statement
+  * Implemented Query.encapsulate() to generate @property wrappers
+  * Improvements to Query.add_argument() and positional arguments
+  * No longer depends on shelling-out to patch command for applying diffs
+  * Fix Query.write() to be non-interactive and silent
+  * Fix unexpected error code after successful queries
+  * Marked package as typed for PEP 561 support
+
+-------------------------------------------------------------------

Old:
----
  bowler-0.8.0.tar.gz

New:
----
  bowler-0.9.0.tar.gz

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

Other differences:
------------------
++++++ python-bowler.spec ++++++
--- /var/tmp/diff_new_pack.Yl1CmM/_old  2020-10-29 09:47:53.328144965 +0100
+++ /var/tmp/diff_new_pack.Yl1CmM/_new  2020-10-29 09:47:53.328144965 +0100
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-bowler
-Version:        0.8.0
+Version:        0.9.0
 Release:        0
 Summary:        Safe code refactoring for modern Python projects
 License:        MIT
@@ -30,15 +30,17 @@
 BuildRequires:  %{python_module base >= 3.6}
 BuildRequires:  %{python_module click}
 BuildRequires:  %{python_module fissix}
+BuildRequires:  %{python_module moreorless}
 BuildRequires:  %{python_module setuptools >= 38.6.0}
-BuildRequires:  %{python_module sh}
+BuildRequires:  %{python_module volatile}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-attrs
 Requires:       python-click
 Requires:       python-fissix
+Requires:       python-moreorless
 Requires:       python-setuptools
-Requires:       python-sh
+Requires:       python-volatile
 Requires(post): update-alternatives
 Requires(postun): update-alternatives
 BuildArch:      noarch

++++++ bowler-0.8.0.tar.gz -> bowler-0.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/PKG-INFO new/bowler-0.9.0/PKG-INFO
--- old/bowler-0.8.0/PKG-INFO   2019-06-12 20:12:23.000000000 +0200
+++ new/bowler-0.9.0/PKG-INFO   2020-09-17 03:55:20.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: bowler
-Version: 0.8.0
+Version: 0.9.0
 Summary: Safe code refactoring for modern Python projects
 Home-page: https://github.com/facebookincubator/bowler
 Author: John Reese, Facebook
@@ -10,8 +10,8 @@
         
         **Safe code refactoring for modern Python projects.**
         
-        [![build 
status](https://travis-ci.com/facebookincubator/Bowler.svg?branch=master)](https://travis-ci.com/facebookincubator/Bowler)
-        [![code 
coverage](https://img.shields.io/coveralls/github/facebookincubator/Bowler/master.svg)](https://coveralls.io/github/facebookincubator/Bowler)
+        [![build 
status](https://github.com/facebookincubator/Bowler/workflows/Build/badge.svg)](https://github.com/facebookincubator/Bowler/actions)
+        [![code 
coverage](https://img.shields.io/codecov/c/github/facebookincubator/Bowler)](https://codecov.io/gh/facebookincubator/Bowler)
         
[![version](https://img.shields.io/pypi/v/bowler.svg)](https://pypi.org/project/bowler)
         
[![changelog](https://img.shields.io/badge/change-log-blue.svg)](https://github.com/facebookincubator/bowler/blob/master/CHANGELOG.md)
         
[![license](https://img.shields.io/pypi/l/bowler.svg)](https://github.com/facebookincubator/bowler/blob/master/LICENSE)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/README.md new/bowler-0.9.0/README.md
--- old/bowler-0.8.0/README.md  2019-06-12 19:47:22.000000000 +0200
+++ new/bowler-0.9.0/README.md  2020-09-16 20:33:46.000000000 +0200
@@ -2,8 +2,8 @@
 
 **Safe code refactoring for modern Python projects.**
 
-[![build 
status](https://travis-ci.com/facebookincubator/Bowler.svg?branch=master)](https://travis-ci.com/facebookincubator/Bowler)
-[![code 
coverage](https://img.shields.io/coveralls/github/facebookincubator/Bowler/master.svg)](https://coveralls.io/github/facebookincubator/Bowler)
+[![build 
status](https://github.com/facebookincubator/Bowler/workflows/Build/badge.svg)](https://github.com/facebookincubator/Bowler/actions)
+[![code 
coverage](https://img.shields.io/codecov/c/github/facebookincubator/Bowler)](https://codecov.io/gh/facebookincubator/Bowler)
 
[![version](https://img.shields.io/pypi/v/bowler.svg)](https://pypi.org/project/bowler)
 
[![changelog](https://img.shields.io/badge/change-log-blue.svg)](https://github.com/facebookincubator/bowler/blob/master/CHANGELOG.md)
 
[![license](https://img.shields.io/pypi/l/bowler.svg)](https://github.com/facebookincubator/bowler/blob/master/LICENSE)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/__init__.py 
new/bowler-0.9.0/bowler/__init__.py
--- old/bowler-0.8.0/bowler/__init__.py 2019-06-12 19:47:22.000000000 +0200
+++ new/bowler-0.9.0/bowler/__init__.py 2020-09-17 02:16:15.000000000 +0200
@@ -8,7 +8,7 @@
 """Safe code refactoring for modern Python projects."""
 
 __author__ = "John Reese, Facebook"
-__version__ = "0.8.0"
+__version__ = "0.9.0"
 
 from .imr import FunctionArgument, FunctionSpec
 from .query import Query
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/helpers.py 
new/bowler-0.9.0/bowler/helpers.py
--- old/bowler-0.8.0/bowler/helpers.py  2019-03-20 06:25:36.000000000 +0100
+++ new/bowler-0.9.0/bowler/helpers.py  2020-09-17 02:19:34.000000000 +0200
@@ -20,7 +20,10 @@
 
 
 def print_selector_pattern(
-    node: LN, results: Capture = None, filename: Filename = None
+    node: LN,
+    results: Capture = None,
+    filename: Filename = None,
+    first: bool = True,
 ):
     key = ""
     if results:
@@ -37,9 +40,12 @@
         if node.children:
             click.echo("< ", nl=False)
             for child in node.children:
-                print_selector_pattern(child, results, filename)
+                print_selector_pattern(child, results, filename, first=False)
             click.echo("> ", nl=False)
 
+    if first:
+        click.echo()
+
 
 def print_tree(
     node: LN,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/main.py 
new/bowler-0.9.0/bowler/main.py
--- old/bowler-0.8.0/bowler/main.py     2019-06-12 19:47:22.000000000 +0200
+++ new/bowler-0.9.0/bowler/main.py     2020-09-17 02:19:34.000000000 +0200
@@ -6,10 +6,14 @@
 # LICENSE file in the root directory of this source tree.
 
 import importlib
+import importlib.util
 import logging
+import os.path
 import sys
+import unittest
+from importlib.abc import Loader
 from pathlib import Path
-from typing import List
+from typing import List, cast
 
 import click
 
@@ -87,7 +91,7 @@
     result = eval(code)  # noqa eval() - developer tool, hopefully they're not 
dumb
 
     if isinstance(result, Query):
-        if result.retcode is not None:
+        if result.retcode:
             exc = click.ClickException("query failed")
             exc.exit_code = result.retcode
             raise exc
@@ -138,5 +142,23 @@
         sys.argv[1:] = original_argv
 
 
+@main.command()
+@click.argument("codemod", required=True, type=str)
+def test(codemod: str) -> None:
+    """
+    Run the tests in the codemod file
+    """
+
+    # TODO: Unify the import code between 'run' and 'test'
+    module_name_from_codemod = os.path.basename(codemod).replace(".py", "")
+    spec = importlib.util.spec_from_file_location(module_name_from_codemod, 
codemod)
+    foo = importlib.util.module_from_spec(spec)
+    cast(Loader, spec.loader).exec_module(foo)
+    suite = unittest.TestLoader().loadTestsFromModule(foo)
+
+    result = unittest.TextTestRunner().run(suite)
+    sys.exit(not result.wasSuccessful())
+
+
 if __name__ == "__main__":
     main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/query.py 
new/bowler-0.9.0/bowler/query.py
--- old/bowler-0.8.0/bowler/query.py    2019-06-12 19:47:22.000000000 +0200
+++ new/bowler-0.9.0/bowler/query.py    2020-09-16 20:33:46.000000000 +0200
@@ -10,9 +10,8 @@
 import pathlib
 import re
 from functools import wraps
-from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union, 
cast
+from typing import Callable, List, Optional, Type, TypeVar, Union, cast
 
-from attr import Factory, dataclass
 from fissix.fixer_base import BaseFix
 from fissix.fixer_util import Attr, Comma, Dot, LParen, Name, Newline, RParen
 from fissix.pytree import Leaf, Node, type_repr
@@ -89,12 +88,14 @@
         self,
         *paths: Union[str, List[str]],
         filename_matcher: Optional[FilenameMatcher] = None,
+        python_version: int = 3,
     ) -> None:
         self.paths: List[str] = []
         self.transforms: List[Transform] = []
         self.processors: List[Processor] = []
         self.retcode: Optional[int] = None
         self.filename_matcher = filename_matcher
+        self.python_version = python_version
         self.exceptions: List[BowlerException] = []
 
         for path in paths:
@@ -506,6 +507,7 @@
                             Node(
                                 SYMBOL.decorator,
                                 [
+                                    Leaf(TOKEN.INDENT, indent),
                                     Leaf(TOKEN.AT, "@"),
                                     Name("property"),
                                     Leaf(TOKEN.NEWLINE, "\n"),
@@ -525,7 +527,7 @@
                                         SYMBOL.suite,
                                         [
                                             Newline(),
-                                            Leaf(TOKEN.INDENT, indent.value + 
"    "),
+                                            Leaf(TOKEN.INDENT, indent + "    
"),
                                             Node(
                                                 SYMBOL.simple_stmt,
                                                 [
@@ -633,9 +635,10 @@
 
                     prev = find_previous(getter, TOKEN.DEDENT, recursive=True)
                     curr = find_last(setter, TOKEN.DEDENT, recursive=True)
-                    assert isinstance(prev, Leaf) and isinstance(curr, Leaf)
-                    prev.prefix, curr.prefix = curr.prefix, prev.prefix
-                    prev.value, curr.value = curr.value, prev.value
+                    if prev and curr:
+                        assert isinstance(prev, Leaf) and isinstance(curr, 
Leaf)
+                        prev.prefix, curr.prefix = curr.prefix, prev.prefix
+                        prev.value, curr.value = curr.value, prev.value
 
         transform.callbacks.append(encapsulate_transform)
         return self
@@ -754,9 +757,13 @@
                     cast(str, type_annotation) if type_annotation != SENTINEL 
else "",
                 )
                 for index, argument in enumerate(spec.arguments):
+                    if after == argument.name:
+                        spec.arguments.insert(index + 1, new_arg)
+                        done = True
+                        break
+
                     if (
                         after == START
-                        or after == argument.name
                         or (positional and (argument.value or argument.star))
                         or (
                             keyword
@@ -779,12 +786,12 @@
                         done = True
                         break
 
-                    if (
-                        after == START
-                        or index == stop_at
-                        or argument.name
-                        or argument.star
-                    ):
+                    if index == stop_at:
+                        spec.arguments.insert(index + 1, new_arg)
+                        done = True
+                        break
+
+                    if after == START or argument.name or argument.star:
                         spec.arguments.insert(index, new_arg)
                         done = True
                         break
@@ -989,6 +996,8 @@
             kwargs["hunk_processor"] = processor
 
         kwargs.setdefault("filename_matcher", self.filename_matcher)
+        if self.python_version == 3:
+            kwargs.setdefault("options", {})["print_function"] = True
         tool = BowlerTool(fixers, **kwargs)
         self.retcode = tool.run(self.paths)
         self.exceptions = tool.exceptions
@@ -1013,4 +1022,4 @@
         return self.execute(silent=True, **kwargs)
 
     def write(self, **kwargs) -> "Query":
-        return self.execute(write=True, **kwargs)
+        return self.execute(write=True, silent=True, interactive=False, 
**kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tests/helpers.py 
new/bowler-0.9.0/bowler/tests/helpers.py
--- old/bowler-0.8.0/bowler/tests/helpers.py    2019-06-12 19:47:22.000000000 
+0200
+++ new/bowler-0.9.0/bowler/tests/helpers.py    2020-09-16 20:33:46.000000000 
+0200
@@ -70,14 +70,14 @@
     def test_print_selector_pattern(self):
         node = self.parse_line("x + 1")
         expected = """\
-arith_expr < 'x' '+' '1' > """
+arith_expr < 'x' '+' '1' > \n"""
         print_selector_pattern(node)
         self.assertMultiLineEqual(expected, self.buffer.getvalue())
 
     def test_print_selector_pattern_capture(self):
         node = self.parse_line("x + 1")
         expected = """\
-arith_expr < 'x' op='+' '1' > """
+arith_expr < 'x' op='+' '1' > \n"""
         print_selector_pattern(node, {"op": node.children[1]})
         self.assertMultiLineEqual(expected, self.buffer.getvalue())
 
@@ -85,7 +85,7 @@
         node = self.parse_line("x + 1")
         # This is not ideal, but hard to infer a good pattern
         expected = """\
-arith_expr < 'x' rest='+' rest='1' > """
+arith_expr < 'x' rest='+' rest='1' > \n"""
         print_selector_pattern(node, {"rest": node.children[1:]})
         self.assertMultiLineEqual(expected, self.buffer.getvalue())
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tests/lib.py 
new/bowler-0.9.0/bowler/tests/lib.py
--- old/bowler-0.8.0/bowler/tests/lib.py        2019-06-12 19:47:22.000000000 
+0200
+++ new/bowler-0.9.0/bowler/tests/lib.py        2020-09-16 20:33:46.000000000 
+0200
@@ -8,12 +8,11 @@
 import functools
 import multiprocessing
 import sys
-import tempfile
 import unittest
-from contextlib import contextmanager
 from io import StringIO
 
 import click
+import volatile
 from fissix import pygram, pytree
 from fissix.pgen2.driver import Driver
 
@@ -97,12 +96,12 @@
         if query_func is None:
             query_func = default_query_func
 
-        with tempfile.NamedTemporaryFile(suffix=".py") as f:
+        with volatile.file(mode="w", suffix=".py") as f:
             # TODO: I'm almost certain this will not work on Windows, since
             # NamedTemporaryFile has it already open for writing.  Consider
             # using mktemp directly?
-            with open(f.name, "w") as fw:
-                fw.write(input_text + "\n")
+            f.write(input_text + "\n")
+            f.close()
 
             query = query_func([f.name])
             assert query is not None, "Remember to return the Query"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tests/query.py 
new/bowler-0.9.0/bowler/tests/query.py
--- old/bowler-0.8.0/bowler/tests/query.py      2019-06-12 19:47:22.000000000 
+0200
+++ new/bowler-0.9.0/bowler/tests/query.py      2020-09-16 20:33:46.000000000 
+0200
@@ -8,7 +8,7 @@
 from unittest import mock
 
 from ..query import SELECTORS, Query
-from ..types import TOKEN, BowlerException, Leaf
+from ..types import TOKEN, Leaf
 from .lib import BowlerTestCase
 
 
@@ -48,6 +48,82 @@
             query_func=query_func,
         )
 
+    def test_parse_print_func_py3(self):
+        # Py 3 mode is the default
+        def select_print_func(arg):
+            return Query(arg).select_var("bar").rename("baz")
+
+        template = """{} = 1; {}"""
+        self.run_bowler_modifiers(
+            [
+                (
+                    # ParseError prevents rename succeeding
+                    template.format("bar", 'print "hello world"'),
+                    template.format("bar", 'print "hello world"'),
+                ),
+                (
+                    template.format("bar", 'print("hello world")'),
+                    template.format("baz", 'print("hello world")'),
+                ),
+                (
+                    template.format("bar", 'print("hello world", end="")'),
+                    template.format("baz", 'print("hello world", end="")'),
+                ),
+            ],
+            query_func=select_print_func,
+        )
+
+    def test_parse_print_func_py2(self):
+        def select_print_func(arg):
+            return Query(arg, python_version=2).select_var("bar").rename("baz")
+
+        template = """{} = 1; {}"""
+        self.run_bowler_modifiers(
+            [
+                (
+                    template.format("bar", 'print "hello world"'),
+                    template.format("baz", 'print "hello world"'),
+                ),
+                (
+                    # not a print function call, just parenthesised statement
+                    template.format("bar", 'print("hello world")'),
+                    template.format("baz", 'print("hello world")'),
+                ),
+                (
+                    # ParseError prevents rename succeeding
+                    template.format("bar", 'print("hello world", end="")'),
+                    template.format("bar", 'print("hello world", end="")'),
+                ),
+            ],
+            query_func=select_print_func,
+        )
+
+    def test_parse_print_func_py2_future_print(self):
+        def select_print_func(arg):
+            return Query(arg, python_version=2).select_var("bar").rename("baz")
+
+        template = """\
+from __future__ import print_function
+{} = 1; {}"""
+        self.run_bowler_modifiers(
+            [
+                (
+                    # ParseError prevents rename succeeding
+                    template.format("bar", 'print "hello world"'),
+                    template.format("bar", 'print "hello world"'),
+                ),
+                (
+                    template.format("bar", 'print("hello world")'),
+                    template.format("baz", 'print("hello world")'),
+                ),
+                (
+                    template.format("bar", 'print("hello world", end="")'),
+                    template.format("baz", 'print("hello world", end="")'),
+                ),
+            ],
+            query_func=select_print_func,
+        )
+
     def test_rename_class(self):
         self.run_bowler_modifiers(
             [("class Bar(Foo):\n  pass", "class FooBar(Foo):\n  pass")],
@@ -172,19 +248,60 @@
             [("def f(): pass", "def f(): pass")], query_func=query_func_bar
         )
 
-    def test_add_argument(self):
+    def test_encapsulate(self):
+        input = """\
+class Bar:
+    f = '42'
+"""
+
+        def query_bar_f(x):
+            return 
Query(x).select_attribute("f").in_class("Bar").encapsulate("_f")
+
+        expected = """\
+class Bar:
+    _f = '42'
+    @property
+    def f(self):
+        return self._f
+
+    @f.setter
+    def f(self, value):
+        self._f = value"""
+        output = self.run_bowler_modifier(
+            input, query_func=query_bar_f, in_process=True
+        )
+        self.assertMultiLineEqual(expected, output)
+
+    def test_add_keyword_argument(self):
         def query_func(x):
             return Query(x).select_function("f").add_argument("y", "5")
 
         self.run_bowler_modifiers(
             [
                 ("def f(x): pass", "def f(x, y=5): pass"),
+                ("def f(x, **a): pass", "def f(x, y=5, **a): pass"),
                 ("def g(x): pass", "def g(x): pass"),
                 # ("f()", "???"),
                 ("g()", "g()"),
             ],
             query_func=query_func,
         )
+
+    def test_add_positional_agument(self):
+        def f(x, y, z):
+            pass
+
+        def query_func(x):
+            return Query(x).select_function(f).add_argument("y", "5", True, 
"x")
+
+        self.run_bowler_modifiers(
+            [
+                ("def f(x): pass", "def f(x, y): pass"),
+                ("def g(x): pass", "def g(x): pass"),
+                ("f(3)", "f(3, 5)"),
+            ],
+            query_func=query_func,
+        )
 
     def test_modifier_return_value(self):
         input = "a+b"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tests/smoke-selftest.py 
new/bowler-0.9.0/bowler/tests/smoke-selftest.py
--- old/bowler-0.8.0/bowler/tests/smoke-selftest.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/bowler-0.9.0/bowler/tests/smoke-selftest.py     2020-09-16 
20:33:46.000000000 +0200
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) Facebook, Inc. and its affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+
+from bowler.tests.lib import BowlerTestCase
+
+
+class Tests(BowlerTestCase):
+    def test_pass(self):
+        pass
+
+    def test_fail(self):
+        assert False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tests/smoke.py 
new/bowler-0.9.0/bowler/tests/smoke.py
--- old/bowler-0.8.0/bowler/tests/smoke.py      2019-06-12 19:47:22.000000000 
+0200
+++ new/bowler-0.9.0/bowler/tests/smoke.py      2020-09-16 20:33:46.000000000 
+0200
@@ -7,6 +7,8 @@
 
 import io
 import logging
+import subprocess
+import sys
 from pathlib import Path
 from unittest import TestCase
 from unittest.mock import Mock
@@ -82,3 +84,13 @@
         )
         self.assertTrue(any(isinstance(e, BadTransform) for e in 
query.exceptions))
         mock_processor.assert_not_called()
+
+    def test_click_test(self):
+        proc = subprocess.run(
+            [sys.executable, "-m", "bowler", "test", 
"bowler/tests/smoke-selftest.py"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            encoding="utf-8",
+        )
+        self.assertIn("Ran 2 tests", proc.stderr)
+        self.assertEqual(1, proc.returncode)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tests/tool.py 
new/bowler-0.9.0/bowler/tests/tool.py
--- old/bowler-0.8.0/bowler/tests/tool.py       2019-06-12 19:47:22.000000000 
+0200
+++ new/bowler-0.9.0/bowler/tests/tool.py       2020-09-16 20:33:46.000000000 
+0200
@@ -50,32 +50,27 @@
 
 class ToolTest(TestCase):
     def setUp(self):
-        self.orig_file = Path(__file__).parent / "smoke-target.py.orig"
-        self.rej_file = Path(__file__).parent / "smoke-target.py.rej"
         echo_patcher = mock.patch("bowler.tool.click.echo")
         secho_patcher = mock.patch("bowler.tool.click.secho")
         self.addCleanup(echo_patcher.stop)
         self.addCleanup(secho_patcher.stop)
         self.mock_echo = echo_patcher.start()
         self.mock_secho = secho_patcher.start()
-        self.addCleanup(self.cleanup_files)
 
-    def cleanup_files(self):
-        if os.path.isfile(self.orig_file):
-            os.rename(self.orig_file, target)
-        if os.path.isfile(self.rej_file):
-            os.remove(self.rej_file)
-
-    @mock.patch("bowler.tool.sh.patch")
+    @mock.patch("bowler.tool.apply_single_file")
     def test_process_hunks_patch_called_correctly(self, mock_patch):
         tool = BowlerTool(Query().compile(), write=True, interactive=False, 
silent=True)
+        mock_patch.side_effect = lambda f, _: f
 
         tool.process_hunks(target, hunks)
         string_hunks = ""
         for hunk in hunks:
             string_hunks += "\n".join(hunk[2:]) + "\n"
         string_hunks = f"--- {target}\n+++ {target}\n" + string_hunks
-        mock_patch.assert_called_with("-u", target, 
_in=string_hunks.encode("utf-8"))
+        with open(target) as f:
+            input = f.read()
+
+        mock_patch.assert_called_with(input, string_hunks)
 
     @mock.patch.object(log, "exception")
     def test_process_hunks_invalid_hunks(self, mock_log):
@@ -83,66 +78,63 @@
 
         tool.process_hunks(target, hunks)
         mock_log.assert_called_with(
-            f"hunks failed to apply, rejects saved to {target}.rej"
+            f"failed to apply patch hunk: context error 4: start before 
range_start"
         )
-        self.assertTrue(os.path.isfile(self.rej_file))
-        self.assertTrue(os.path.isfile(self.orig_file))
 
     @mock.patch.object(log, "exception")
     def test_process_hunks_no_hunks(self, mock_log):
         tool = BowlerTool(Query().compile(), write=True, interactive=False)
         empty_hunks = [[]]
         tool.process_hunks(target, empty_hunks)
-        patch_stderr = "/usr/bin/patch: **** Only garbage was found in the 
patch input."
+        patch_stderr = "Lines without hunk header at '\\n'"
         mock_log.assert_called_with(f"failed to apply patch hunk: 
{patch_stderr}")
 
     @mock.patch("bowler.tool.prompt_user")
-    @mock.patch("bowler.tool.sh.patch")
+    @mock.patch("bowler.tool.apply_single_file")
     def test_process_hunks_after_skip_rest(self, mock_patch, mock_prompt):
         # Test that we apply the hunks that have been 'yessed' and nothing more
         tool = BowlerTool(Query().compile(), silent=False)
+        mock_patch.side_effect = lambda f, _: f
         mock_prompt.side_effect = ["y", "d"]
         tool.process_hunks(target, hunks)
-        mock_patch.assert_called_once_with(
-            "-u", target, _in="\n".join(hunks[0]).encode("utf-8") + b"\n"
-        )
+        mock_patch.assert_called_once_with(mock.ANY, "\n".join(hunks[0]) + 
"\n")
 
     @mock.patch("bowler.tool.prompt_user")
-    @mock.patch("bowler.tool.sh.patch")
+    @mock.patch("bowler.tool.apply_single_file")
     def test_process_hunks_after_quit(self, mock_patch, mock_prompt):
         # Test that we apply the hunks that have been 'yessed' and nothing more
         tool = BowlerTool(Query().compile(), silent=False)
+        mock_patch.side_effect = lambda f, _: f
         mock_prompt.side_effect = ["y", "q"]
         with self.assertRaises(BowlerQuit):
             tool.process_hunks(target, hunks)
-        mock_patch.assert_called_once_with(
-            "-u", target, _in="\n".join(hunks[0]).encode("utf-8") + b"\n"
-        )
+        mock_patch.assert_called_once_with(mock.ANY, "\n".join(hunks[0]) + 
"\n")
 
     @mock.patch("bowler.tool.prompt_user")
-    @mock.patch("bowler.tool.sh.patch")
+    @mock.patch("bowler.tool.apply_single_file")
     def test_process_hunks_after_auto_yes(self, mock_patch, mock_prompt):
         tool = BowlerTool(Query().compile(), silent=False)
+        mock_patch.side_effect = lambda f, _: f
         mock_prompt.side_effect = ["a"]
         tool.process_hunks(target, hunks)
         joined_hunks = "".join(["\n".join(hunk[2:]) + "\n" for hunk in hunks])
-        encoded_hunks = f"--- {target}\n+++ 
{target}\n{joined_hunks}".encode("utf-8")
-        mock_patch.assert_called_once_with("-u", target, _in=encoded_hunks)
+        encoded_hunks = f"--- {target}\n+++ {target}\n{joined_hunks}"
+        mock_patch.assert_called_once_with(mock.ANY, encoded_hunks)
 
     @mock.patch("bowler.tool.prompt_user")
-    @mock.patch("bowler.tool.sh.patch")
+    @mock.patch("bowler.tool.apply_single_file")
     def test_process_hunks_after_no_then_yes(self, mock_patch, mock_prompt):
         tool = BowlerTool(Query().compile(), silent=False)
+        mock_patch.side_effect = lambda f, _: f
         mock_prompt.side_effect = ["n", "y"]
         tool.process_hunks(target, hunks)
-        mock_patch.assert_called_once_with(
-            "-u", target, _in="\n".join(hunks[1]).encode("utf-8") + b"\n"
-        )
+        mock_patch.assert_called_once_with(mock.ANY, "\n".join(hunks[1]) + 
"\n")
 
     @mock.patch("bowler.tool.prompt_user")
-    @mock.patch("bowler.tool.sh.patch")
+    @mock.patch("bowler.tool.apply_single_file")
     def test_process_hunks_after_only_no(self, mock_patch, mock_prompt):
         tool = BowlerTool(Query().compile(), silent=False)
+        mock_patch.side_effect = lambda f, _: f
         mock_prompt.side_effect = ["n", "n"]
         tool.process_hunks(target, hunks)
         mock_patch.assert_not_called()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler/tool.py 
new/bowler-0.9.0/bowler/tool.py
--- old/bowler-0.8.0/bowler/tool.py     2019-06-12 19:47:22.000000000 +0200
+++ new/bowler-0.9.0/bowler/tool.py     2020-09-17 02:19:33.000000000 +0200
@@ -9,14 +9,16 @@
 import logging
 import multiprocessing
 import os
+import sys
 import time
 from queue import Empty
-from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple
+from typing import Any, Iterator, List, Optional, Sequence, Tuple
 
 import click
-import sh
+from fissix import pygram
 from fissix.pgen2.parse import ParseError
-from fissix.refactor import RefactoringTool
+from fissix.refactor import RefactoringTool, _detect_future_features
+from moreorless.patch import PatchException, apply_single_file
 
 from .helpers import filename_endswith
 from .types import (
@@ -27,7 +29,6 @@
     FilenameMatcher,
     Fixers,
     Hunk,
-    Node,
     Processor,
     RetryFile,
 )
@@ -89,13 +90,12 @@
         interactive: bool = True,
         write: bool = False,
         silent: bool = False,
-        in_process: bool = False,
+        in_process: Optional[bool] = None,
         hunk_processor: Processor = None,
         filename_matcher: Optional[FilenameMatcher] = None,
         **kwargs,
     ) -> None:
         options = kwargs.pop("options", {})
-        options["print_function"] = True
         super().__init__(fixers, *args, options=options, **kwargs)
         self.queue_count = 0
         self.queue = multiprocessing.JoinableQueue()  # type: ignore
@@ -104,8 +104,13 @@
         self.interactive = interactive
         self.write = write
         self.silent = silent
-        # pick the most restrictive of flags
-        self.in_process = in_process or self.IN_PROCESS
+        if in_process is None:
+            in_process = self.IN_PROCESS
+        # pick the most restrictive of flags; we can pickle fixers when
+        # using spawn.
+        if sys.platform == "win32" or sys.version_info > (3, 7):
+            in_process = True
+        self.in_process = in_process
         self.exceptions: List[BowlerException] = []
         if hunk_processor is not None:
             self.hunk_processor = hunk_processor
@@ -141,6 +146,9 @@
             if hunk:
                 hunks.append([a, b, *hunk])
 
+            original_grammar = self.driver.grammar
+            if "print_function" in _detect_future_features(new_text):
+                self.driver.grammar = pygram.python_grammar_no_print_statement
             try:
                 new_tree = self.driver.parse_string(new_text)
                 if new_tree is None:
@@ -151,6 +159,8 @@
                     filename=filename,
                     hunks=hunks,
                 ) from e
+            finally:
+                self.driver.grammar = original_grammar
 
         return hunks
 
@@ -341,21 +351,18 @@
 
     def apply_hunks(self, accepted_hunks, filename):
         if accepted_hunks:
-            accepted_hunks = f"--- {filename}\n+++ 
{filename}\n{accepted_hunks}"
-            args = ["patch", "-u", filename]
-            self.log_debug(f"running {args}")
+            with open(filename) as f:
+                data = f.read()
+
             try:
-                sh.patch(*args[1:], _in=accepted_hunks.encode("utf-8"))  # 
type: ignore
-            except sh.ErrorReturnCode as e:
-                if e.stderr:
-                    err = e.stderr.strip().decode("utf-8")
-                else:
-                    err = e.stdout.strip().decode("utf-8")
-                    if "saving rejects to file" in err:
-                        err = err.split("saving rejects to file")[1]
-                        log.exception(f"hunks failed to apply, rejects saved 
to{err}")
-                        return
+                accepted_hunks = f"--- {filename}\n+++ 
{filename}\n{accepted_hunks}"
+                new_data = apply_single_file(data, accepted_hunks)
+            except PatchException as err:
                 log.exception(f"failed to apply patch hunk: {err}")
+                return
+
+            with open(filename, "w") as f:
+                f.write(new_data)
 
     def run(self, paths: Sequence[str]) -> int:
         if not self.errors:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler.egg-info/PKG-INFO 
new/bowler-0.9.0/bowler.egg-info/PKG-INFO
--- old/bowler-0.8.0/bowler.egg-info/PKG-INFO   2019-06-12 20:12:23.000000000 
+0200
+++ new/bowler-0.9.0/bowler.egg-info/PKG-INFO   2020-09-17 03:55:20.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: bowler
-Version: 0.8.0
+Version: 0.9.0
 Summary: Safe code refactoring for modern Python projects
 Home-page: https://github.com/facebookincubator/bowler
 Author: John Reese, Facebook
@@ -10,8 +10,8 @@
         
         **Safe code refactoring for modern Python projects.**
         
-        [![build 
status](https://travis-ci.com/facebookincubator/Bowler.svg?branch=master)](https://travis-ci.com/facebookincubator/Bowler)
-        [![code 
coverage](https://img.shields.io/coveralls/github/facebookincubator/Bowler/master.svg)](https://coveralls.io/github/facebookincubator/Bowler)
+        [![build 
status](https://github.com/facebookincubator/Bowler/workflows/Build/badge.svg)](https://github.com/facebookincubator/Bowler/actions)
+        [![code 
coverage](https://img.shields.io/codecov/c/github/facebookincubator/Bowler)](https://codecov.io/gh/facebookincubator/Bowler)
         
[![version](https://img.shields.io/pypi/v/bowler.svg)](https://pypi.org/project/bowler)
         
[![changelog](https://img.shields.io/badge/change-log-blue.svg)](https://github.com/facebookincubator/bowler/blob/master/CHANGELOG.md)
         
[![license](https://img.shields.io/pypi/l/bowler.svg)](https://github.com/facebookincubator/bowler/blob/master/LICENSE)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler.egg-info/SOURCES.txt 
new/bowler-0.9.0/bowler.egg-info/SOURCES.txt
--- old/bowler-0.8.0/bowler.egg-info/SOURCES.txt        2019-06-12 
20:12:23.000000000 +0200
+++ new/bowler-0.9.0/bowler.egg-info/SOURCES.txt        2020-09-17 
03:55:20.000000000 +0200
@@ -26,6 +26,7 @@
 bowler/tests/helpers.py
 bowler/tests/lib.py
 bowler/tests/query.py
+bowler/tests/smoke-selftest.py
 bowler/tests/smoke-target.py
 bowler/tests/smoke.py
 bowler/tests/tool.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/bowler.egg-info/requires.txt 
new/bowler-0.9.0/bowler.egg-info/requires.txt
--- old/bowler-0.8.0/bowler.egg-info/requires.txt       2019-06-12 
20:12:23.000000000 +0200
+++ new/bowler-0.9.0/bowler.egg-info/requires.txt       2020-09-17 
03:55:20.000000000 +0200
@@ -1,4 +1,5 @@
 attrs
 click
 fissix
-sh
+moreorless>=0.2.0
+volatile
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/docs/api-query.md 
new/bowler-0.9.0/docs/api-query.md
--- old/bowler-0.8.0/docs/api-query.md  2018-12-07 15:13:40.000000000 +0100
+++ new/bowler-0.9.0/docs/api-query.md  2020-09-16 20:33:46.000000000 +0200
@@ -45,7 +45,11 @@
 Create a new query object to process the given set of files or directories.
 
 ```python
-Query(*paths: Union[str, List[str]], filename_matcher: FilenameMatcher)
+Query(
+  *paths: Union[str, List[str]],
+  python_version: int,
+  filename_matcher: FilenameMatcher,
+)
 ```
 
 * `*paths` - Accepts either individual file or directory paths (relative to 
the current
@@ -56,6 +60,11 @@
   eligible for refactoring.  Defaults to only matching files that end with
   `.py`.
 
+* `python_version` - The 'major' python version of the files to be refactored, 
i.e. `2`
+  or `3`. This allows the parser to handle `print` statement vs function 
correctly. This
+  includes detecting use of `from __future__ import print_function` when
+  `python_version=2`. Default is `3`.
+
 
 ### `.select()`
 
@@ -168,7 +177,7 @@
 
 ### `.write()`
 
-Alias for `.execute(interactive=False, write=True)`
+Alias for `.execute(interactive=False, write=True, silent=True)`
 
 ### `.dump()`
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/docs/api-selectors.md 
new/bowler-0.9.0/docs/api-selectors.md
--- old/bowler-0.8.0/docs/api-selectors.md      2019-03-20 06:25:36.000000000 
+0100
+++ new/bowler-0.9.0/docs/api-selectors.md      2020-09-16 20:33:46.000000000 
+0200
@@ -14,9 +14,19 @@
 [pattern grammar][]. Matching elements of the [Python grammar][] is done by 
listing
 the grammar element, optionally followed by angle brackets containing nested 
match
 expressions. The `any` keyword can be used to match grammar elements, 
regardless of
-their type, while `*` denotes elements that repeat zero or more times. Make 
sure to
-include necessary string literal tokens when using nested expressions, and 
`any*` to
-match remaining grammar elements.
+their type, while `*` denotes elements that repeat zero or more times. 
+
+Make sure to include _necessary_ string literal tokens when using nested 
expressions, 
+and `any*` to match remaining _grammar_ elements.
+
+```python
+# any* does not capture the '=' string literal
+expr_stmt<
+    attr_name='{name}' attr_value=any*
+>
+# but declare '(' and ')' string literals to differentiate from '[' and ']'
+trailer< '(' function_arguments=any* ')' >
+```
 
 Example pattern to match class definitions that contain a function definition 
(the
 "suite" denotes the body of the class definition):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/bowler-0.8.0/requirements.txt 
new/bowler-0.9.0/requirements.txt
--- old/bowler-0.8.0/requirements.txt   2018-12-01 15:10:53.000000000 +0100
+++ new/bowler-0.9.0/requirements.txt   2020-09-16 20:33:46.000000000 +0200
@@ -1,4 +1,5 @@
 attrs
 click
 fissix
-sh
+moreorless>=0.2.0
+volatile


Reply via email to