Skip to content

Commit

Permalink
Allow formatting tags on a single line
Browse files Browse the repository at this point in the history
Add a --single-line-tags option, which causes consecutive tags to be
output on a single line. Also add a --multi-line-tags option to
explicitly specify the current default behaviour of outputting one tag
per line.

When the --single-line-tags option is specified, tags separated by
comments are merged into one line, as discussed in issue #22.
  • Loading branch information
ducminh-phan committed Jan 19, 2020
1 parent 12f8bdf commit de10398
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 19 deletions.
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
49 changes: 45 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,
Expand Down Expand Up @@ -184,25 +185,50 @@ 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)
__tag_groups: List[TagGroup] = attrib(factory=list)

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)

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

self.__nodes = sorted(
list(self.ast) + self.__tag_groups, key=lambda node: node.location
)

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

def __group_tags(self):
"""
Group the tags of a node, so that we can render them on a single line.
"""
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
)
self.__tag_groups.append(tag_group)

def __construct_contexts(self) -> ContextMap:
"""
Construct the information about the context a certain line might need to know to
Expand All @@ -212,6 +238,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 @@ -286,6 +315,9 @@ def __add_language_header(self) -> None:

def generate(self) -> Lines:
for node in self.__nodes:
if self.tag_line_mode is TagLineMode.SINGLELINE and isinstance(node, Tag):
continue

yield from self.visit(node)

if node in self.__nodes_with_newline:
Expand Down Expand Up @@ -324,6 +356,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
53 changes: 53 additions & 0 deletions tests/data/valid/full/expected_single_line_tags.feature
@@ -0,0 +1,53 @@
@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 |

# 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
8 changes: 5 additions & 3 deletions tests/test_formatter.py
@@ -1,6 +1,6 @@
from reformat_gherkin.ast_node import GherkinDocument
from reformat_gherkin.formatter import INDENT_LEVEL_MAP, LineGenerator
from reformat_gherkin.options import AlignmentMode
from reformat_gherkin.options import AlignmentMode, TagLineMode


def verify_indent_level_map():
Expand All @@ -20,8 +20,10 @@ def verify_indent_level_map():
assert node_type in INDENT_LEVEL_MAP


def format_ast(ast, alignment_mode=AlignmentMode.LEFT):
line_generator = LineGenerator(ast, alignment_mode)
def format_ast(
ast, alignment_mode=AlignmentMode.LEFT, tag_line_mode=TagLineMode.MULTILINE
):
line_generator = LineGenerator(ast, alignment_mode, tag_line_mode)
lines = line_generator.generate()
return "\n".join(lines)

Expand Down

0 comments on commit de10398

Please sign in to comment.