Skip to content

Commit

Permalink
Merge 67d5385 into 21cff12
Browse files Browse the repository at this point in the history
  • Loading branch information
ducminh-phan committed Jan 25, 2020
2 parents 21cff12 + 67d5385 commit b2e599b
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 41 deletions.
47 changes: 26 additions & 21 deletions README.md
Expand Up @@ -59,27 +59,32 @@ Usage: reformat-gherkin [OPTIONS] [SRC]...
recursively.
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-aligned, spaces
are inserted before/after the keywords to
right/left align them. By 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]
--config FILE Read configuration from FILE.
--version Show the version and exit.
--help Show this message and exit.
--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-
aligned, spaces are inserted before/after
the keywords to right/left align them. By
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: --multi-line-tags]
--config FILE Read configuration from FILE.
--version Show the version and exit.
--help Show this message and exit.
```

### Config file
Expand Down
2 changes: 2 additions & 0 deletions reformat_gherkin/ast_node/__init__.py
Expand Up @@ -14,6 +14,7 @@
from .table_cell import TableCell
from .table_row import TableRow
from .tag import Tag
from .tag_group import TagGroup

Node = Union[
Background,
Expand All @@ -30,4 +31,5 @@
TableCell,
TableRow,
Tag,
TagGroup,
]
15 changes: 15 additions & 0 deletions reformat_gherkin/ast_node/tag_group.py
@@ -0,0 +1,15 @@
from typing import Tuple, Union

from ._base import prepare
from .examples import Examples
from .feature import Feature
from .location import LocationMixin
from .scenario import Scenario
from .scenario_outline import ScenarioOutline
from .tag import Tag


@prepare
class TagGroup(LocationMixin):
members: Tuple[Tag, ...]
context: Union[Examples, Feature, Scenario, ScenarioOutline]
15 changes: 14 additions & 1 deletion reformat_gherkin/cli.py
Expand Up @@ -5,7 +5,7 @@
from .config import read_config_file
from .core import reformat
from .errors import EmptySources
from .options import AlignmentMode, NewlineMode, Options, WriteBackMode
from .options import AlignmentMode, NewlineMode, Options, TagLineMode, WriteBackMode
from .report import Report
from .utils import out
from .version import __version__
Expand Down Expand Up @@ -55,6 +55,16 @@
is_flag=True,
help="If --fast given, skip the sanity checks of file contents. [default: --safe]",
)
@click.option(
"--single-line-tags/--multi-line-tags",
is_flag=True,
default=False,
help=(
"If --single-line-tags given, output consecutive tags on one line. "
"If --multi-line-tags given, output one tag per line. "
"[default: --multi-line-tags]"
),
)
@click.option(
"--config",
type=click.Path(
Expand All @@ -73,6 +83,7 @@ def main(
alignment: Optional[str],
newline: Optional[str],
fast: bool,
single_line_tags: bool,
config: Optional[str],
) -> None:
"""
Expand All @@ -84,12 +95,14 @@ def main(
write_back_mode = WriteBackMode.from_configuration(check)
alignment_mode = AlignmentMode.from_configuration(alignment)
newline_mode = NewlineMode.from_configuration(newline)
tag_line_mode = TagLineMode.from_configuration(single_line_tags)

options = Options(
write_back=write_back_mode,
step_keyword_alignment=alignment_mode,
newline=newline_mode,
fast=fast,
tag_line_mode=tag_line_mode,
)

report = Report(check=check)
Expand Down
4 changes: 3 additions & 1 deletion reformat_gherkin/core.py
Expand Up @@ -106,7 +106,9 @@ def format_str(src_contents: str, *, options: Options) -> str:
"""
ast = parse(src_contents)

line_generator = LineGenerator(ast, options.step_keyword_alignment)
line_generator = LineGenerator(
ast, options.step_keyword_alignment, options.tag_line_mode
)
lines = line_generator.generate()

return "\n".join(lines)
Expand Down
53 changes: 49 additions & 4 deletions reformat_gherkin/formatter.py
@@ -1,5 +1,5 @@
from itertools import chain, zip_longest
from typing import Any, Dict, Iterator, List, Mapping, Optional, Set, Union
from typing import Any, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union

from attr import attrib, dataclass

Expand All @@ -18,8 +18,9 @@
Step,
TableRow,
Tag,
TagGroup,
)
from .options import AlignmentMode
from .options import AlignmentMode, TagLineMode
from .utils import camel_to_snake_case, extract_beginning_spaces, get_display_width

INDENT = " "
Expand Down Expand Up @@ -177,27 +178,59 @@ def generate_doc_string_lines(docstring: DocString) -> List[str]:
return [f"{INDENT * indent_level}{line}" for line in raw_lines]


ContextMap = Dict[Union[Comment, Tag, TableRow], Any]
ContextMap = Dict[Union[Comment, Tag, TagGroup, TableRow], Any]
Lines = Iterator[str]


@dataclass
class LineGenerator:
ast: GherkinDocument
step_keyword_alignment: AlignmentMode
tag_line_mode: TagLineMode
__nodes: List[Node] = attrib(init=False)
__contexts: ContextMap = attrib(init=False)
__nodes_with_newline: Set[Node] = attrib(init=False)
__max_step_keyword_width: int = attrib(init=False)

def __attrs_post_init__(self):
# Use `__attrs_post_init__` instead of `property` to avoid re-computing attributes
self.__nodes = sorted(list(self.ast), key=lambda node: node.location)

self.__nodes = list(self.ast)

if self.tag_line_mode is TagLineMode.SINGLELINE:
self.__group_tags()

self.__nodes.sort(key=lambda node: node.location)

self.__contexts = self.__construct_contexts()
self.__nodes_with_newline = self.__find_nodes_with_newline()
self.__max_step_keyword_width = self.__find_max_step_keyword_width()
self.__add_language_header()

def __group_tags(self):
"""
Group the tags of a node, so that we can render them on a single line.
"""

tag_groups: List[TagGroup] = []
node: Node
for node in self.ast:
if hasattr(node, "tags"):
tags: Tuple[Tag, ...] = node.tags

if tags:
# The tag group should be placed at the position of the last tag
tag_group = TagGroup(
members=tags, context=node, location=tags[-1].location
)
tag_groups.append(tag_group)

# After grouping the tags, we need to include the tag groups into
# the list of nodes and remove the tags from the list.
self.__nodes = [
node for node in self.__nodes if not isinstance(node, Tag)
] + tag_groups

def __construct_contexts(self) -> ContextMap:
"""
Construct the information about the context a certain line might need to know to
Expand All @@ -207,6 +240,9 @@ def __construct_contexts(self) -> ContextMap:
nodes = self.__nodes

for node in nodes:
if hasattr(node, "context"):
contexts[node] = node.context # type: ignore

# We want tags to have the same indentation level with their parents
for tag in getattr(node, "tags", []):
contexts[tag] = node
Expand Down Expand Up @@ -342,6 +378,15 @@ def visit_tag(self, tag: Tag) -> Lines:

yield f"{INDENT * indent_level}{tag.name}"

def visit_tag_group(self, tag_group: TagGroup) -> Lines:
context = self.__contexts[tag_group]

indent_level = INDENT_LEVEL_MAP[type(context)]

line_content = " ".join(tag.name for tag in tag_group.members)

yield f"{INDENT * indent_level}{line_content}"

def visit_table_row(self, row: TableRow) -> Lines:
context = self.__contexts[row]

Expand Down
11 changes: 11 additions & 0 deletions reformat_gherkin/options.py
Expand Up @@ -36,9 +36,20 @@ def from_configuration(cls, newline: Optional[str]) -> "NewlineMode":
return NewlineMode(newline)


@unique
class TagLineMode(Enum):
SINGLELINE = "singleline"
MULTILINE = "multiline"

@classmethod
def from_configuration(cls, single_line_tags: bool) -> "TagLineMode":
return TagLineMode.SINGLELINE if single_line_tags else TagLineMode.MULTILINE


@dataclass(frozen=True)
class Options:
write_back: WriteBackMode
step_keyword_alignment: AlignmentMode
newline: NewlineMode
tag_line_mode: TagLineMode
fast: bool
57 changes: 57 additions & 0 deletions tests/data/valid/full/expected_single_line_tags.feature
@@ -0,0 +1,57 @@
@tag-1 @tag-2
Feature: Some meaningful feature
Some meaningful feature description

Background: A solid background
Some description for this background
This description has multiple lines

Given A lot of money
| EUR |
| USD |
| VND |

@tag-no-1 @decorate-1 @fixture-1
Scenario: Gain a lot of money
This is a description for this scenario

Given I go to the bank
Then I rob the bank

# Some comment here...
@tag-no-2 @decorate-2 @prepare-2 @fixture-2
# Another comment...
Scenario Outline: Break the bank's vault
A description for this scenario outline

# Is it helpful to put a comment here?
Given I stand in front of the bank's vault
And I break the vault's door
"""
Some docstring here
A docstring can have multiple lines
With indentation
"""
Then I enter the vault
"""
Some docstring there
"""
And I see a lot of money

# Examples can have tags? Hmmm...
@test-examples-tags
Examples: Continents
This is the description for these examples

| Asia | 111 |
| Europe | 22 |
# We can even have a comment in the middle of a table
| America | 3 |
# Pipe characters in table cells need to be escaped
| a \| b | 4 |

Scenario: Escaping the bank
When I exit the bank
Then the police will start to chase me

# Some random comment at the end of the document
32 changes: 22 additions & 10 deletions tests/helpers.py
@@ -1,22 +1,34 @@
from reformat_gherkin.options import AlignmentMode, NewlineMode, Options, WriteBackMode


def make_options(alignment_mode):
from reformat_gherkin.options import (
AlignmentMode,
NewlineMode,
Options,
TagLineMode,
WriteBackMode,
)


def make_options(
*, step_keyword_alignment=AlignmentMode.NONE, tag_line_mode=TagLineMode.MULTILINE
):
return Options(
write_back=WriteBackMode.CHECK,
step_keyword_alignment=alignment_mode,
step_keyword_alignment=step_keyword_alignment,
newline=NewlineMode.KEEP,
tag_line_mode=tag_line_mode,
fast=False,
)


OPTIONS = [make_options(alignment_mode) for alignment_mode in AlignmentMode]

OPTIONS = [
make_options(step_keyword_alignment=alignment_mode)
for alignment_mode in AlignmentMode
]

FILENAME_OPTION_MAP = {
"expected_default": make_options(AlignmentMode.NONE),
"expected_left_aligned": make_options(AlignmentMode.LEFT),
"expected_right_aligned": make_options(AlignmentMode.RIGHT),
"expected_default": make_options(step_keyword_alignment=AlignmentMode.NONE),
"expected_left_aligned": make_options(step_keyword_alignment=AlignmentMode.LEFT),
"expected_right_aligned": make_options(step_keyword_alignment=AlignmentMode.RIGHT),
"expected_single_line_tags": make_options(tag_line_mode=TagLineMode.SINGLELINE),
}


Expand Down

0 comments on commit b2e599b

Please sign in to comment.