Skip to content

Commit

Permalink
Better formatting
Browse files Browse the repository at this point in the history
* Triple quoted string formatting only on parameter values (#39)
* Top-level elements trigger two newlines for spacing (#38)
	Elements are python code and snakemake keywords
  With simplified, and added, unit tests
  • Loading branch information
bricoletc committed May 10, 2020
1 parent 8200459 commit 495d4c9
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 59 deletions.
44 changes: 20 additions & 24 deletions snakefmt/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(
):
self._line_length = line_length
self.result = ""
self.from_rule, self.from_comment = False, False
self.from_comment = False
self.first = True

if black_config is None:
Expand Down Expand Up @@ -77,49 +77,51 @@ def get_formatted(self):

def flush_buffer(self, from_python: bool = False):
if len(self.buffer) == 0 or self.buffer.isspace():
self.from_comment = False
self.result += self.buffer
self.buffer = ""
return

if not from_python:
formatted = self.run_black_format_str(self.buffer, self.target_indent)
formatted = self.run_black_format_str(self.buffer)
if self.target_indent > 0:
formatted = textwrap.indent(formatted, TAB * self.target_indent)

self.add_newlines(self.target_indent)
self.from_comment = False
try:
if formatted.splitlines()[-1][0] == "#":
all_lines = formatted.splitlines()
if len(all_lines) > 0 and len(all_lines[-1]) > 0:
if all_lines[-1][0] == "#":
self.from_comment = True
except Exception:
pass
self.add_newlines(self.target_indent, keyword_name="")
else:
formatted = self.buffer.rstrip(TAB)
self.result += formatted
self.buffer = ""

def process_keyword_context(self):
cur_indent = self.context.cur_indent
self.add_newlines(cur_indent, self.context.keyword_name)
self.add_newlines(cur_indent)
formatted = (
f"{TAB * cur_indent}{self.context.keyword_name}:{self.context.comment}"
+ "\n"
)
self.result += formatted

def process_keyword_param(self, param_context):
self.add_newlines(param_context.target_indent - 1, param_context.keyword_name)
self.add_newlines(param_context.target_indent - 1)
in_rule = issubclass(param_context.incident_vocab.__class__, SnakeRule)
self.result += self.format_params(param_context, in_rule)

def run_black_format_str(self, string: str, target_indent: int) -> str:
def run_black_format_str(self, string: str) -> str:
try:
fmted = black.format_str(string, mode=self.black_mode)
except black.InvalidInput as e:
raise InvalidPython(
f"Got error:\n```\n{str(e)}\n```\n" f"while formatting code with black."
) from None
fmted = self.format_string(fmted, target_indent)
return fmted

def format_string(self, string: str, target_indent: int) -> str:
def string_format(self, string: str, target_indent: int) -> str:
# Only indent non-triple-quoted string portions
pos = 0
used_indent = TAB * target_indent
Expand All @@ -143,7 +145,7 @@ def format_string(self, string: str, target_indent: int) -> str:
def format_param(
self,
parameter: Parameter,
target_indent: str,
target_indent: int,
inline_formatting: bool,
single_param: bool = False,
) -> str:
Expand All @@ -158,10 +160,10 @@ def format_param(
raise InvalidParameterSyntax(f"{parameter.line_nb}{val}") from None

if inline_formatting:
val = val.replace("\n", "")
val = val.replace("\n", "") # collapse strings on multiple lines
try:
val = self.run_black_format_str(val, 0)
val = self.format_string(val, target_indent)
val = self.run_black_format_str(val)
val = self.string_format(val, target_indent)
if parameter.has_a_key(): # Remove space either side of '='
match_equal = re.match("(.*?) = (.*)", val, re.DOTALL)
val = f"{match_equal.group(1)}={match_equal.group(2)}"
Expand Down Expand Up @@ -201,16 +203,10 @@ def format_params(self, parameters: ParameterSyntax, in_rule: bool) -> str:
)
return result

def add_newlines(self, cur_indent: int, keyword_name: str = ""):
def add_newlines(self, cur_indent: int):
if cur_indent == 0:
if self.from_rule:
if not self.first and not self.from_comment:
self.result += "\n\n"
elif not self.first:
if self.rule_like(keyword_name) and not self.from_comment:
self.result += "\n\n"
elif keyword_name == "":
self.result += "\n" # Add newline for python code
self.from_rule = True if self.rule_like(keyword_name) else False
if self.first:
self.first = False

Expand Down
1 change: 1 addition & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_config_adherence_for_python_outside_rules(self, cli_runner, tmp_path):

expected_output = """include: \"a\"
list_of_lots_of_things = [
1,
2,
Expand Down
82 changes: 47 additions & 35 deletions tests/test_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_line_wrapped_python_code_outside_rule(self):
f"{TAB}1,\n{TAB}2,\n{TAB}3,\n{TAB}4,\n{TAB}5,\n{TAB}6,\n{TAB}7,\n"
f"{TAB}8,\n{TAB}9,\n{TAB}10,\n"
"]\n"
"include: snakefile\n"
"\n\ninclude: snakefile\n"
)

assert actual == expected
Expand Down Expand Up @@ -145,16 +145,16 @@ def test_python_code_after_nested_snakecode_gets_formatted(self):
"snakefmt.formatter.Formatter.run_black_format_str", spec=True
) as mock_m:
mock_m.return_value = ""
formatter = setup_formatter(snakecode)
setup_formatter(snakecode)
assert mock_m.call_count == 2
assert mock_m.call_args_list[0] == mock.call('"a"', 0)
assert mock_m.call_args_list[1] == mock.call("b = 2\n", 0)
assert mock_m.call_args_list[0] == mock.call('"a"')
assert mock_m.call_args_list[1] == mock.call("b = 2\n")

formatter = setup_formatter(snakecode)
expected = (
"if condition:\n"
f'{TAB * 1}include: "a"\n'
"\nb = 2\n" # python code gets formatted here
"\n\nb = 2\n" # python code gets formatted here
)
assert formatter.get_formatted() == expected

Expand Down Expand Up @@ -230,7 +230,7 @@ def test_parameter_keywords_inside_python_code(self):
f'{TAB * 1}include: "a"\n'
f"else:\n"
f'{TAB * 1}include: "b"\n'
f'include: "c"\n'
f'\n\ninclude: "c"\n'
)
formatter = setup_formatter(snakecode)
assert formatter.get_formatted() == snakecode
Expand Down Expand Up @@ -313,7 +313,8 @@ def test_improperly_spaced_tpq_gets_respaced(self):
'''
assert formatter.get_formatted() == expected

def test_docstrings_get_reindented(self):
def test_docstrings_get_retabbed(self):
"""But not reindented"""
snakecode = f'''def f():
"""Does not do
much
Expand All @@ -339,8 +340,8 @@ def test_docstrings_get_reindented(self):
rule a:
{TAB * 1}"""
{TAB * 1}The rule
{TAB * 1}{' ' * 6}a
{TAB * 1}{' ' * 2}The rule
{TAB * 1}{' ' * 8}a
{TAB * 1}"""
{TAB * 1}message:
{TAB * 2}"a"
Expand Down Expand Up @@ -521,10 +522,11 @@ def test_rule_re_indenting(self):


class TestCommentTreatment:
def test_comment_after_parameter_keyword_not_absorbed(self):
snakecode = f'include: "a"\n\n# A comment\n'
def test_comment_after_parameter_keyword_twonewlines(self):
snakecode = f'include: "a"\n# A comment\n'
formatter = setup_formatter(snakecode)
assert formatter.get_formatted() == snakecode
expected = f'include: "a"\n\n\n# A comment\n'
assert formatter.get_formatted() == expected

def test_comments_after_parameters_kept(self):
snakecode = (
Expand All @@ -538,19 +540,20 @@ def test_comments_after_parameters_kept(self):


class TestNewlineSpacing:
def test_non_rule_has_no_keyword_spacing_above(self):
formatter = setup_formatter("# load config\n" 'configfile: "config.yaml"')
def test_parameter_keyword_spacing_above(self):
formatter = setup_formatter("b = 2\n" 'configfile: "config.yaml"')

actual = formatter.get_formatted()
expected = '# load config\nconfigfile: "config.yaml"\n'
expected = 'b = 2\n\n\nconfigfile: "config.yaml"\n'

assert actual == expected

def test_non_rule_has_no_keyword_spacing_below(self):
def test_parameter_keyword_spacing_below(self):
snakecode = 'configfile: "config.yaml"\nreport: "report.rst"\n'
formatter = setup_formatter(snakecode)
expected = 'configfile: "config.yaml"\n\n\nreport: "report.rst"\n'

formatter.get_formatted() == snakecode
assert formatter.get_formatted() == expected

def test_double_spacing_for_rules(self):
formatter = setup_formatter(
Expand Down Expand Up @@ -583,25 +586,35 @@ def test_double_spacing_for_rules(self):

assert actual == expected

def test_rule_with_three_newlines_below_only_has_two_after_formatting(self):
formatter = setup_formatter(
f"rule all:\n"
f"{TAB * 1}input:\n"
f'{TAB * 2}"a",\n'
f"\n\n\n"
f'foo = "bar"'
def test_keyword_three_newlines_below_two_after_formatting(self):
formatter = setup_formatter(f'include: "a"\n\n\n\nconfigfile: "b"\n')
expected = f'include: "a"\n\n\nconfigfile: "b"\n'

assert formatter.get_formatted() == expected

def test_python_code_mixed_with_keywords_proper_spacing(self):
snakecode = (
"def p():\n"
f"{TAB * 1}pass\n"
f"include: a\n"
f"def p2():\n"
f"{TAB * 1}pass\n"
f"def p3():\n"
f"{TAB * 1}pass\n"
)
formatter = setup_formatter(snakecode)

actual = formatter.get_formatted()
expected = (
f"rule all:\n"
f"{TAB * 1}input:\n"
f'{TAB * 2}"a",\n'
f"\n\n"
f'foo = "bar"\n'
"def p():\n"
f"{TAB * 1}pass\n\n\n"
f"include: a\n\n\n"
f"def p2():\n"
f"{TAB * 1}pass\n\n\n"
f"def p3():\n"
f"{TAB * 1}pass\n"
)

assert actual == expected
assert formatter.get_formatted() == expected

def test_comment_above_does_not_trigger_spacing(self):
snakecode = (
Expand All @@ -611,12 +624,12 @@ def test_comment_above_does_not_trigger_spacing(self):
formatter = setup_formatter(snakecode)
assert formatter.get_formatted() == snakecode

def test_comment_below_rule_gets_spaced(self):
def test_comment_below_keyword_gets_spaced(self):
formatter = setup_formatter(
f"""# Rules
rule all:
{TAB * 1}input: output_files
# https://github.com/nanoporetech/taiyaki/blob/master/docs/walkthrough.rst#bam-of-mapped-basecalls
# Comment
"""
)

Expand All @@ -627,7 +640,6 @@ def test_comment_below_rule_gets_spaced(self):
{TAB * 2}output_files,
# https://github.com/nanoporetech/taiyaki/blob/master/docs/walkthrough.rst#bam-of-mapped-basecalls
# Comment
"""

assert actual == expected

0 comments on commit 495d4c9

Please sign in to comment.