behave-format 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ """behave-format — The opinionated formatter for Behave .feature files.
2
+
3
+ behave-format is the equivalent of Black for Gherkin .feature files.
4
+ It consumes a behave-model Project and produces deterministic,
5
+ beautifully formatted output.
6
+
7
+ Public API:
8
+
9
+ from behave_format import format_project, render_project, Settings
10
+ from behave_model import load_project
11
+
12
+ project = load_project("features/")
13
+ format_project(project)
14
+ # or render to text:
15
+ text = render_project(project)
16
+
17
+ CLI:
18
+
19
+ behave-format features/ # format in place
20
+ behave-format --check features/ # check mode (exit 1 if changes needed)
21
+ behave-format --diff features/ # show diff without writing
22
+ """
23
+
24
+ from behave_format.config.settings import Settings
25
+ from behave_format.pipeline.formatter import (
26
+ format_feature,
27
+ format_project,
28
+ render_feature,
29
+ render_project,
30
+ )
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ "Settings",
36
+ "format_feature",
37
+ "format_project",
38
+ "render_feature",
39
+ "render_project",
40
+ "__version__",
41
+ ]
@@ -0,0 +1 @@
1
+ """CLI package for behave-format."""
@@ -0,0 +1,175 @@
1
+ """CLI entry point for behave-format.
2
+
3
+ Usage:
4
+ behave-format [OPTIONS] PATH...
5
+
6
+ Options:
7
+ --check Check mode: exit 1 if formatting is needed, don't write.
8
+ --diff Show diffs without writing files.
9
+ --config PATH Path to pyproject.toml (default: auto-discover).
10
+ --quiet Suppress output except errors.
11
+ --help Show help message.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import difflib
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from behave_model import load_feature
22
+
23
+ from behave_format.config.settings import Settings
24
+ from behave_format.pipeline.formatter import format_feature
25
+ from behave_format.printer.feature_printer import print_feature
26
+
27
+
28
+ def main(argv: list[str] | None = None) -> int:
29
+ """CLI entry point.
30
+
31
+ Args:
32
+ argv: Command-line arguments (defaults to sys.argv).
33
+
34
+ Returns:
35
+ Exit code: 0 on success, 1 if formatting is needed (--check).
36
+ """
37
+ parser = _build_parser()
38
+ args = parser.parse_args(argv)
39
+
40
+ settings = _load_settings(args.config)
41
+
42
+ paths = [Path(p) for p in args.paths]
43
+ if not paths:
44
+ parser.print_help()
45
+ return 0
46
+
47
+ needs_formatting = False
48
+
49
+ for path in paths:
50
+ if path.is_dir():
51
+ changed = _process_directory(
52
+ path, settings, check=args.check, diff=args.diff, quiet=args.quiet
53
+ )
54
+ if changed:
55
+ needs_formatting = True
56
+ elif path.is_file() and path.suffix == ".feature":
57
+ changed = _process_file(
58
+ path, settings, check=args.check, diff=args.diff, quiet=args.quiet
59
+ )
60
+ if changed:
61
+ needs_formatting = True
62
+ else:
63
+ print(f"Warning: skipping {path} (not a .feature file or directory)", file=sys.stderr)
64
+
65
+ if args.check and needs_formatting:
66
+ if not args.quiet:
67
+ print("Files would be reformatted")
68
+ return 1
69
+
70
+ if not args.quiet and not args.check and not args.diff:
71
+ if needs_formatting:
72
+ print("Formatted files")
73
+ else:
74
+ print("No changes needed")
75
+
76
+ return 0
77
+
78
+
79
+ def _build_parser() -> argparse.ArgumentParser:
80
+ parser = argparse.ArgumentParser(
81
+ prog="behave-format",
82
+ description="The opinionated formatter for Behave .feature files.",
83
+ )
84
+ parser.add_argument(
85
+ "paths",
86
+ nargs="*",
87
+ help="Paths to .feature files or directories containing them.",
88
+ )
89
+ parser.add_argument(
90
+ "--check",
91
+ action="store_true",
92
+ help="Check mode: exit 1 if formatting is needed, don't write files.",
93
+ )
94
+ parser.add_argument(
95
+ "--diff",
96
+ action="store_true",
97
+ help="Show diffs without writing files.",
98
+ )
99
+ parser.add_argument(
100
+ "--config",
101
+ default=None,
102
+ help="Path to pyproject.toml for configuration.",
103
+ )
104
+ parser.add_argument(
105
+ "--quiet",
106
+ action="store_true",
107
+ help="Suppress output except errors.",
108
+ )
109
+ return parser
110
+
111
+
112
+ def _load_settings(config_path: str | None) -> Settings:
113
+ if config_path:
114
+ return Settings.from_pyproject(config_path)
115
+ return Settings.from_pyproject("pyproject.toml")
116
+
117
+
118
+ def _process_directory(
119
+ directory: Path,
120
+ settings: Settings,
121
+ *,
122
+ check: bool,
123
+ diff: bool,
124
+ quiet: bool,
125
+ ) -> bool:
126
+ feature_files = sorted(directory.rglob("*.feature"))
127
+ changed = False
128
+ for fpath in feature_files:
129
+ if _process_file(fpath, settings, check=check, diff=diff, quiet=quiet):
130
+ changed = True
131
+ return changed
132
+
133
+
134
+ def _process_file(
135
+ fpath: Path,
136
+ settings: Settings,
137
+ *,
138
+ check: bool,
139
+ diff: bool,
140
+ quiet: bool,
141
+ ) -> bool:
142
+ original = fpath.read_text(encoding="utf-8")
143
+
144
+ feature = load_feature(fpath)
145
+ format_feature(feature, settings)
146
+ formatted = print_feature(feature, indent=settings.indent) + "\n"
147
+
148
+ if formatted == original:
149
+ return False
150
+
151
+ if diff:
152
+ _show_diff(fpath, original, formatted, quiet=quiet)
153
+
154
+ if not check and not diff:
155
+ fpath.write_text(formatted, encoding="utf-8")
156
+ if not quiet:
157
+ print(f"reformatted {fpath}")
158
+
159
+ return True
160
+
161
+
162
+ def _show_diff(fpath: Path, original: str, formatted: str, *, quiet: bool) -> None:
163
+ diff_lines = difflib.unified_diff(
164
+ original.splitlines(keepends=True),
165
+ formatted.splitlines(keepends=True),
166
+ fromfile=str(fpath),
167
+ tofile=str(fpath),
168
+ )
169
+ diff_text = "".join(diff_lines)
170
+ if diff_text and not quiet:
171
+ sys.stdout.write(diff_text)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Configuration package for behave-format."""
@@ -0,0 +1,81 @@
1
+ """Formatter settings with minimal configuration philosophy.
2
+
3
+ Settings can be loaded from ``pyproject.toml`` under ``[tool.behave-format]``
4
+ or constructed programmatically.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ try:
12
+ import tomllib
13
+ except ModuleNotFoundError:
14
+ import tomli as tomllib # type: ignore[no-redef]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Settings:
19
+ """Immutable formatter settings.
20
+
21
+ Attributes:
22
+ indent: Number of spaces for indentation (default 2).
23
+ sort_tags: Sort tags alphabetically (default True).
24
+ sort_features: Sort features by name (default False).
25
+ sort_scenarios: Sort scenarios by name (default False).
26
+ line_length: Maximum line length for reference (default 120).
27
+ """
28
+
29
+ indent: int = 2
30
+ sort_tags: bool = True
31
+ sort_features: bool = False
32
+ sort_scenarios: bool = False
33
+ line_length: int = 120
34
+
35
+ @classmethod
36
+ def from_pyproject(cls, path: str = "pyproject.toml") -> Settings:
37
+ """Load settings from a ``pyproject.toml`` file.
38
+
39
+ Args:
40
+ path: Path to the ``pyproject.toml`` file.
41
+
42
+ Returns:
43
+ A Settings instance. If the file or section is missing,
44
+ defaults are returned.
45
+ """
46
+ from pathlib import Path
47
+
48
+ p = Path(path)
49
+ if not p.exists():
50
+ return cls()
51
+
52
+ with p.open("rb") as f:
53
+ data = tomllib.load(f)
54
+
55
+ section = data.get("tool", {}).get("behave-format", {})
56
+ return cls(
57
+ indent=section.get("indent", 2),
58
+ sort_tags=section.get("sort_tags", True),
59
+ sort_features=section.get("sort_features", False),
60
+ sort_scenarios=section.get("sort_scenarios", False),
61
+ line_length=section.get("line_length", 120),
62
+ )
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict) -> Settings:
66
+ """Create settings from a dictionary.
67
+
68
+ Args:
69
+ data: Dictionary with optional keys: indent, sort_tags,
70
+ sort_features, sort_scenarios, line_length.
71
+
72
+ Returns:
73
+ A Settings instance.
74
+ """
75
+ return cls(
76
+ indent=data.get("indent", 2),
77
+ sort_tags=data.get("sort_tags", True),
78
+ sort_features=data.get("sort_features", False),
79
+ sort_scenarios=data.get("sort_scenarios", False),
80
+ line_length=data.get("line_length", 120),
81
+ )
@@ -0,0 +1 @@
1
+ """Formatting pipeline package."""
@@ -0,0 +1,69 @@
1
+ """Align stage — table alignment and trailing whitespace removal.
2
+
3
+ This stage ensures tables are properly aligned and no trailing
4
+ whitespace remains in any text content. It operates on the model
5
+ before printing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from behave_model.model.feature import Feature
11
+ from behave_model.model.project import Project
12
+ from behave_model.model.scenario_outline import ScenarioOutline
13
+ from behave_model.model.step import Step
14
+ from behave_model.model.table import Table
15
+
16
+
17
+ def align_project(project: Project) -> Project:
18
+ """Apply alignment rules to the project.
19
+
20
+ Args:
21
+ project: The project to align (mutated in place).
22
+
23
+ Returns:
24
+ The same project, aligned.
25
+ """
26
+ for feature in project.features:
27
+ _align_feature(feature)
28
+ return project
29
+
30
+
31
+ def _align_feature(feature: Feature) -> None:
32
+ if feature.background:
33
+ for step in feature.background.steps:
34
+ _align_step(step)
35
+ for scenario in feature.scenarios:
36
+ for step in scenario.steps:
37
+ _align_step(step)
38
+ if isinstance(scenario, ScenarioOutline):
39
+ for ex in scenario.examples:
40
+ _align_table(examples_table=ex.table)
41
+ for rule in feature.rules:
42
+ if rule.background:
43
+ for step in rule.background.steps:
44
+ _align_step(step)
45
+ for scenario in rule.scenarios:
46
+ for step in scenario.steps:
47
+ _align_step(step)
48
+ if isinstance(scenario, ScenarioOutline):
49
+ for ex in scenario.examples:
50
+ _align_table(examples_table=ex.table)
51
+
52
+
53
+ def _align_step(step: Step) -> None:
54
+ if step.data_table:
55
+ _align_table(step.data_table)
56
+
57
+
58
+ def _align_table(table: Table, *, examples_table: Table | None = None) -> None:
59
+ target = examples_table if examples_table is not None else table
60
+ _ensure_rectangular(target)
61
+
62
+
63
+ def _ensure_rectangular(table: Table) -> None:
64
+ num_cols = len(table.headers)
65
+ for row in table.rows:
66
+ while len(row.cells) < num_cols:
67
+ row.cells.append("")
68
+ if len(row.cells) > num_cols:
69
+ row.cells = row.cells[:num_cols]
@@ -0,0 +1,96 @@
1
+ """Formatter — the main orchestrator for the formatting pipeline.
2
+
3
+ The pipeline is:
4
+ 1. Normalize — clean whitespace, standardize structure
5
+ 2. Sort — order tags, features, scenarios
6
+ 3. Align — table alignment, trailing whitespace
7
+ 4. Print — convert model to .feature text
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from behave_model.model.feature import Feature
13
+ from behave_model.model.project import Project
14
+
15
+ from behave_format.config.settings import Settings
16
+ from behave_format.pipeline.align import align_project
17
+ from behave_format.pipeline.normalize import normalize_project
18
+ from behave_format.pipeline.sort import sort_project
19
+ from behave_format.printer.feature_printer import print_feature
20
+
21
+
22
+ def format_project(project: Project, settings: Settings | None = None) -> Project:
23
+ """Format a behave-model Project in place.
24
+
25
+ Applies the full pipeline: normalize → sort → align.
26
+ The project is mutated and returned.
27
+
28
+ Args:
29
+ project: The project to format.
30
+ settings: Optional formatter settings. Defaults to Settings().
31
+
32
+ Returns:
33
+ The same project, formatted.
34
+ """
35
+ if settings is None:
36
+ settings = Settings()
37
+
38
+ normalize_project(project)
39
+ sort_project(project, settings)
40
+ align_project(project)
41
+ return project
42
+
43
+
44
+ def format_feature(feature: Feature, settings: Settings | None = None) -> Feature:
45
+ """Format a single Feature in place.
46
+
47
+ Args:
48
+ feature: The feature to format.
49
+ settings: Optional formatter settings. Defaults to Settings().
50
+
51
+ Returns:
52
+ The same feature, formatted.
53
+ """
54
+ if settings is None:
55
+ settings = Settings()
56
+
57
+ from behave_format.pipeline.normalize import _normalize_feature
58
+ from behave_format.pipeline.sort import _sort_feature_tags
59
+
60
+ _normalize_feature(feature)
61
+ if settings.sort_tags:
62
+ _sort_feature_tags(feature)
63
+ # align is handled at print time
64
+ return feature
65
+
66
+
67
+ def render_feature(feature: Feature, settings: Settings | None = None) -> str:
68
+ """Format and render a Feature as .feature text.
69
+
70
+ Args:
71
+ feature: The feature to format and print.
72
+ settings: Optional formatter settings.
73
+
74
+ Returns:
75
+ The formatted .feature file content as a string.
76
+ """
77
+ format_feature(feature, settings)
78
+ return print_feature(feature, indent=settings.indent if settings else 2)
79
+
80
+
81
+ def render_project(project: Project, settings: Settings | None = None) -> str:
82
+ """Format and render an entire Project as .feature text.
83
+
84
+ Features are separated by a single blank line.
85
+
86
+ Args:
87
+ project: The project to format and print.
88
+ settings: Optional formatter settings.
89
+
90
+ Returns:
91
+ The formatted content for all features, joined by blank lines.
92
+ """
93
+ format_project(project, settings)
94
+ indent = settings.indent if settings else 2
95
+ parts = [print_feature(f, indent=indent) for f in project.features]
96
+ return "\n\n".join(parts)
@@ -0,0 +1,124 @@
1
+ """Normalize stage — clean whitespace and standardize structure.
2
+
3
+ This stage operates on a behave-model Project in place.
4
+ It NEVER changes semantics: only whitespace, indentation, and
5
+ structural consistency are normalized.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from behave_model.model.background import Background
11
+ from behave_model.model.docstring import DocString
12
+ from behave_model.model.examples import Examples
13
+ from behave_model.model.feature import Feature
14
+ from behave_model.model.project import Project
15
+ from behave_model.model.rule import Rule
16
+ from behave_model.model.scenario import Scenario
17
+ from behave_model.model.scenario_outline import ScenarioOutline
18
+ from behave_model.model.step import Step
19
+ from behave_model.model.table import Table
20
+ from behave_model.model.tag import Tag
21
+
22
+
23
+ def normalize_project(project: Project) -> Project:
24
+ """Normalize whitespace and structure across the entire project.
25
+
26
+ Args:
27
+ project: The project to normalize (mutated in place).
28
+
29
+ Returns:
30
+ The same project, normalized.
31
+ """
32
+ for feature in project.features:
33
+ _normalize_feature(feature)
34
+ project.global_tags.sort(key=lambda t: t.name)
35
+ return project
36
+
37
+
38
+ def _normalize_feature(feature: Feature) -> None:
39
+ feature.name = " ".join(feature.name.split())
40
+ feature.description = _normalize_description(feature.description)
41
+ _normalize_tags(feature.tags)
42
+ if feature.background:
43
+ _normalize_background(feature.background)
44
+ for scenario in feature.scenarios:
45
+ _normalize_scenario(scenario)
46
+ for rule in feature.rules:
47
+ _normalize_rule(rule)
48
+ for comment in feature.comments:
49
+ comment.text = comment.text.rstrip()
50
+
51
+
52
+ def _normalize_rule(rule: Rule) -> None:
53
+ rule.name = " ".join(rule.name.split())
54
+ rule.description = _normalize_description(rule.description)
55
+ _normalize_tags(rule.tags)
56
+ if rule.background:
57
+ _normalize_background(rule.background)
58
+ for scenario in rule.scenarios:
59
+ _normalize_scenario(scenario)
60
+ for comment in rule.comments:
61
+ comment.text = comment.text.rstrip()
62
+
63
+
64
+ def _normalize_background(background: Background) -> None:
65
+ background.name = " ".join(background.name.split())
66
+ for step in background.steps:
67
+ _normalize_step(step)
68
+
69
+
70
+ def _normalize_scenario(scenario: Scenario | ScenarioOutline) -> None:
71
+ scenario.name = " ".join(scenario.name.split())
72
+ scenario.description = _normalize_description(scenario.description)
73
+ _normalize_tags(scenario.tags)
74
+ for step in scenario.steps:
75
+ _normalize_step(step)
76
+ if isinstance(scenario, ScenarioOutline):
77
+ for examples in scenario.examples:
78
+ _normalize_examples(examples)
79
+ for comment in scenario.comments:
80
+ comment.text = comment.text.rstrip()
81
+
82
+
83
+ def _normalize_examples(examples: Examples) -> None:
84
+ examples.name = " ".join(examples.name.split())
85
+ _normalize_tags(examples.tags)
86
+ _normalize_table(examples.table)
87
+
88
+
89
+ def _normalize_step(step: Step) -> None:
90
+ step.keyword = step.keyword.strip()
91
+ step.name = " ".join(step.name.split())
92
+ if step.doc_string:
93
+ _normalize_doc_string(step.doc_string)
94
+ if step.data_table:
95
+ _normalize_table(step.data_table)
96
+ for comment in step.comments:
97
+ comment.text = comment.text.rstrip()
98
+
99
+
100
+ def _normalize_doc_string(doc_string: DocString) -> None:
101
+ doc_string.content_type = doc_string.content_type.strip()
102
+ delimiter = doc_string.delimiter or '"""'
103
+ doc_string.delimiter = delimiter
104
+
105
+
106
+ def _normalize_table(table: Table) -> None:
107
+ table.headers = [h.strip() for h in table.headers]
108
+ for row in table.rows:
109
+ row.cells = [c.strip() for c in row.cells]
110
+
111
+
112
+ def _normalize_tags(tags: list[Tag]) -> None:
113
+ for tag in tags:
114
+ tag.name = tag.name.strip()
115
+ if not tag.name.startswith("@"):
116
+ tag.name = "@" + tag.name
117
+
118
+
119
+ def _normalize_description(description: str) -> str:
120
+ lines = description.splitlines()
121
+ normalized = []
122
+ for line in lines:
123
+ normalized.append(" ".join(line.split()))
124
+ return "\n".join(normalized)
@@ -0,0 +1,31 @@
1
+ """Formatting rules registry.
2
+
3
+ Each rule is a callable that transforms the project at a specific
4
+ stage of the pipeline. Rules are applied in order.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+
11
+ from behave_model.model.project import Project
12
+
13
+ from behave_format.config.settings import Settings
14
+
15
+ Rule = Callable[[Project, Settings], Project]
16
+
17
+
18
+ def apply_rules(project: Project, settings: Settings, rules: list[Rule]) -> Project:
19
+ """Apply a list of formatting rules to the project.
20
+
21
+ Args:
22
+ project: The project to format.
23
+ settings: Formatter settings.
24
+ rules: Ordered list of rule callables.
25
+
26
+ Returns:
27
+ The formatted project.
28
+ """
29
+ for rule in rules:
30
+ project = rule(project, settings)
31
+ return project
@@ -0,0 +1,65 @@
1
+ """Sort stage — order tags, features, and scenarios.
2
+
3
+ Sorting is configurable via Settings. By default only tags are sorted
4
+ alphabetically. Feature and scenario sorting are opt-in.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from behave_model.model.feature import Feature
10
+ from behave_model.model.project import Project
11
+ from behave_model.model.rule import Rule
12
+ from behave_model.model.scenario_outline import ScenarioOutline
13
+
14
+ from behave_format.config.settings import Settings
15
+
16
+
17
+ def sort_project(project: Project, settings: Settings) -> Project:
18
+ """Apply sorting rules to the project.
19
+
20
+ Args:
21
+ project: The project to sort (mutated in place).
22
+ settings: Formatter settings controlling sort behavior.
23
+
24
+ Returns:
25
+ The same project, sorted.
26
+ """
27
+ if settings.sort_tags:
28
+ _sort_all_tags(project)
29
+
30
+ if settings.sort_features:
31
+ project.features.sort(key=lambda f: f.name)
32
+
33
+ if settings.sort_scenarios:
34
+ for feature in project.features:
35
+ feature.scenarios.sort(key=lambda s: s.name)
36
+ for rule in feature.rules:
37
+ rule.scenarios.sort(key=lambda s: s.name)
38
+
39
+ return project
40
+
41
+
42
+ def _sort_all_tags(project: Project) -> None:
43
+ project.global_tags.sort(key=lambda t: t.name)
44
+ for feature in project.features:
45
+ _sort_feature_tags(feature)
46
+
47
+
48
+ def _sort_feature_tags(feature: Feature) -> None:
49
+ feature.tags.sort(key=lambda t: t.name)
50
+ for scenario in feature.scenarios:
51
+ scenario.tags.sort(key=lambda t: t.name)
52
+ if isinstance(scenario, ScenarioOutline):
53
+ for ex in scenario.examples:
54
+ ex.tags.sort(key=lambda t: t.name)
55
+ for rule in feature.rules:
56
+ _sort_rule_tags(rule)
57
+
58
+
59
+ def _sort_rule_tags(rule: Rule) -> None:
60
+ rule.tags.sort(key=lambda t: t.name)
61
+ for scenario in rule.scenarios:
62
+ scenario.tags.sort(key=lambda t: t.name)
63
+ if isinstance(scenario, ScenarioOutline):
64
+ for ex in scenario.examples:
65
+ ex.tags.sort(key=lambda t: t.name)
@@ -0,0 +1 @@
1
+ """Printer package — converts behave-model objects to .feature text."""
@@ -0,0 +1,93 @@
1
+ """Feature printer — formats a complete Feature as .feature text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_model.model.feature import Feature
6
+ from behave_model.model.rule import Rule
7
+ from behave_model.model.scenario_outline import ScenarioOutline
8
+
9
+ from behave_format.printer.scenario_printer import (
10
+ print_background,
11
+ print_scenario,
12
+ print_scenario_outline,
13
+ )
14
+ from behave_format.printer.tag_printer import print_tags
15
+
16
+
17
+ def print_feature(feature: Feature, indent: int = 2) -> str:
18
+ """Format a Feature as valid, deterministic Gherkin text.
19
+
20
+ Args:
21
+ feature: The Feature to print.
22
+ indent: Base indentation for scenarios and rules.
23
+
24
+ Returns:
25
+ Multi-line string representing the complete feature file content.
26
+ """
27
+ lines: list[str] = []
28
+
29
+ if feature.tags:
30
+ lines.append(print_tags(feature.tags, indent=0))
31
+
32
+ header = f"Feature: {feature.name}" if feature.name else "Feature:"
33
+ lines.append(header)
34
+
35
+ if feature.description:
36
+ for desc_line in feature.description.splitlines():
37
+ if desc_line:
38
+ lines.append(f"{' ' * indent}{desc_line}")
39
+ else:
40
+ lines.append("")
41
+
42
+ if feature.background:
43
+ lines.append("")
44
+ lines.append(print_background(feature.background, indent=indent))
45
+
46
+ for scenario in feature.scenarios:
47
+ lines.append("")
48
+ if isinstance(scenario, ScenarioOutline):
49
+ lines.append(print_scenario_outline(scenario, indent=indent))
50
+ else:
51
+ lines.append(print_scenario(scenario, indent=indent))
52
+
53
+ for rule in feature.rules:
54
+ lines.append("")
55
+ lines.append(_print_rule(rule, indent=indent))
56
+
57
+ return "\n".join(lines)
58
+
59
+
60
+ def _print_rule(rule: Rule, indent: int = 2) -> str:
61
+ prefix = " " * indent
62
+ lines: list[str] = []
63
+
64
+ if rule.tags:
65
+ lines.append(print_tags(rule.tags, indent=indent))
66
+
67
+ header = f"Rule: {rule.name}" if rule.name else "Rule:"
68
+ lines.append(f"{prefix}{header}")
69
+
70
+ if rule.description:
71
+ for desc_line in rule.description.splitlines():
72
+ if desc_line:
73
+ lines.append(f"{prefix} {desc_line}")
74
+ else:
75
+ lines.append("")
76
+
77
+ first_child = True
78
+ if rule.background:
79
+ if not first_child:
80
+ lines.append("")
81
+ lines.append(print_background(rule.background, indent=indent + 2))
82
+ first_child = False
83
+
84
+ for scenario in rule.scenarios:
85
+ if not first_child:
86
+ lines.append("")
87
+ if isinstance(scenario, ScenarioOutline):
88
+ lines.append(print_scenario_outline(scenario, indent=indent + 2))
89
+ else:
90
+ lines.append(print_scenario(scenario, indent=indent + 2))
91
+ first_child = False
92
+
93
+ return "\n".join(lines)
@@ -0,0 +1,117 @@
1
+ """Scenario printer — formats scenarios and scenario outlines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_model.model.background import Background
6
+ from behave_model.model.examples import Examples
7
+ from behave_model.model.scenario import Scenario
8
+ from behave_model.model.scenario_outline import ScenarioOutline
9
+
10
+ from behave_format.printer.step_printer import print_step
11
+ from behave_format.printer.table_printer import print_table
12
+ from behave_format.printer.tag_printer import print_tags
13
+
14
+
15
+ def print_background(background: Background, indent: int = 2) -> str:
16
+ """Format a Background block.
17
+
18
+ Args:
19
+ background: The Background to print.
20
+ indent: Base indentation level.
21
+
22
+ Returns:
23
+ Multi-line string with the background and its steps.
24
+ """
25
+ prefix = " " * indent
26
+ header = "Background:"
27
+ if background.name:
28
+ header = f"Background: {background.name}"
29
+ lines: list[str] = [f"{prefix}{header}"]
30
+ for step in background.steps:
31
+ lines.append(print_step(step, indent=indent + 2))
32
+ return "\n".join(lines)
33
+
34
+
35
+ def print_scenario(scenario: Scenario, indent: int = 2) -> str:
36
+ """Format a Scenario as Gherkin text.
37
+
38
+ Args:
39
+ scenario: The Scenario to print.
40
+ indent: Base indentation level.
41
+
42
+ Returns:
43
+ Multi-line string with tags, scenario header, and steps.
44
+ """
45
+ prefix = " " * indent
46
+ lines: list[str] = []
47
+
48
+ if scenario.tags:
49
+ lines.append(print_tags(scenario.tags, indent=indent))
50
+
51
+ header = f"Scenario: {scenario.name}" if scenario.name else "Scenario:"
52
+ lines.append(f"{prefix}{header}")
53
+
54
+ if scenario.description:
55
+ for desc_line in scenario.description.splitlines():
56
+ if desc_line:
57
+ lines.append(f"{prefix} {desc_line}")
58
+ else:
59
+ lines.append("")
60
+
61
+ for step in scenario.steps:
62
+ lines.append(print_step(step, indent=indent + 2))
63
+
64
+ return "\n".join(lines)
65
+
66
+
67
+ def print_scenario_outline(outline: ScenarioOutline, indent: int = 2) -> str:
68
+ """Format a ScenarioOutline as Gherkin text.
69
+
70
+ Args:
71
+ outline: The ScenarioOutline to print.
72
+ indent: Base indentation level.
73
+
74
+ Returns:
75
+ Multi-line string with tags, outline header, steps, and examples.
76
+ """
77
+ prefix = " " * indent
78
+ lines: list[str] = []
79
+
80
+ if outline.tags:
81
+ lines.append(print_tags(outline.tags, indent=indent))
82
+
83
+ header = f"Scenario Outline: {outline.name}" if outline.name else "Scenario Outline:"
84
+ lines.append(f"{prefix}{header}")
85
+
86
+ if outline.description:
87
+ for desc_line in outline.description.splitlines():
88
+ if desc_line:
89
+ lines.append(f"{prefix} {desc_line}")
90
+ else:
91
+ lines.append("")
92
+
93
+ for step in outline.steps:
94
+ lines.append(print_step(step, indent=indent + 2))
95
+
96
+ for examples in outline.examples:
97
+ lines.append("")
98
+ lines.append(_print_examples(examples, indent=indent + 2))
99
+
100
+ return "\n".join(lines)
101
+
102
+
103
+ def _print_examples(examples: Examples, indent: int = 4) -> str:
104
+ prefix = " " * indent
105
+ lines: list[str] = []
106
+
107
+ if examples.tags:
108
+ lines.append(print_tags(examples.tags, indent=indent))
109
+
110
+ header = "Examples:"
111
+ if examples.name:
112
+ header = f"Examples: {examples.name}"
113
+ lines.append(f"{prefix}{header}")
114
+
115
+ lines.append(print_table(examples.table, indent=indent + 2))
116
+
117
+ return "\n".join(lines)
@@ -0,0 +1,46 @@
1
+ """Step printer — formats steps with proper indentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_model.model.docstring import DocString
6
+ from behave_model.model.step import Step
7
+
8
+ from behave_format.printer.table_printer import print_table
9
+
10
+
11
+ def print_step(step: Step, indent: int = 4) -> str:
12
+ """Format a single step as Gherkin text.
13
+
14
+ Args:
15
+ step: The Step to print.
16
+ indent: Number of spaces for indentation.
17
+
18
+ Returns:
19
+ Multi-line string with the step and any attached docstring or table.
20
+ """
21
+ prefix = " " * indent
22
+ lines: list[str] = [f"{prefix}{step.keyword} {step.name}".rstrip()]
23
+
24
+ if step.doc_string:
25
+ lines.append(_print_doc_string(step.doc_string, indent))
26
+
27
+ if step.data_table:
28
+ lines.append(print_table(step.data_table, indent=indent + 2))
29
+
30
+ return "\n".join(lines)
31
+
32
+
33
+ def _print_doc_string(doc_string: DocString, indent: int) -> str:
34
+ prefix = " " * (indent + 2)
35
+ delimiter = doc_string.delimiter or '"""'
36
+ content_type = doc_string.content_type
37
+
38
+ lines: list[str] = []
39
+ header = f"{prefix}{delimiter}"
40
+ if content_type:
41
+ header += content_type
42
+ lines.append(header)
43
+ for content_line in doc_string.lines:
44
+ lines.append(f"{prefix}{content_line}")
45
+ lines.append(f"{prefix}{delimiter}")
46
+ return "\n".join(lines)
@@ -0,0 +1,47 @@
1
+ """Table printer — formats data tables with aligned columns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_model.model.table import Table
6
+
7
+
8
+ def print_table(table: Table, indent: int = 4) -> str:
9
+ """Format a Table as aligned Gherkin table text.
10
+
11
+ Args:
12
+ table: The Table to print.
13
+ indent: Number of spaces for indentation.
14
+
15
+ Returns:
16
+ Multi-line string with aligned table rows.
17
+ """
18
+ if not table.headers and not table.rows:
19
+ return ""
20
+
21
+ widths = _compute_widths(table)
22
+ prefix = " " * indent
23
+ lines: list[str] = []
24
+
25
+ header_cells = [
26
+ h.ljust(widths[i]) if i < len(widths) else h for i, h in enumerate(table.headers)
27
+ ]
28
+ lines.append(f"{prefix}| {' | '.join(header_cells)} |")
29
+
30
+ for row in table.rows:
31
+ row_cells = [
32
+ cell.ljust(widths[i]) if i < len(widths) else cell for i, cell in enumerate(row.cells)
33
+ ]
34
+ lines.append(f"{prefix}| {' | '.join(row_cells)} |")
35
+
36
+ return "\n".join(lines)
37
+
38
+
39
+ def _compute_widths(table: Table) -> list[int]:
40
+ widths = [len(h) for h in table.headers]
41
+ for row in table.rows:
42
+ for i, cell in enumerate(row.cells):
43
+ if i < len(widths):
44
+ widths[i] = max(widths[i], len(cell))
45
+ else:
46
+ widths.append(len(cell))
47
+ return widths
@@ -0,0 +1,21 @@
1
+ """Tag printer — formats tags as space-separated strings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_model.model.tag import Tag
6
+
7
+
8
+ def print_tags(tags: list[Tag], indent: int = 0) -> str:
9
+ """Format a list of tags as a single line.
10
+
11
+ Args:
12
+ tags: List of Tag objects.
13
+ indent: Number of spaces to prefix.
14
+
15
+ Returns:
16
+ A space-separated tag string, or empty string if no tags.
17
+ """
18
+ if not tags:
19
+ return ""
20
+ prefix = " " * indent
21
+ return prefix + " ".join(t.name for t in tags)
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: behave-format
3
+ Version: 1.0.0
4
+ Summary: The opinionated formatter for Behave .feature files
5
+ Project-URL: Homepage, https://github.com/MathiasPaulenko/behave-format
6
+ Project-URL: Repository, https://github.com/MathiasPaulenko/behave-format
7
+ Project-URL: Issues, https://github.com/MathiasPaulenko/behave-format/issues
8
+ Project-URL: Changelog, https://github.com/MathiasPaulenko/behave-format/blob/main/CHANGELOG.md
9
+ Author: Mathias Paulenko
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: bdd,behave,cucumber,format,formatter,gherkin,testing
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: behave-model>=0.1.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.5; extra == 'dev'
28
+ Provides-Extra: docs
29
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
30
+ Requires-Dist: mkdocs>=1.6; extra == 'docs'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # behave-format
34
+
35
+ > The opinionated formatter for Behave `.feature` files.
36
+
37
+ [Black](https://github.com/psf/black) is for Python. [gofmt](https://go.dev/blog/gofmt) is for Go. **behave-format** is for Gherkin.
38
+
39
+ ---
40
+
41
+ ## Overview
42
+
43
+ `behave-format` is a deterministic, opinionated formatter for Behave `.feature` files. It consumes the canonical domain model from [behave-model](https://github.com/MathiasPaulenko/behave-model) and produces clean, consistent, beautifully formatted output.
44
+
45
+ **Key principle:** behave-format does NOT parse Gherkin. It does NOT lint. It does NOT validate. It ONLY transforms a `behave-model.Project` into formatted `.feature` files.
46
+
47
+ ```text
48
+ .feature files → behave-model (domain model) → behave-format → formatted .feature files
49
+ ```
50
+
51
+ ## Features
52
+
53
+ - **Opinionated** — minimal configuration, sensible defaults
54
+ - **Deterministic** — same input always produces same output
55
+ - **Idempotent** — `format(format(x)) == format(x)`
56
+ - **Fast** — handles thousands of feature files efficiently
57
+ - **CI-friendly** — `--check` mode with exit code 1 when formatting is needed
58
+ - **Safe** — never changes semantics (names, step text, table values, docstrings)
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install behave-format
64
+ ```
65
+
66
+ ## Quick Start
67
+
68
+ ### CLI
69
+
70
+ ```bash
71
+ # Format files in place (default)
72
+ behave-format features/
73
+
74
+ # Check mode (CI) — exit 1 if formatting is needed
75
+ behave-format --check features/
76
+
77
+ # Diff mode — show differences without writing
78
+ behave-format --diff features/
79
+ ```
80
+
81
+ ### Python API
82
+
83
+ ```python
84
+ from behave_model import load_project
85
+ from behave_format import format_project, render_project, Settings
86
+
87
+ project = load_project("features/")
88
+
89
+ # Format the project model in place
90
+ format_project(project)
91
+
92
+ # Or render to text
93
+ text = render_project(project, Settings())
94
+ ```
95
+
96
+ ## Formatting Rules
97
+
98
+ ### Tags
99
+
100
+ Tags are sorted alphabetically by default:
101
+
102
+ ```gherkin
103
+ @api @smoke
104
+ ```
105
+
106
+ ### Features
107
+
108
+ - One blank line before each Feature
109
+ - Clean title formatting
110
+
111
+ ### Scenarios
112
+
113
+ - One blank line before each Scenario
114
+ - Two-space indentation for steps
115
+
116
+ ```gherkin
117
+ Given user exists
118
+ When user logs in
119
+ Then dashboard is shown
120
+ ```
121
+
122
+ ### Tables
123
+
124
+ Tables are always aligned:
125
+
126
+ Before:
127
+
128
+ ```gherkin
129
+ |user|password|
130
+ |john|123|
131
+ ```
132
+
133
+ After:
134
+
135
+ ```gherkin
136
+ | user | password |
137
+ | john | 123 |
138
+ ```
139
+
140
+ ### Blank Lines
141
+
142
+ - No trailing blank lines
143
+ - No multiple consecutive empty lines
144
+ - Consistent spacing between blocks
145
+
146
+ ### Indentation
147
+
148
+ - Spaces only (no tabs)
149
+ - Default: 2 spaces
150
+
151
+ ## Configuration
152
+
153
+ Minimal configuration via `pyproject.toml`:
154
+
155
+ ```toml
156
+ [tool.behave-format]
157
+ indent = 2
158
+ sort_tags = true
159
+ sort_features = false
160
+ sort_scenarios = false
161
+ line_length = 120
162
+ ```
163
+
164
+ | Option | Default | Description |
165
+ |--------|---------|-------------|
166
+ | `indent` | `2` | Number of spaces for indentation |
167
+ | `sort_tags` | `true` | Sort tags alphabetically |
168
+ | `sort_features` | `false` | Sort features by name |
169
+ | `sort_scenarios` | `false` | Sort scenarios by name |
170
+ | `line_length` | `120` | Maximum line length (reference) |
171
+
172
+ ## Before / After
173
+
174
+ ### Before
175
+
176
+ ```gherkin
177
+ @smoke @auth
178
+ Feature: Login
179
+ As a user
180
+ I want to log in
181
+
182
+ Background:
183
+ Given a database connection
184
+
185
+ @happy
186
+ Scenario: Successful login
187
+ Given the user is on the login page
188
+ When the user enters "admin" and "password"
189
+ Then the user should be logged in
190
+ ```
191
+
192
+ ### After
193
+
194
+ ```gherkin
195
+ @auth @smoke
196
+ Feature: Login
197
+ As a user
198
+ I want to log in
199
+
200
+ Background:
201
+ Given a database connection
202
+
203
+ @happy
204
+ Scenario: Successful login
205
+ Given the user is on the login page
206
+ When the user enters "admin" and "password"
207
+ Then the user should be logged in
208
+ ```
209
+
210
+ ## Architecture
211
+
212
+ ```text
213
+ behave_format/
214
+ ├── config/
215
+ │ └── settings.py # Settings dataclass + pyproject.toml loader
216
+ ├── pipeline/
217
+ │ ├── normalize.py # Whitespace, indentation, tag normalization
218
+ │ ├── sort.py # Sort tags, features, scenarios
219
+ │ ├── align.py # Table alignment, trailing whitespace
220
+ │ ├── rules.py # Formatting rules registry
221
+ │ └── formatter.py # Main orchestrator (format_project)
222
+ ├── printer/
223
+ │ ├── feature_printer.py
224
+ │ ├── scenario_printer.py
225
+ │ ├── step_printer.py
226
+ │ ├── table_printer.py
227
+ │ └── tag_printer.py
228
+ └── cli/
229
+ └── main.py # CLI entry point
230
+ ```
231
+
232
+ ## Pipeline
233
+
234
+ 1. **Normalize** — clean whitespace, standardize indentation, normalize tags
235
+ 2. **Sort** — order tags (alphabetically by default), optionally features and scenarios
236
+ 3. **Align** — align table columns, remove trailing spaces
237
+ 4. **Print** — convert `behave-model` → `.feature` text (deterministic)
238
+
239
+ ## Safety
240
+
241
+ The formatter NEVER changes semantics:
242
+
243
+ - Feature names: preserved
244
+ - Scenario names: preserved
245
+ - Step text: preserved (only whitespace normalized)
246
+ - DocString content: preserved
247
+ - Table values: preserved (only alignment changes)
248
+ - Comments content: preserved
249
+
250
+ ## Integration
251
+
252
+ `behave-format` integrates naturally with the Behave ecosystem:
253
+
254
+ - [behave-model](https://github.com/MathiasPaulenko/behave-model) — single source of truth
255
+ - [behave-lint](https://github.com/MathiasPaulenko/behave-lint) — linting
256
+ - [behave-modern-json-report](https://github.com/MathiasPaulenko/behave-modern-json-report)
257
+ - [behave-modern-report](https://github.com/MathiasPaulenko/behave-modern-report)
258
+ - [behave-markdown-report](https://github.com/MathiasPaulenko/behave-markdown-report)
259
+
260
+ ## Development
261
+
262
+ ```bash
263
+ pip install -e ".[dev]"
264
+ pytest tests/ -v
265
+ ruff check .
266
+ ruff format --check .
267
+ ```
268
+
269
+ ## Contributing
270
+
271
+ Contributions are welcome! Please:
272
+
273
+ 1. Fork the repository
274
+ 2. Create a feature branch
275
+ 3. Run `ruff check .` and `pytest tests/` before submitting
276
+ 4. Open a Pull Request
277
+
278
+ ## License
279
+
280
+ MIT
@@ -0,0 +1,22 @@
1
+ behave_format/__init__.py,sha256=qa2JDyFPm5NkhhqOK4bpa1V7oq3o1mjwQGYWFszj2Bc,1041
2
+ behave_format/cli/__init__.py,sha256=9ogAdUoeTrMYLl2IZz6MygctP1elHvpfFM777i1cEcs,37
3
+ behave_format/cli/main.py,sha256=DSH0l8L8EayHR3QT_zotK1HNOtJFhnbT05B6BAghELA,4645
4
+ behave_format/config/__init__.py,sha256=uGsMkjI0Hyg0_utGKM3lOsp-cko-hevIQNX7L1VpPI8,47
5
+ behave_format/config/settings.py,sha256=oTxX3BGf-Fyi8jedFRDmvuzX8XE5B2BxZCzNynsXAnA,2439
6
+ behave_format/pipeline/__init__.py,sha256=Gh5e_EKrI36jOEpvfIC0mf_LjZs08_iSzFxlF30ebG8,35
7
+ behave_format/pipeline/align.py,sha256=omZpO9LNGGTTxnWynymNWeIeMwDgV19H-GR31gbEQ0o,2169
8
+ behave_format/pipeline/formatter.py,sha256=FXFYnyrU5sh20wptNtVFg03WHqrtP8BkE2LBE_3y14U,2986
9
+ behave_format/pipeline/normalize.py,sha256=Lf090fIGz9r5R2mJePryGkoEVj5VSAq-moiSUSjfHvg,4095
10
+ behave_format/pipeline/rules.py,sha256=pcdXtvhB7vR5Qh8I6nf43nCajJfaejNYOUE3AiVGYDI,789
11
+ behave_format/pipeline/sort.py,sha256=tHU7tEwv1Mlm1OKG9YzYZhZcxwsHwKxH4vBrREWxs0s,2036
12
+ behave_format/printer/__init__.py,sha256=rMnxwqPHv68zwrq2UicaUE6Dt1Sik6-Ni7GeBjTFfLM,74
13
+ behave_format/printer/feature_printer.py,sha256=1ksgWapnkyhhZFzZ0wFWrwKCmAMfqc6NuppG44_kYd0,2805
14
+ behave_format/printer/scenario_printer.py,sha256=fxnakBkH4O_VPhm33XTqx0vlM0gjHu8GOb8WFmziERY,3445
15
+ behave_format/printer/step_printer.py,sha256=W7gSvn3yuSgcs1hFMLBEcAkGs9-o7ZVTuGUbj62Az9A,1344
16
+ behave_format/printer/table_printer.py,sha256=CFJwkYG-_iambqFCnOEBmXNGbUYQzHMYfu7UqANPvvI,1334
17
+ behave_format/printer/tag_printer.py,sha256=cZQE2tvfkVZ8HDw0U0-BflLEF17eA48kgFCc-YOQ-Sc,535
18
+ behave_format-1.0.0.dist-info/METADATA,sha256=D96PgzdE7WdgI9cfkGpCRptp0a52TBQwbuADgKCSX7M,7222
19
+ behave_format-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
20
+ behave_format-1.0.0.dist-info/entry_points.txt,sha256=n9Q-LKVSfdDO-5vez6RQ96hwl3pFFMmR_uDiKjSN52A,62
21
+ behave_format-1.0.0.dist-info/licenses/LICENSE,sha256=LLr8AP65kT83seFEcknE3-76LL7oikrP87U33tZKYro,1073
22
+ behave_format-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ behave-format = behave_format.cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mathias Paulenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.