|
| 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 |
0 commit comments