Skip to content

Commit 7ad9890

Browse files
authored
add API (#99)
1 parent e052f15 commit 7ad9890

File tree

6 files changed

+157
-51
lines changed

6 files changed

+157
-51
lines changed

robotidy/api.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Methods for transforming Robot Framework ast model programmatically.
3+
"""
4+
from typing import Optional
5+
6+
from robotidy.app import Robotidy
7+
from robotidy.cli import find_and_read_config, TransformType
8+
from robotidy.utils import GlobalFormattingConfig
9+
10+
11+
class RobotidyAPI(Robotidy):
12+
def __init__(self, src: str, **kwargs):
13+
config = find_and_read_config([src])
14+
config = {
15+
k: str(v) if not isinstance(v, (list, dict)) else v
16+
for k, v in config.items()
17+
}
18+
converter = TransformType()
19+
transformers = [converter.convert(tr, None, None) for tr in config.get('transform', ())]
20+
configurations = [converter.convert(c, None, None) for c in config.get('configure', ())]
21+
formatting_config = GlobalFormattingConfig(
22+
space_count=kwargs.get('spacecount', None) or int(config.get('spacecount', 4)),
23+
line_sep=config.get('lineseparator', 'native'),
24+
start_line=kwargs.get('startline', None) or int(config['startline']) if 'startline' in config else None,
25+
end_line=kwargs.get('endline', None) or int(config['endline']) if 'endline' in config else None
26+
)
27+
super().__init__(
28+
transformers=transformers,
29+
transformers_config=configurations,
30+
src=(),
31+
overwrite=False,
32+
show_diff=False,
33+
formatting_config=formatting_config,
34+
verbose=False,
35+
check=False
36+
)
37+
38+
39+
def transform_model(model, root_dir: str, **kwargs) -> Optional[str]:
40+
"""
41+
:param model: The model to be transformed.
42+
:param root_dir: Root directory. Configuration file is searched based
43+
on this directory or one of its parents.
44+
:param kwargs: Default values for global formatting parameters
45+
such as ``spacecount``, ``startline`` and ``endline``.
46+
:return: The transformed model converted to string or None if no transformation took place.
47+
"""
48+
transformer = RobotidyAPI(root_dir, **kwargs)
49+
diff, _, new_model = transformer.transform(model)
50+
if not diff:
51+
return None
52+
return new_model.text

robotidy/app.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from typing import List, Tuple, Dict, Set, Any
1+
from collections import defaultdict
22
from difflib import unified_diff
3+
from pathlib import Path
4+
from typing import List, Tuple, Dict, Iterator, Iterable
35

46
import click
57
from robot.api import get_model
@@ -12,25 +14,31 @@
1214
GlobalFormattingConfig
1315
)
1416

17+
INCLUDE_EXT = ('.robot', '.resource')
18+
1519

1620
class Robotidy:
1721
def __init__(self,
1822
transformers: List[Tuple[str, List]],
19-
transformers_config: Dict[str, List],
20-
src: Set,
23+
transformers_config: List[Tuple[str, List]],
24+
src: Tuple[str, ...],
2125
overwrite: bool,
2226
show_diff: bool,
2327
formatting_config: GlobalFormattingConfig,
2428
verbose: bool,
2529
check: bool
2630
):
27-
self.sources = src
31+
self.sources = self.get_paths(src)
2832
self.overwrite = overwrite
2933
self.show_diff = show_diff
3034
self.check = check
3135
self.verbose = verbose
3236
self.formatting_config = formatting_config
37+
transformers_config = self.convert_configure(transformers_config)
3338
self.transformers = load_transformers(transformers, transformers_config)
39+
for transformer in self.transformers:
40+
# inject global settings TODO: handle it better
41+
setattr(transformer, 'formatting_config', self.formatting_config)
3442

3543
def transform_files(self):
3644
changed_files = 0
@@ -39,13 +47,8 @@ def transform_files(self):
3947
if self.verbose:
4048
click.echo(f'Transforming {source} file')
4149
model = get_model(source)
42-
old_model = StatementLinesCollector(model)
43-
for transformer in self.transformers:
44-
# inject global settings TODO: handle it better
45-
setattr(transformer, 'formatting_config', self.formatting_config)
46-
transformer.visit(model)
47-
new_model = StatementLinesCollector(model)
48-
if new_model != old_model:
50+
diff, old_model, new_model = self.transform(model)
51+
if diff:
4952
changed_files += 1
5053
self.output_diff(model.source, old_model, new_model)
5154
if not self.check:
@@ -59,6 +62,13 @@ def transform_files(self):
5962
return 0
6063
return 1
6164

65+
def transform(self, model):
66+
old_model = StatementLinesCollector(model)
67+
for transformer in self.transformers:
68+
transformer.visit(model)
69+
new_model = StatementLinesCollector(model)
70+
return new_model != old_model, old_model, new_model
71+
6272
def save_model(self, model):
6373
if self.overwrite:
6474
model.save()
@@ -71,3 +81,32 @@ def output_diff(self, path: str, old_model: StatementLinesCollector, new_model:
7181
lines = list(unified_diff(old, new, fromfile=f'{path}\tbefore', tofile=f'{path}\tafter'))
7282
colorized_output = decorate_diff_with_color(lines)
7383
click.echo(colorized_output.encode('ascii', 'ignore').decode('ascii'), color=True)
84+
85+
def get_paths(self, src: Tuple[str, ...]):
86+
sources = set()
87+
for s in src:
88+
path = Path(s).resolve()
89+
if path.is_file():
90+
sources.add(path)
91+
elif path.is_dir():
92+
sources.update(self.iterate_dir(path.iterdir()))
93+
elif s == '-':
94+
sources.add(path)
95+
96+
return sources
97+
98+
def iterate_dir(self, paths: Iterable[Path]) -> Iterator[Path]:
99+
for path in paths:
100+
if path.is_file():
101+
if path.suffix not in INCLUDE_EXT:
102+
continue
103+
yield path
104+
elif path.is_dir():
105+
yield from self.iterate_dir(path.iterdir())
106+
107+
@staticmethod
108+
def convert_configure(configure: List[Tuple[str, List]]) -> Dict[str, List]:
109+
config_map = defaultdict(list)
110+
for transformer, args in configure:
111+
config_map[transformer].extend(args)
112+
return config_map

robotidy/cli.py

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
1+
from pathlib import Path
12
from typing import (
23
Tuple,
34
Dict,
45
List,
5-
Iterator,
66
Iterable,
77
Optional,
88
Any
99
)
10-
from pathlib import Path
10+
1111
import click
1212
import toml
13-
from collections import defaultdict
1413

15-
from robotidy.version import __version__
1614
from robotidy.app import Robotidy
1715
from robotidy.transformers import load_transformers
1816
from robotidy.utils import (
1917
GlobalFormattingConfig,
2018
split_args_from_name_or_path,
2119
remove_rst_formatting
2220
)
21+
from robotidy.version import __version__
2322

2423

25-
INCLUDE_EXT = ('.robot', '.resource')
2624
HELP_MSG = f"""
2725
Version: {__version__}
2826
@@ -81,13 +79,6 @@ def convert(self, value, param, ctx):
8179
return name, args
8280

8381

84-
def convert_configure(configure: List[Tuple[str, List]]) -> Dict[str, List]:
85-
config_map = defaultdict(list)
86-
for transformer, args in configure:
87-
config_map[transformer].extend(args)
88-
return config_map
89-
90-
9182
def find_project_root(srcs: Iterable[str]) -> Path:
9283
"""Return a directory containing .git, or robotidy.toml.
9384
That directory will be a common parent of all files and directories
@@ -132,6 +123,7 @@ def find_and_read_config(src_paths: Iterable[str]) -> Dict[str, Any]:
132123
pyproject_path = project_root / 'pyproject.toml'
133124
if pyproject_path.is_file():
134125
return read_pyproject_config(str(pyproject_path))
126+
return {}
135127

136128

137129
def load_toml_file(path: str) -> Dict[str, Any]:
@@ -180,30 +172,6 @@ def read_config(ctx: click.Context, param: click.Parameter, value: Optional[str]
180172
ctx.default_map = default_map
181173

182174

183-
def iterate_dir(paths: Iterable[Path]) -> Iterator[Path]:
184-
for path in paths:
185-
if path.is_file():
186-
if path.suffix not in INCLUDE_EXT:
187-
continue
188-
yield path
189-
elif path.is_dir():
190-
yield from iterate_dir(path.iterdir())
191-
192-
193-
def get_paths(src: Tuple[str, ...]):
194-
sources = set()
195-
for s in src:
196-
path = Path(s).resolve()
197-
if path.is_file():
198-
sources.add(path)
199-
elif path.is_dir():
200-
sources.update(iterate_dir(path.iterdir()))
201-
elif s == '-':
202-
sources.add(path)
203-
204-
return sources
205-
206-
207175
@click.command(cls=RawHelp, help=HELP_MSG, epilog=EPILOG)
208176
@click.option(
209177
'--transform',
@@ -366,12 +334,10 @@ def cli(
366334
start_line=startline,
367335
end_line=endline
368336
)
369-
sources = get_paths(src)
370-
configure_transform = convert_configure(configure)
371337
tidy = Robotidy(
372338
transformers=transform,
373-
transformers_config=configure_transform,
374-
src=sources,
339+
transformers_config=configure,
340+
src=src,
375341
overwrite=overwrite,
376342
show_diff=diff,
377343
formatting_config=formatting_config,

tests/utest/test_api.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pathlib import Path
2+
3+
from robot.api import get_model
4+
5+
from robotidy.api import transform_model
6+
7+
8+
class TestAPI:
9+
def test_load_pyproject_and_transform(self):
10+
expected = "*** Settings ***\n" \
11+
"\n\n" \
12+
"*** Test Cases ***\n" \
13+
"Test\n" \
14+
" [Documentation] doc\n" \
15+
" [Tags] sometag\n" \
16+
" Pass\n" \
17+
" Keyword\n" \
18+
" One More\n" \
19+
"\n\n" \
20+
"*** Comments ***\n" \
21+
"robocop: disable=all"
22+
config_path = str(Path(Path(__file__).parent, 'testdata', 'only_pyproject'))
23+
source = str(Path(Path(__file__).parent.parent, 'atest', 'transformers', 'DiscardEmptySections', 'source',
24+
'removes_empty_sections.robot'))
25+
model = get_model(source)
26+
transformed = transform_model(model, config_path)
27+
assert transformed == expected
28+
29+
def test_with_default_parameters(self):
30+
expected = "*** Comments ***\n" \
31+
"robocop: disable=all\n" \
32+
"\n" \
33+
"*** Test Cases ***\n" \
34+
"Test\n" \
35+
" [Documentation] doc\n" \
36+
" [Tags] sometag\n" \
37+
" Pass\n" \
38+
" Keyword\n" \
39+
" One More\n"
40+
41+
config_path = '.'
42+
source = str(Path(Path(__file__).parent.parent, 'atest', 'transformers', 'DiscardEmptySections', 'source',
43+
'removes_empty_sections.robot'))
44+
model = get_model(source)
45+
transformed = transform_model(model, config_path, spacecount=8, linestart=10, endline=20)
46+
assert transformed == expected

tests/utest/test_cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def test_read_pyproject_config(self):
8787
'overwrite': False,
8888
'diff': False,
8989
'startline': 10,
90+
'endline': 20,
9091
'transform': [
9192
'DiscardEmptySections:allow_only_comments=True',
9293
'SplitTooLongLine'
@@ -104,6 +105,7 @@ def test_read_pyproject_config_e2e(self):
104105
'overwrite': 'False',
105106
'diff': 'False',
106107
'startline': '10',
108+
'endline': '20',
107109
'transform': [
108110
'DiscardEmptySections:allow_only_comments=True',
109111
'SplitTooLongLine'

tests/utest/testdata/only_pyproject/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
overwrite = false
33
diff = false
44
startline = 10
5+
endline = 20
56
transform = [
67
"DiscardEmptySections:allow_only_comments=True",
78
"SplitTooLongLine"

0 commit comments

Comments
 (0)