Skip to content

Commit 2dc2931

Browse files
authored
Load custom transformer on top of default ones (#480)
1 parent 0173ef5 commit 2dc2931

File tree

16 files changed

+240
-60
lines changed

16 files changed

+240
-60
lines changed

docs/releasenotes/3.6.0.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,25 @@ although in vast majority of cases one extra run should suffice (and only in cas
2626

2727
Example usage::
2828

29-
robotidy --reruns 3 --diff test.robot
29+
> robotidy --reruns 3 --diff test.robot
3030

3131
Note that if you enable it, it can double the execution time of Robotidy (if the file was modified, it will be
3232
transformed again to check if next transformation does not further modify the file). It should be not a problem because
3333
Robotidy is fast enough but report any issues with this feature.
34+
35+
Load custom transformers together with defaults
36+
------------------------------------------------
37+
38+
Previously Robotidy only supported importing custom transformers with ``--transform`` option. This option disables
39+
any other transformer not listed with ``--transform``. That's why if user would run following::
40+
41+
> robotidy --transform MyCustomClass.py test.robot
42+
43+
It would disable all default transformers and only run MyCustomClass.
44+
This release introduces new option ``--load-transformer`` which imports custom transformers on top of the default ones::
45+
46+
> robotidy --load-transformer MyCustomClass.py test.robot
47+
48+
It is also possible to pass transformer configuration either using this option or through ``--configure``::
49+
50+
> robotidy -c ExtClass1.py:param=value --load-transformer ExtClass2.py:param2=value test.robot

docs/source/configuration/configuring_transformers.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@
33
Configuring Transformers
44
========================
55

6-
Transformers can be configured through two different options: ``--transform`` (``-t``) and ``--configure`` (``-c``). They share the same
7-
syntax for parameter names and values. The main difference is that ``--transform`` is also used to select what
8-
transformers will be used. For example::
6+
Transformers can be configured through three different options: ``--transform`` (``-t``), ``--load-transformer`` and
7+
``--configure`` (``-c``). They share the same syntax for parameter names and values.
8+
9+
- ``--configure`` simply provides the configuration to the transformer,
10+
- ``--transform`` is used to include only transformers that have ``--transform`` option,
11+
- ``--load-transformer`` is used to load user transformers. Read more at :ref:`external-transformers`.
12+
13+
For example::
914

1015
robotidy --transform NormalizeNewLines:test_case_lines=2 src
1116
robotidy --configure NormalizeNewLines:test_case_lines=2 src
17+
robotidy --configure NormalizeNewLines:test_case_lines=1 --load-transformer MyCustomTransformer.py:param=value src
1218

1319
With first command robotidy will run only ``NormalizeNewLines`` transformer and it will configure it with ``test_case_lines = 2``.
20+
1421
Second command robotidy will run all of the transformers and will configure ``NormalizeNewLines`` with ``test_case_lines = 2``.
1522

23+
Third command will run all of the transformers, configure ``NormalizeNewLines`` with ``test_case_lines = 1`` and
24+
import user transformer ``MyCustomTransformer`` with `param=value` configuration.
25+
1626
You can also run all transformers except selected ones. For that you need to configure transformer you want to exclude
1727
with ``enabled`` parameter::
1828

docs/source/external_transformers.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ file with class to run external transformers with *robotidy*::
77

88
robotidy --transform MyTransformers.YourCustomTransformer src
99
robotidy --transform C:\transformers\YourCustomTransformer2.py src
10+
robotidy --load-transformer C:\transformers\YourCustomTransformer2.py src
1011

1112
External transformers can be configured in the same way internal transformers are configured - see :ref:`configuring-transformers`.
1213

14+
You can use both ``--transform`` and ``--load-transformer`` options to load custom user transformer. The main difference
15+
is that ``--transform`` works like include and will only run transformers listed with ``--transform``. While ``--load-transformer``
16+
will run default transformers first and then user transformers.
17+
1318
You can use the same syntax ``robotidy`` is using for developing internal transformers. The name of the file should
1419
be the same as name of the class containing your transformer. Your transformer should inherit from ``robot.api.parsing.ModelTransformer``
1520
parent class.
@@ -60,10 +65,6 @@ class:
6065
6166
6267
class ExternalTransformer(Transformer):
63-
def __init__(self):
64-
super().__init__()
68+
pass
6569
6670
``Transformer`` also inherits from ``ModelTransformer`` but provides more utility methods (and better lint support).
67-
However because of how we are dynamically loading class arguments from cli/config we need to make a call to
68-
``super().__init__()`` even if our class don't have any arguments to set. If you're unsure what to use - use
69-
``ModelTransformer``.

robotidy/api.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ def get_formatting_config(config, kwargs):
5454

5555

5656
def get_robotidy(src: str, output: Optional[str], **kwargs):
57+
def convert_transformers_config(param_name, config):
58+
return [converter.convert(tr, None, None) for tr in config.get(param_name, ())]
59+
5760
# TODO Refactor - Config should be read in one place both for API and CLI
5861
# TODO Remove kwargs usage - other SDKs are not using this feature
5962
config = files.find_and_read_config((src,))
6063
config = {k: str(v) if not isinstance(v, (list, dict)) else v for k, v in config.items()}
6164
converter = transformers.TransformType()
62-
transformer_list = [converter.convert(tr, None, None) for tr in config.get("transform", ())]
63-
configurations = [converter.convert(c, None, None) for c in config.get("configure", ())]
65+
transformer_list = convert_transformers_config("transform", config)
66+
custom_transformers = convert_transformers_config("load-transformers", config)
67+
configurations = convert_transformers_config("configure", config)
6468
formatting_config = get_formatting_config(config, kwargs)
6569
exclude = config.get("exclude", None)
6670
extend_exclude = config.get("extend_exclude", None)
@@ -71,6 +75,7 @@ def get_robotidy(src: str, output: Optional[str], **kwargs):
7175
language = config.get("language", None)
7276
configuration = Config(
7377
transformers=transformer_list,
78+
custom_transformers=custom_transformers,
7479
transformers_config=configurations,
7580
skip=global_skip,
7681
src=(),

robotidy/cli.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"name": "Run only selected transformers",
2424
"options": ["--transform"],
2525
},
26+
{
27+
"name": "Load custom transformers",
28+
"options": ["--load-transformers"],
29+
},
2630
{
2731
"name": "Work modes",
2832
"options": ["--overwrite", "--diff", "--check", "--force-order"],
@@ -165,7 +169,8 @@ def print_transformer_docs(transformer):
165169

166170
@decorators.optional_rich
167171
def print_description(name: str, target_version: int):
168-
transformers = load_transformers(None, {}, allow_disabled=True, target_version=target_version)
172+
# TODO: --desc works only for default transformers, it should also print custom transformer desc
173+
transformers = load_transformers([], [], {}, allow_disabled=True, target_version=target_version)
169174
transformer_by_names = {transformer.name: transformer for transformer in transformers}
170175
if name == "all":
171176
for transformer in transformers:
@@ -182,6 +187,7 @@ def print_description(name: str, target_version: int):
182187

183188
def _load_external_transformers(
184189
transformers: List,
190+
custom_transformers: List[Tuple[str, List]],
185191
transformers_from_config: List[Tuple[str, List]],
186192
transformer_config: List[Tuple[str, List]],
187193
target_version: int,
@@ -190,7 +196,7 @@ def _load_external_transformers(
190196
transformers_names = {transformer.name for transformer in transformers}
191197
transformer_config_converted = Config.convert_configure(transformer_config)
192198
transformers_from_conf = load_transformers(
193-
transformers_from_config, transformer_config_converted, target_version=target_version
199+
transformers_from_config, custom_transformers, transformer_config_converted, target_version=target_version
194200
)
195201
for transformer in transformers_from_conf:
196202
if transformer.name not in transformers_names:
@@ -201,6 +207,7 @@ def _load_external_transformers(
201207
@decorators.optional_rich
202208
def print_transformers_list(
203209
transformers_from_config: List[Tuple[str, List]],
210+
custom_transformers: List[Tuple[str, List]],
204211
transformer_config: List[Tuple[str, List]],
205212
config: Config,
206213
target_version: int,
@@ -211,9 +218,11 @@ def print_transformers_list(
211218
table = Table(title="Transformers", header_style="bold red")
212219
table.add_column("Name", justify="left", no_wrap=True)
213220
table.add_column("Enabled")
214-
transformers = load_transformers(None, {}, allow_disabled=True, target_version=target_version)
221+
transformers = load_transformers([], [], {}, allow_disabled=True, target_version=target_version)
215222
transformers.extend(
216-
_load_external_transformers(transformers, transformers_from_config, transformer_config, target_version)
223+
_load_external_transformers(
224+
transformers, custom_transformers, transformers_from_config, transformer_config, target_version
225+
)
217226
)
218227

219228
for transformer in transformers:
@@ -248,7 +257,15 @@ def print_transformers_list(
248257
type=TransformType(),
249258
multiple=True,
250259
metavar="TRANSFORMER_NAME",
251-
help="Transform files from \[PATH(S)] with given transformer",
260+
help="Transform files from [PATH(S)] with given transformer",
261+
)
262+
@click.option(
263+
"--load-transformers",
264+
"custom_transformers",
265+
type=TransformType(),
266+
multiple=True,
267+
metavar="TRANSFORMER_NAME",
268+
help="Load custom transformer from the path and run them after default ones.",
252269
)
253270
@click.option(
254271
"--configure",
@@ -474,6 +491,7 @@ def print_transformers_list(
474491
def cli(
475492
ctx: click.Context,
476493
transform: List[Tuple[str, List]],
494+
custom_transformers: List[Tuple[str, List]],
477495
configure: List[Tuple[str, List]],
478496
src: Tuple[str, ...],
479497
exclude: Optional[Pattern],
@@ -566,10 +584,13 @@ def cli(
566584
end_line=endline,
567585
line_length=line_length,
568586
)
587+
# array of some class, that holds info if it's forced, configure or custom
588+
# or just name:args TODO
569589
config = Config(
570590
formatting=formatting,
571591
skip=skip_config,
572592
transformers=transform,
593+
custom_transformers=custom_transformers,
573594
transformers_config=configure,
574595
src=src,
575596
exclude=exclude,
@@ -588,7 +609,7 @@ def cli(
588609
)
589610

590611
if list_transformers:
591-
print_transformers_list(transform, configure, config, target_version, list_transformers)
612+
print_transformers_list(transform, custom_transformers, configure, config, target_version, list_transformers)
592613
sys.exit(0)
593614
if desc is not None:
594615
return_code = print_description(desc, target_version)

robotidy/config.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __init__(
6363
formatting: FormattingConfig,
6464
skip,
6565
transformers: List[Tuple[str, List]],
66+
custom_transformers: List[Tuple[str, List]],
6667
transformers_config: List[Tuple[str, List]],
6768
src: Tuple[str, ...],
6869
exclude: Optional[Pattern],
@@ -92,11 +93,20 @@ def __init__(
9293
transformers_config = self.convert_configure(transformers_config)
9394
self.transformers = []
9495
self.transformers_lookup = dict()
95-
self.load_transformers(transformers, transformers_config, force_order, target_version, skip)
96+
self.load_transformers(
97+
transformers, custom_transformers, transformers_config, force_order, target_version, skip
98+
)
9699

97-
def load_transformers(self, transformers, transformers_config, force_order, target_version, skip):
100+
def load_transformers(
101+
self, transformers, custom_transformers, transformers_config, force_order, target_version, skip
102+
):
98103
transformers = load_transformers(
99-
transformers, transformers_config, force_order=force_order, target_version=target_version, skip=skip
104+
transformers,
105+
custom_transformers,
106+
transformers_config,
107+
force_order=force_order,
108+
target_version=target_version,
109+
skip=skip,
100110
)
101111
for transformer in transformers:
102112
# inject global settings TODO: handle it better

robotidy/transformers/__init__.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -325,42 +325,56 @@ def assert_not_duplicated_transform(allowed_mapped, allowed_transformers):
325325

326326

327327
def load_transformers(
328-
allowed_transformers,
328+
selected_transformers,
329+
custom_transformers,
329330
config,
330331
target_version,
331332
skip=None,
332333
allow_disabled=False,
333334
force_order=False,
334335
allow_version_mismatch=True,
335336
):
336-
"""Dynamically load all classes from this file with attribute `name` defined in allowed_transformers"""
337+
"""Dynamically load all classes from this file with attribute `name` defined in selected_transformers"""
337338
loaded_transformers = []
338-
allowed_mapped = {name: args for name, args in allowed_transformers} if allowed_transformers else {}
339-
assert_not_duplicated_transform(allowed_mapped, allowed_transformers)
339+
allowed_mapped = {name: args for name, args in selected_transformers}
340+
custom_mapped = {name: args for name, args in custom_transformers}
341+
assert_not_duplicated_transform(allowed_mapped, selected_transformers)
340342
validate_config(config, allowed_mapped)
341343
if not force_order:
342344
for name in TRANSFORMERS:
345+
# load all default ones if allowed_mapped is not defined, or only those listed in allowed_mapped
343346
if not allowed_mapped or name in allowed_mapped:
344347
args = get_args(name, allowed_mapped, config)
345348
container = load_transformer(name, args, skip)
346349
if container is None:
347350
continue
348351
enabled = getattr(container.instance, "ENABLED", True) or args.get("enabled", False)
349-
if allowed_mapped or allow_disabled or enabled:
350-
overwritten = name in allowed_mapped or args.get("enabled", False)
351-
if can_run_in_robot_version(
352-
container.instance, overwritten=overwritten, target_version=target_version
353-
):
354-
loaded_transformers.append(container)
355-
elif allow_version_mismatch and allow_disabled:
356-
setattr(container.instance, "ENABLED", False)
357-
container.enabled_by_default = False
358-
loaded_transformers.append(container)
352+
if not (allowed_mapped or allow_disabled or enabled):
353+
continue
354+
overwritten = name in allowed_mapped or args.get("enabled", False)
355+
if can_run_in_robot_version(container.instance, overwritten=overwritten, target_version=target_version):
356+
loaded_transformers.append(container)
357+
elif allow_version_mismatch and allow_disabled:
358+
setattr(container.instance, "ENABLED", False)
359+
container.enabled_by_default = False
360+
loaded_transformers.append(container)
359361
for name in allowed_mapped:
360362
if force_order or name not in TRANSFORMERS:
361363
args = get_args(name, allowed_mapped, config)
362364
container = load_transformer(name, args, skip)
363-
if container is not None:
364-
if can_run_in_robot_version(container.instance, overwritten=True, target_version=target_version):
365-
loaded_transformers.append(container)
365+
if container is None:
366+
continue
367+
if can_run_in_robot_version(container.instance, overwritten=True, target_version=target_version):
368+
loaded_transformers.append(container)
369+
for name in custom_mapped:
370+
args = get_args(name, custom_mapped, config)
371+
container = load_transformer(name, args, skip)
372+
if container is None:
373+
continue
374+
enabled = getattr(container.instance, "ENABLED", True) or args.get("enabled", False)
375+
if not (allow_disabled or enabled):
376+
continue
377+
overwritten = args.get("enabled", False)
378+
if can_run_in_robot_version(container.instance, overwritten=overwritten, target_version=target_version):
379+
loaded_transformers.append(container)
366380
return loaded_transformers
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from robotidy.transformers import Transformer
2+
3+
4+
class ExternalDisabledTransformer(Transformer):
5+
"""
6+
This transformer is disabled by default. If it is enabled, it replaces setting names to lowercase.
7+
"""
8+
9+
ENABLED = False
10+
11+
def visit_SectionHeader(self, node): # noqa
12+
if not node.name:
13+
return node
14+
node.data_tokens[0].value = node.data_tokens[0].value.lower()
15+
return node
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*** settings ***
2+
Library KeywordLibrary
3+
4+
5+
*** test cases
6+
Test
7+
Pass
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*** Settings ***
2+
Library KeywordLibrary
3+
4+
5+
*** Test Cases ***
6+
Test
7+
Pass

0 commit comments

Comments
 (0)