Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow formatting tags on a single line (different implementation) #29

Merged
merged 6 commits into from Jan 25, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
27 changes: 23 additions & 4 deletions tests/test_formatter.py
@@ -1,10 +1,29 @@
from reformat_gherkin.ast_node import GherkinDocument
from reformat_gherkin.formatter import LineGenerator
from reformat_gherkin.options import AlignmentMode
from reformat_gherkin.formatter import INDENT_LEVEL_MAP, LineGenerator
from reformat_gherkin.options import AlignmentMode, TagLineMode


def format_ast(ast, alignment_mode=AlignmentMode.LEFT):
line_generator = LineGenerator(ast, alignment_mode)
def verify_indent_level_map():
"""
Make sure that all node types with tags are included in the indent map.
"""
import reformat_gherkin.ast_node as ast_node

node_types = [
attr
for attr in (getattr(ast_node, name) for name in dir(ast_node))
if isinstance(attr, type)
]

for node_type in node_types:
if hasattr(node_type, "tags"):
assert node_type in INDENT_LEVEL_MAP


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