From 5701e96db8de930cb9010f613d3aeb7fd5c8819b Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Mon, 7 Jun 2021 09:23:35 +0200 Subject: [PATCH] add API --- robotidy/api.py | 52 ++++++++++++++++ robotidy/app.py | 61 +++++++++++++++---- robotidy/cli.py | 46 ++------------ tests/utest/test_api.py | 46 ++++++++++++++ tests/utest/test_cli.py | 2 + .../testdata/only_pyproject/pyproject.toml | 1 + 6 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 robotidy/api.py create mode 100644 tests/utest/test_api.py diff --git a/robotidy/api.py b/robotidy/api.py new file mode 100644 index 00000000..a2587511 --- /dev/null +++ b/robotidy/api.py @@ -0,0 +1,52 @@ +""" +Methods for transforming Robot Framework ast model programmatically. +""" +from typing import Optional + +from robotidy.app import Robotidy +from robotidy.cli import find_and_read_config, TransformType +from robotidy.utils import GlobalFormattingConfig + + +class RobotidyAPI(Robotidy): + def __init__(self, src: str, **kwargs): + config = find_and_read_config([src]) + config = { + k: str(v) if not isinstance(v, (list, dict)) else v + for k, v in config.items() + } + converter = TransformType() + transformers = [converter.convert(tr, None, None) for tr in config.get('transform', ())] + configurations = [converter.convert(c, None, None) for c in config.get('configure', ())] + formatting_config = GlobalFormattingConfig( + space_count=kwargs.get('spacecount', None) or int(config.get('spacecount', 4)), + line_sep=config.get('lineseparator', 'native'), + start_line=kwargs.get('startline', None) or int(config['startline']) if 'startline' in config else None, + end_line=kwargs.get('endline', None) or int(config['endline']) if 'endline' in config else None + ) + super().__init__( + transformers=transformers, + transformers_config=configurations, + src=(), + overwrite=False, + show_diff=False, + formatting_config=formatting_config, + verbose=False, + check=False + ) + + +def transform_model(model, root_dir: str, **kwargs) -> Optional[str]: + """ + :param model: The model to be transformed. + :param root_dir: Root directory. Configuration file is searched based + on this directory or one of its parents. + :param kwargs: Default values for global formatting parameters + such as ``spacecount``, ``startline`` and ``endline``. + :return: The transformed model converted to string or None if no transformation took place. + """ + transformer = RobotidyAPI(root_dir, **kwargs) + diff, _, new_model = transformer.transform(model) + if not diff: + return None + return new_model.text diff --git a/robotidy/app.py b/robotidy/app.py index 9d40662a..213f5317 100644 --- a/robotidy/app.py +++ b/robotidy/app.py @@ -1,5 +1,7 @@ -from typing import List, Tuple, Dict, Set, Any +from collections import defaultdict from difflib import unified_diff +from pathlib import Path +from typing import List, Tuple, Dict, Iterator, Iterable import click from robot.api import get_model @@ -12,25 +14,31 @@ GlobalFormattingConfig ) +INCLUDE_EXT = ('.robot', '.resource') + class Robotidy: def __init__(self, transformers: List[Tuple[str, List]], - transformers_config: Dict[str, List], - src: Set, + transformers_config: List[Tuple[str, List]], + src: Tuple[str, ...], overwrite: bool, show_diff: bool, formatting_config: GlobalFormattingConfig, verbose: bool, check: bool ): - self.sources = src + self.sources = self.get_paths(src) self.overwrite = overwrite self.show_diff = show_diff self.check = check self.verbose = verbose self.formatting_config = formatting_config + transformers_config = self.convert_configure(transformers_config) self.transformers = load_transformers(transformers, transformers_config) + for transformer in self.transformers: + # inject global settings TODO: handle it better + setattr(transformer, 'formatting_config', self.formatting_config) def transform_files(self): changed_files = 0 @@ -39,13 +47,8 @@ def transform_files(self): if self.verbose: click.echo(f'Transforming {source} file') model = get_model(source) - old_model = StatementLinesCollector(model) - for transformer in self.transformers: - # inject global settings TODO: handle it better - setattr(transformer, 'formatting_config', self.formatting_config) - transformer.visit(model) - new_model = StatementLinesCollector(model) - if new_model != old_model: + diff, old_model, new_model = self.transform(model) + if diff: changed_files += 1 self.output_diff(model.source, old_model, new_model) if not self.check: @@ -59,6 +62,13 @@ def transform_files(self): return 0 return 1 + def transform(self, model): + old_model = StatementLinesCollector(model) + for transformer in self.transformers: + transformer.visit(model) + new_model = StatementLinesCollector(model) + return new_model != old_model, old_model, new_model + def save_model(self, model): if self.overwrite: model.save() @@ -71,3 +81,32 @@ def output_diff(self, path: str, old_model: StatementLinesCollector, new_model: lines = list(unified_diff(old, new, fromfile=f'{path}\tbefore', tofile=f'{path}\tafter')) colorized_output = decorate_diff_with_color(lines) click.echo(colorized_output.encode('ascii', 'ignore').decode('ascii'), color=True) + + def get_paths(self, src: Tuple[str, ...]): + sources = set() + for s in src: + path = Path(s).resolve() + if path.is_file(): + sources.add(path) + elif path.is_dir(): + sources.update(self.iterate_dir(path.iterdir())) + elif s == '-': + sources.add(path) + + return sources + + def iterate_dir(self, paths: Iterable[Path]) -> Iterator[Path]: + for path in paths: + if path.is_file(): + if path.suffix not in INCLUDE_EXT: + continue + yield path + elif path.is_dir(): + yield from self.iterate_dir(path.iterdir()) + + @staticmethod + def convert_configure(configure: List[Tuple[str, List]]) -> Dict[str, List]: + config_map = defaultdict(list) + for transformer, args in configure: + config_map[transformer].extend(args) + return config_map diff --git a/robotidy/cli.py b/robotidy/cli.py index f7fc2748..7217ffa8 100644 --- a/robotidy/cli.py +++ b/robotidy/cli.py @@ -1,18 +1,16 @@ +from pathlib import Path from typing import ( Tuple, Dict, List, - Iterator, Iterable, Optional, Any ) -from pathlib import Path + import click import toml -from collections import defaultdict -from robotidy.version import __version__ from robotidy.app import Robotidy from robotidy.transformers import load_transformers from robotidy.utils import ( @@ -20,9 +18,9 @@ split_args_from_name_or_path, remove_rst_formatting ) +from robotidy.version import __version__ -INCLUDE_EXT = ('.robot', '.resource') HELP_MSG = f""" Version: {__version__} @@ -81,13 +79,6 @@ def convert(self, value, param, ctx): return name, args -def convert_configure(configure: List[Tuple[str, List]]) -> Dict[str, List]: - config_map = defaultdict(list) - for transformer, args in configure: - config_map[transformer].extend(args) - return config_map - - def find_project_root(srcs: Iterable[str]) -> Path: """Return a directory containing .git, or robotidy.toml. 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]: pyproject_path = project_root / 'pyproject.toml' if pyproject_path.is_file(): return read_pyproject_config(str(pyproject_path)) + return {} 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] ctx.default_map = default_map -def iterate_dir(paths: Iterable[Path]) -> Iterator[Path]: - for path in paths: - if path.is_file(): - if path.suffix not in INCLUDE_EXT: - continue - yield path - elif path.is_dir(): - yield from iterate_dir(path.iterdir()) - - -def get_paths(src: Tuple[str, ...]): - sources = set() - for s in src: - path = Path(s).resolve() - if path.is_file(): - sources.add(path) - elif path.is_dir(): - sources.update(iterate_dir(path.iterdir())) - elif s == '-': - sources.add(path) - - return sources - - @click.command(cls=RawHelp, help=HELP_MSG, epilog=EPILOG) @click.option( '--transform', @@ -366,12 +334,10 @@ def cli( start_line=startline, end_line=endline ) - sources = get_paths(src) - configure_transform = convert_configure(configure) tidy = Robotidy( transformers=transform, - transformers_config=configure_transform, - src=sources, + transformers_config=configure, + src=src, overwrite=overwrite, show_diff=diff, formatting_config=formatting_config, diff --git a/tests/utest/test_api.py b/tests/utest/test_api.py new file mode 100644 index 00000000..eb4790a5 --- /dev/null +++ b/tests/utest/test_api.py @@ -0,0 +1,46 @@ +from pathlib import Path + +from robot.api import get_model + +from robotidy.api import transform_model + + +class TestAPI: + def test_load_pyproject_and_transform(self): + expected = "*** Settings ***\n" \ + "\n\n" \ + "*** Test Cases ***\n" \ + "Test\n" \ + " [Documentation] doc\n" \ + " [Tags] sometag\n" \ + " Pass\n" \ + " Keyword\n" \ + " One More\n" \ + "\n\n" \ + "*** Comments ***\n" \ + "robocop: disable=all" + config_path = str(Path(Path(__file__).parent, 'testdata', 'only_pyproject')) + source = str(Path(Path(__file__).parent.parent, 'atest', 'transformers', 'DiscardEmptySections', 'source', + 'removes_empty_sections.robot')) + model = get_model(source) + transformed = transform_model(model, config_path) + assert transformed == expected + + def test_with_default_parameters(self): + expected = "*** Comments ***\n" \ + "robocop: disable=all\n" \ + "\n" \ + "*** Test Cases ***\n" \ + "Test\n" \ + " [Documentation] doc\n" \ + " [Tags] sometag\n" \ + " Pass\n" \ + " Keyword\n" \ + " One More\n" + + config_path = '.' + source = str(Path(Path(__file__).parent.parent, 'atest', 'transformers', 'DiscardEmptySections', 'source', + 'removes_empty_sections.robot')) + model = get_model(source) + transformed = transform_model(model, config_path, spacecount=8, linestart=10, endline=20) + assert transformed == expected diff --git a/tests/utest/test_cli.py b/tests/utest/test_cli.py index 5762ced9..fab99255 100644 --- a/tests/utest/test_cli.py +++ b/tests/utest/test_cli.py @@ -87,6 +87,7 @@ def test_read_pyproject_config(self): 'overwrite': False, 'diff': False, 'startline': 10, + 'endline': 20, 'transform': [ 'DiscardEmptySections:allow_only_comments=True', 'SplitTooLongLine' @@ -104,6 +105,7 @@ def test_read_pyproject_config_e2e(self): 'overwrite': 'False', 'diff': 'False', 'startline': '10', + 'endline': '20', 'transform': [ 'DiscardEmptySections:allow_only_comments=True', 'SplitTooLongLine' diff --git a/tests/utest/testdata/only_pyproject/pyproject.toml b/tests/utest/testdata/only_pyproject/pyproject.toml index 325fce03..07bfb4d7 100644 --- a/tests/utest/testdata/only_pyproject/pyproject.toml +++ b/tests/utest/testdata/only_pyproject/pyproject.toml @@ -2,6 +2,7 @@ overwrite = false diff = false startline = 10 +endline = 20 transform = [ "DiscardEmptySections:allow_only_comments=True", "SplitTooLongLine"