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.** - [](https://travis-ci.com/facebookincubator/Bowler) - [](https://coveralls.io/github/facebookincubator/Bowler) + [](https://github.com/facebookincubator/Bowler/actions) + [](https://codecov.io/gh/facebookincubator/Bowler) [](https://pypi.org/project/bowler) [](https://github.com/facebookincubator/bowler/blob/master/CHANGELOG.md) [](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.** -[](https://travis-ci.com/facebookincubator/Bowler) -[](https://coveralls.io/github/facebookincubator/Bowler) +[](https://github.com/facebookincubator/Bowler/actions) +[](https://codecov.io/gh/facebookincubator/Bowler) [](https://pypi.org/project/bowler) [](https://github.com/facebookincubator/bowler/blob/master/CHANGELOG.md) [](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.** - [](https://travis-ci.com/facebookincubator/Bowler) - [](https://coveralls.io/github/facebookincubator/Bowler) + [](https://github.com/facebookincubator/Bowler/actions) + [](https://codecov.io/gh/facebookincubator/Bowler) [](https://pypi.org/project/bowler) [](https://github.com/facebookincubator/bowler/blob/master/CHANGELOG.md) [](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