Skip to content

Commit c6f41dd

Browse files
authored
Add OrderSettingsSection transformer (#102)
* add OrderSettingsSection transformer
1 parent 7ad9890 commit c6f41dd

23 files changed

+606
-8
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- New SmartSortKeywords transformer (disabled by default) for sorting out keywords inside ``*** Keywords ***`` section [#52](https://github.com/MarketSquare/robotframework-tidy/issues/52)
1414
- New MergeAndOrderSections transformer for merging duplicated sections and ordering them (order is configurable) [#70](https://github.com/MarketSquare/robotframework-tidy/issues/70)
1515
- New OrderSettings transformer for ordering settings like [Arguments], [Setup], [Return] inside Keywords and Test Cases [#59](https://github.com/MarketSquare/robotframework-tidy/issues/59)
16+
- New OrderSettingsSection transformer for ordering settings, imports inside ``*** Settings ****`` section [#100](https://github.com/MarketSquare/robotframework-tidy/issues/100)
1617

1718
### Features
1819
- New option ``--configure`` or ``-c`` for configuring transformer parameters. It works the same way configuring through ``--transform`` works. The benefit of using ``--configure`` is that you can configure selected transformers and still run all transformers [#96](https://github.com/MarketSquare/robotframework-tidy/issues/96)

robotidy/transformers/MergeAndOrderSections.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,26 @@ class MergeAndOrderSections(ModelTransformer):
2424
2525
If both ``*** Test Cases ***`` and ``*** Tasks ***`` are defined in one file they will be merged into one (header
2626
name will be taken from first encountered section).
27+
28+
Any data before first section is treated as comment in Robot Framework. This transformer add ``*** Comments ***``
29+
section for such lines::
30+
31+
i am comment
32+
# robocop: disable
33+
*** Settings ***
34+
35+
To::
36+
37+
*** Comments ***
38+
i am comment
39+
# robocop: disable
40+
*** Settings ***
41+
42+
You can disable this behaviour by setting ``create_comment_section`` to False.
2743
"""
28-
def __init__(self, order: str = ''):
44+
def __init__(self, order: str = '', create_comment_section: bool = True):
2945
self.sections_order = self.parse_order(order)
46+
self.create_comment_section = create_comment_section
3047

3148
@staticmethod
3249
def parse_order(order):
@@ -118,14 +135,14 @@ def from_last_section(node):
118135
node.header = Statement.from_tokens(list(node.header.tokens[:-1]) + [Token(Token.EOL, '\n')])
119136
return node
120137

121-
@staticmethod
122-
def get_section_type(section):
138+
def get_section_type(self, section):
123139
header_tokens = (Token.COMMENT_HEADER, Token.TESTCASE_HEADER, Token.SETTING_HEADER, Token.KEYWORD_HEADER,
124140
Token.VARIABLE_HEADER)
125141
if section.header:
126142
name_token = section.header.get_token(*header_tokens)
127143
section_type = name_token.type
128144
else:
129145
section_type = Token.COMMENT_HEADER
130-
section.header = SectionHeader.from_params(section_type, '*** Comments ***')
146+
if self.create_comment_section:
147+
section.header = SectionHeader.from_params(section_type, '*** Comments ***')
131148
return section_type

robotidy/transformers/OrderSettings.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,8 @@ def get_order(order, default, name_map):
7878
if not order:
7979
return []
8080
splitted = order.lower().split(',')
81-
parsed_order = []
8281
try:
83-
for split in splitted:
84-
parsed_order.append(name_map[split])
85-
return parsed_order
82+
return [name_map[split] for split in splitted]
8683
except KeyError:
8784
raise click.BadOptionUsage(
8885
option_name='transform',
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
from collections import defaultdict
2+
3+
import click
4+
from robot.api.parsing import (
5+
ModelTransformer,
6+
Comment,
7+
Token,
8+
EmptyLine,
9+
LibraryImport
10+
)
11+
from robot.libraries import STDLIBS
12+
13+
14+
class OrderSettingsSection(ModelTransformer):
15+
"""
16+
Order settings inside *** Settings *** section.
17+
18+
Settings are grouped in following groups:
19+
- documentation (Documentation, Metadata),
20+
- imports (Library, Resource, Variables),
21+
- settings (Suite Setup and Teardown, Test Setup and Teardown, Test Timeout, Test Template),
22+
- tags (Force Tags, Default Tags)
23+
24+
Then ordered by groups (according to ``group_order = documentation,imports,settings,tags`` order). Every
25+
group is separated by ``new_lines_between_groups = 1`` new lines.
26+
Settings are grouped inside group. Default order can be modified through following parameters:
27+
- ``documentation_order = documentation,metadata``
28+
- ``imports_order = library,resource,variables``
29+
- ``settings_order = suite_setup,suite_teardown,test_setup,test_teardown,test_timeout,test_template``
30+
31+
Setting names omitted from custom order will be removed from the file. In following example we are missing metadata
32+
therefore all metadata will be removed::
33+
34+
robotidy --configure OrderSettingsSection:documentation_order=documentation
35+
36+
Libraries are grouped into built in libraries and custom libraries.
37+
Parsing errors (such as Resources instead of Resource, duplicated settings) are moved to the end of section.
38+
"""
39+
def __init__(self, new_lines_between_groups: int = 1, group_order: str = None, documentation_order: str = None,
40+
imports_order: str = None, settings_order: str = None, tags_order: str = None):
41+
self.last_section = None
42+
self.new_lines_between_groups = new_lines_between_groups
43+
self.group_order = self.parse_group_order(group_order)
44+
self.documentation_order = self.parse_order_in_group(
45+
documentation_order,
46+
(
47+
Token.DOCUMENTATION,
48+
Token.METADATA
49+
),
50+
{
51+
'documentation': Token.DOCUMENTATION,
52+
'metadata': Token.METADATA
53+
}
54+
)
55+
self.imports_order = self.parse_order_in_group(
56+
imports_order,
57+
(
58+
Token.LIBRARY,
59+
Token.RESOURCE,
60+
Token.VARIABLES
61+
),
62+
{
63+
'library': Token.LIBRARY,
64+
'resource': Token.RESOURCE,
65+
'variables': Token.VARIABLES
66+
}
67+
)
68+
self.settings_order = self.parse_order_in_group(
69+
settings_order,
70+
(
71+
Token.SUITE_SETUP,
72+
Token.SUITE_TEARDOWN,
73+
Token.TEST_SETUP,
74+
Token.TEST_TEARDOWN,
75+
Token.TEST_TIMEOUT,
76+
Token.TEST_TEMPLATE
77+
),
78+
{
79+
'suite_setup': Token.SUITE_SETUP,
80+
'suite_teardown': Token.SUITE_TEARDOWN,
81+
'test_setup': Token.TEST_SETUP,
82+
'test_teardown': Token.TEST_TEARDOWN,
83+
'test_timeout': Token.TEST_TIMEOUT,
84+
'test_template': Token.TEST_TEMPLATE
85+
}
86+
)
87+
self.tags_order = self.parse_order_in_group(
88+
tags_order,
89+
(
90+
Token.FORCE_TAGS,
91+
Token.DEFAULT_TAGS
92+
),
93+
{
94+
'force_tags': Token.FORCE_TAGS,
95+
'default_tags': Token.DEFAULT_TAGS
96+
}
97+
)
98+
99+
@staticmethod
100+
def parse_group_order(order):
101+
default = (
102+
'documentation',
103+
'imports',
104+
'settings',
105+
'tags'
106+
)
107+
if order is None:
108+
return default
109+
if not order:
110+
return []
111+
splitted = order.lower().split(',')
112+
if any(split not in default for split in splitted):
113+
raise click.BadOptionUsage(
114+
option_name='transform',
115+
message=f"Invalid configurable value: '{order}' for group_order for OrderSettingsSection transformer."
116+
f" Custom order should be provided in comma separated list with valid group names:\n{default}"
117+
)
118+
return splitted
119+
120+
@staticmethod
121+
def parse_order_in_group(order, default, mapping):
122+
if order is None:
123+
return default
124+
if not order:
125+
return []
126+
splitted = order.lower().split(',')
127+
try:
128+
return [mapping[split] for split in splitted]
129+
except KeyError:
130+
raise click.BadOptionUsage(
131+
option_name='transform',
132+
message=f"Invalid configurable value: '{order}' for order for OrderSettingsSection transformer."
133+
f" Custom order should be provided in comma separated list with valid group names:\n"
134+
f"{sorted(mapping.keys())}")
135+
136+
def visit_File(self, node): # noqa
137+
self.last_section = node.sections[-1] if node.sections else None
138+
return self.generic_visit(node)
139+
140+
def visit_SettingSection(self, node): # noqa
141+
if not node.body:
142+
return
143+
if node is self.last_section and not isinstance(node.body[-1], EmptyLine):
144+
node.body[-1] = self.fix_eol(node.body[-1])
145+
comments, errors = [], []
146+
groups = defaultdict(list)
147+
for child in node.body:
148+
child_type = getattr(child, 'type', None)
149+
if isinstance(child, Comment):
150+
comments.append(child)
151+
elif child_type in self.documentation_order:
152+
groups['documentation'].append((comments, child))
153+
comments = []
154+
elif child_type in self.imports_order:
155+
groups['imports'].append((comments, child))
156+
comments = []
157+
elif child_type in self.settings_order:
158+
groups['settings'].append((comments, child))
159+
comments = []
160+
elif child_type in self.tags_order:
161+
groups['tags'].append((comments, child))
162+
comments = []
163+
elif not isinstance(child, EmptyLine):
164+
errors.append(child)
165+
166+
group_map = {
167+
'documentation': self.documentation_order,
168+
'imports': self.imports_order,
169+
'settings': self.settings_order,
170+
'tags': self.tags_order
171+
}
172+
173+
new_body = []
174+
empty_line = EmptyLine.from_params(eol='\n')
175+
order_of_groups = [group for group in self.group_order if group in groups]
176+
last_index = len(order_of_groups) - 1
177+
for index, group in enumerate(order_of_groups):
178+
unordered = groups[group]
179+
if group == 'imports':
180+
unordered = self.sort_builtin_libs(unordered)
181+
order = group_map[group]
182+
for token_type in order:
183+
for comment_lines, child in unordered:
184+
if child.type == token_type:
185+
new_body.extend(comment_lines)
186+
new_body.append(child)
187+
if index != last_index:
188+
new_body.extend([empty_line] * self.new_lines_between_groups)
189+
190+
# not recognized headers, parsing errors like Resources instead of Resource
191+
if errors:
192+
new_body.extend([empty_line] * self.new_lines_between_groups)
193+
new_body.extend(errors)
194+
new_body.extend(comments)
195+
if node is not self.last_section:
196+
new_body.append(empty_line)
197+
node.body = new_body
198+
return node
199+
200+
@staticmethod
201+
def fix_eol(node):
202+
if not getattr(node, 'tokens', None):
203+
return node
204+
if getattr(node.tokens[-1], 'type', None) != Token.EOL:
205+
return node
206+
node.tokens = list(node.tokens[:-1]) + [Token(Token.EOL, '\n')]
207+
return node
208+
209+
@staticmethod
210+
def sort_builtin_libs(statements):
211+
before, after = [], []
212+
for comments, statement in statements:
213+
if isinstance(statement, LibraryImport) and statement.name and statement.name in STDLIBS:
214+
before.append((comments, statement))
215+
else:
216+
after.append((comments, statement))
217+
before = sorted(before, key=lambda x: x[1].name)
218+
return before + after

robotidy/transformers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
'RemoveEmptySettings',
2020
'AssignmentNormalizer',
2121
'OrderSettings',
22+
'OrderSettingsSection',
2223
'AlignSettingsSection',
2324
'AlignVariablesSection',
2425
'NormalizeNewLines',
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
# this is comment section
3+
*** Settings ***
4+
Library somelib.py
5+
Test Template Template
6+
7+
8+
Task Timeout 4min
9+
10+
Force Tags sometag othertag
11+
*** Variables *** this should be left alone
12+
${var} 1
13+
@{var2} 1
14+
... 2
15+
16+
17+
*** Test Cases ***
18+
Test 1
19+
Log 1
20+
21+
Test 2
22+
Log 2
23+
24+
Test 3
25+
Log 3
26+
27+
28+
*** Keywords ***
29+
Keyword
30+
No Operation
31+
32+
Keyword2
33+
Log 2
34+
FOR ${i} IN RANGE 10
35+
Log ${i}
36+
END
37+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*** Settings ***
2+
# comment
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*** Settings ***
2+
Library library.py
3+
Resource resource.robot
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
*** Settings ***
2+
Documentation doc # this is comment
3+
... another line
4+
Metadata value param
5+
6+
Suite Setup Keyword
7+
# We all
8+
# are commenting Suite Teardown
9+
Suite Teardown Keyword2
10+
# i want to be keep together with Test Setup
11+
Test Setup Keyword
12+
Test Timeout 1min
13+
14+
*** Keywords ***
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*** Settings ***
2+
Library library.py
3+
4+
Resources resource.robot

0 commit comments

Comments
 (0)