Skip to content

Commit

Permalink
Merge e2b0217 into 2cd99d8
Browse files Browse the repository at this point in the history
  • Loading branch information
MageJohn committed Mar 28, 2021
2 parents 2cd99d8 + e2b0217 commit 81d2697
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 69 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,16 @@ You can list the available options by running `reformat-gherkin --help`.
```text
Usage: reformat-gherkin [OPTIONS] [SRC]...
Reformat the given Gherkin files and all files in the given directories
recursively.
Reformat the given SRC files and all .feature files in SRC folders. If -
is passed as a file, reformat stdin and print the result to stdout.
Options:
--check Don't write the files back, just return the
status. Return code 0 means nothing would
change. Return code 1 means some files would
be reformatted. Return code 123 means there
was an internal error.
-a, --alignment [left|right] Specify the alignment of step keywords
(Given, When, Then,...). If specified, all
statements after step keywords are left-
Expand All @@ -72,18 +73,23 @@ Options:
default, step keywords are left-aligned, and
there is a single space between the step
keyword and the statement.
-n, --newline [LF|CRLF] Specify the line separators when formatting
files inplace. If not specified, line
separators are preserved.
--fast / --safe If --fast given, skip the sanity checks of
file contents. [default: --safe]
--single-line-tags / --multi-line-tags
If --single-line-tags given, output
consecutive tags on one line. If --multi-
line-tags given, output one tag per line.
[default: --single-line-tags]
--tab-width INTEGER Specify the number of spaces per
indentation-level. [default: 2]
--use-tabs Indent lines with tabs instead of spaces.
--config FILE Read configuration from FILE.
--version Show the version and exit.
Expand All @@ -93,6 +99,7 @@ Options:
Reformat-gherkin is a well-behaved Unix-style command-line tool:

- it does nothing if no sources are passed to it;
- it will read from standard input and write to standard output if - is used as the filename;
- it only outputs messages to users on standard error;
- it exits with code 0 unless an internal error occurred (or --check was used).

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "reformat-gherkin"
version = "2.0.2"
version = "2.0.3"
description = "Formatter for Gherkin language"
readme = "README.md"
authors = ["Duc-Minh Phan <alephvn@gmail.com>"]
Expand Down
9 changes: 7 additions & 2 deletions reformat_gherkin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
"src",
nargs=-1,
type=click.Path(
exists=True, file_okay=True, dir_okay=True, readable=True, resolve_path=True
exists=True,
file_okay=True,
dir_okay=True,
readable=True,
resolve_path=True,
allow_dash=True,
),
is_eager=True,
)
Expand Down Expand Up @@ -108,7 +113,7 @@ def main(
config: Optional[str],
) -> None:
"""
Reformat the given Gherkin files and all files in the given directories recursively.
Reformat the given SRC files and all .feature files in SRC folders. If - is passed as a file, reformat stdin and print the result to stdout.
"""
if config:
out(f"Using configuration from {config}.", bold=False, fg="blue")
Expand Down
61 changes: 44 additions & 17 deletions reformat_gherkin/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
import traceback
from io import TextIOWrapper
from pathlib import Path
from typing import Iterator, Set, Tuple
from typing import BinaryIO, Iterable, Iterator, Set, Tuple, Union

from .ast_node import GherkinDocument
from .errors import (
Expand All @@ -15,14 +17,14 @@
from .options import NewlineMode, Options, WriteBackMode
from .parser import parse
from .report import Report
from .utils import decode_bytes, diff, dump_to_file, err
from .utils import decode_stream, diff, dump_to_file, err, open_stream_or_path

REPORT_URL = "https://github.com/ducminh-phan/reformat-gherkin/issues"

NEWLINE_FROM_OPTION = {NewlineMode.CRLF: "\r\n", NewlineMode.LF: "\n"}


def find_sources(src: Tuple[str]) -> Set[Path]:
def find_sources(src: Iterable[str]) -> Set[Path]:
sources: Set[Path] = set()

for s in src:
Expand All @@ -39,25 +41,48 @@ def find_sources(src: Tuple[str]) -> Set[Path]:


def reformat(src: Tuple[str], report: Report, *, options: Options):
sources = find_sources(src)
use_stdin = "-" in src
sources = find_sources(filter((lambda it: it != "-"), src))

if not sources:
if not sources and not use_stdin:
raise EmptySources

if use_stdin:
changed = reformat_stdin(options=options)
report.done("stdin", changed)

for path in sources:
try:
changed = reformat_single_file(path, options=options)
report.done(path, changed)
report.done(str(path), changed)
except Exception as e:
report.failed(path, str(e))


# noinspection PyTypeChecker
def reformat_stdin(*, options: Options) -> bool:
output = sys.stdout.buffer if options.write_back == WriteBackMode.INPLACE else None
return reformat_stream_or_path(
sys.stdin.buffer, output, force_write=True, options=options
)


def reformat_single_file(path: Path, *, options: Options) -> bool:
with open(path, "rb") as buf:
src_contents, encoding, existing_newline = decode_bytes(buf.read())
out_path = path if options.write_back == WriteBackMode.INPLACE else None
return reformat_stream_or_path(path, out_path, options=options)


def reformat_stream_or_path(
in_stream_or_path: Union[BinaryIO, Path],
out_stream_or_path: Union[None, BinaryIO, Path],
*,
force_write: bool = False,
options: Options,
) -> bool:
with open_stream_or_path(in_stream_or_path, "rb") as in_stream:
src_contents, encoding, existing_newline = decode_stream(in_stream)

newline = NEWLINE_FROM_OPTION.get(options.newline, existing_newline)
newline_changed = newline != existing_newline

content_changed = True
try:
Expand All @@ -66,16 +91,18 @@ def reformat_single_file(path: Path, *, options: Options) -> bool:
content_changed = False
dst_contents = src_contents

# We reformat the file if either the content is changed, or the line separators
# need to be changed.
if not content_changed and newline == existing_newline:
return False
will_write = force_write or content_changed or newline_changed

if options.write_back == WriteBackMode.INPLACE:
with open(path, "w", encoding=encoding, newline=newline) as f:
f.write(dst_contents)
if will_write and out_stream_or_path is not None:
with open_stream_or_path(out_stream_or_path, "wb") as out_stream:
tiow = TextIOWrapper(out_stream, encoding=encoding, newline=newline)
tiow.write(dst_contents)
# Ensures that the underlying stream is not closed when the
# TextIOWrapper is garbage collected. We don't want to close a
# stream that was passed to us.
tiow.detach()

return True
return content_changed or newline_changed


def format_file_contents(src_contents: str, *, options: Options) -> str:
Expand Down
2 changes: 1 addition & 1 deletion reformat_gherkin/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Report:
same_count: int = 0
failure_count: int = 0

def done(self, path: Path, changed: bool) -> None:
def done(self, path: str, changed: bool) -> None:
"""Increment the counter for successful reformatting. Write out a message."""
if changed:
reformatted = "Would reformat" if self.check else "Reformatted"
Expand Down
22 changes: 16 additions & 6 deletions reformat_gherkin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import re
import tempfile
import tokenize
from contextlib import nullcontext
from functools import lru_cache, partial
from typing import Tuple
from pathlib import Path
from typing import IO, AnyStr, BinaryIO, Tuple, Union

import click
from wcwidth import wcswidth
Expand Down Expand Up @@ -61,21 +63,22 @@ def remove_trailing_spaces(string: str) -> str:
return "\n".join(line.rstrip() for line in lines)


def decode_bytes(src: bytes) -> Tuple[str, str, str]:
def decode_stream(src: BinaryIO) -> Tuple[str, str, str]:
"""
Return a tuple of (decoded_contents, encoding, newline).
`newline` is either CRLF or LF but `decoded_contents` is decoded with
universal newlines (i.e. only contains LF).
"""
srcbuf = io.BytesIO(src)
encoding, lines = tokenize.detect_encoding(srcbuf.readline)
# in case the source is not seekable, read into memory now
src = io.BytesIO(src.read())
encoding, lines = tokenize.detect_encoding(src.readline)
if not lines:
return "", encoding, "\n"

newline = "\r\n" if b"\r\n" == lines[0][-2:] else "\n"
srcbuf.seek(0)
with io.TextIOWrapper(srcbuf, encoding) as tiow:
src.seek(0)
with io.TextIOWrapper(src, encoding) as tiow:
return tiow.read(), encoding, newline


Expand All @@ -98,3 +101,10 @@ def get_display_width(text: str) -> int:
if width < 0:
width = len(text)
return width


def open_stream_or_path(stream_or_path: Union[IO[AnyStr], Path], mode: str):
if isinstance(stream_or_path, Path):
return open(stream_or_path, mode)
else:
return nullcontext(stream_or_path)
2 changes: 1 addition & 1 deletion reformat_gherkin/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.2"
__version__ = "2.0.3"
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,4 @@ def fin():

@pytest.fixture
def runner():
return CliRunner()
return CliRunner(mix_stderr=False)
19 changes: 19 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ def make_options(
)


def options_to_cli_args(options):
return [
"" if options.write_back == WriteBackMode.INPLACE else "--check",
""
if options.step_keyword_alignment == AlignmentMode.NONE
else f"--alignment {options.step_keyword_alignment.value}",
""
if options.newline == NewlineMode.KEEP
else f"--newline {options.newline.value}",
"--fast" if options.fast else "--safe",
"--single-line-tags"
if options.tag_line_mode == TagLineMode.SINGLELINE
else "--multi-line-tags",
"--use-tabs"
if options.indent == "\t"
else f"--tab-width {len(options.indent)}",
]


OPTIONS = [
make_options(step_keyword_alignment=alignment_mode)
for alignment_mode in AlignmentMode
Expand Down
35 changes: 33 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,55 @@
from reformat_gherkin.cli import main

from .helpers import options_to_cli_args


def test_cli_success(runner, sources):
result = runner.invoke(main, [*sources(contain_invalid=False)])

assert len(result.stdout) == 0
assert result.exit_code == 0


def test_cli_stdin_success(runner, valid_contents):
for content, expected, options in valid_contents(
with_expected=True, with_options=True
):
args = options_to_cli_args(options) + ["-"]
args.remove("--check")
args = " ".join(args)
result = runner.invoke(main, args=args, input=content)

assert result.stdout == expected
assert result.exit_code == 0


def test_cli_check(runner, sources):
result = runner.invoke(main, [*sources(contain_invalid=False), "--check"])

assert len(result.stdout) == 0
assert result.exit_code == 1


def test_cli_stdin_check(runner, valid_contents):
for content in valid_contents():
args = " ".join(["-", "--check"])
result = runner.invoke(main, args=args, input=content)

assert len(result.stdout) == 0
assert result.exit_code == 1


def test_cli_failed(runner, sources):
result = runner.invoke(main, [*sources(), "--check"])

assert len(result.stdout) == 0
assert result.exit_code == 123


def test_cli_empty_sources(runner):
result = runner.invoke(main)

assert len(result.stdout) == 0
assert result.exit_code == 0


Expand All @@ -30,8 +58,9 @@ def test_cli_check_with_valid_config(runner, sources):
main, [*sources(contain_invalid=False, with_config_file=True)]
)

assert len(result.stdout) == 0
assert result.exit_code == 1
assert result.stdout.startswith("Using configuration from")
assert result.stderr.startswith("Using configuration from")


def test_cli_check_with_invalid_config(runner, sources):
Expand All @@ -40,8 +69,9 @@ def test_cli_check_with_invalid_config(runner, sources):
[*sources(contain_invalid=False, with_config_file=True, valid_config=False)],
)

assert len(result.stdout) == 0
assert result.exit_code == 1
assert result.stdout.startswith("Error: Could not open file")
assert result.stderr.startswith("Error: Could not open file")


def test_cli_check_with_empty_config(runner, sources):
Expand All @@ -50,4 +80,5 @@ def test_cli_check_with_empty_config(runner, sources):
[*sources(contain_invalid=False, with_config_file=True, empty_config=True)],
)

assert len(result.stdout) == 0
assert result.exit_code == 0

0 comments on commit 81d2697

Please sign in to comment.