|
1 |
| -import os |
2 |
| -import six |
3 |
| -from abc import ABCMeta, abstractmethod |
4 |
| -from getgauge.parser_parso import ParsoPythonFile |
5 |
| -from getgauge.parser_redbaron import RedbaronPythonFile |
| 1 | +from getgauge import logger |
| 2 | +from redbaron import RedBaron |
6 | 3 |
|
7 | 4 |
|
8 |
| -class PythonFile(object): |
9 |
| - Class = None |
| 5 | +class Parser(object): |
10 | 6 |
|
11 | 7 | @staticmethod
|
12 | 8 | def parse(file_path, content=None):
|
13 | 9 | """
|
14 |
| - Create a PythonFileABC object with specified file_path and content. If content is None |
15 |
| - then, it is loaded from the file_path method. Otherwise, file_path is only used for |
16 |
| - reporting errors. |
| 10 | + Create a Parser object with specified file_path and content. |
| 11 | + If content is None then, it is loaded from the file_path method. |
| 12 | + Otherwise, file_path is only used for reporting errors. |
17 | 13 | """
|
18 |
| - return PythonFile.Class.parse(file_path, content) |
19 |
| - |
20 |
| - @staticmethod |
21 |
| - def select_python_parser(parser=None): |
| 14 | + try: |
| 15 | + if content is None: |
| 16 | + with open(file_path) as f: |
| 17 | + content = f.read() |
| 18 | + py_tree = RedBaron(content) |
| 19 | + return Parser(file_path, py_tree) |
| 20 | + except Exception as ex: |
| 21 | + # Trim parsing error message to only include failure location |
| 22 | + msg = str(ex) |
| 23 | + marker = "<---- here\n" |
| 24 | + marker_pos = msg.find(marker) |
| 25 | + if marker_pos > 0: |
| 26 | + msg = msg[:marker_pos + len(marker)] |
| 27 | + logger.error("Failed to parse {}: {}".format(file_path, msg)) |
| 28 | + |
| 29 | + def __init__(self, file_path, py_tree): |
| 30 | + self.file_path = file_path |
| 31 | + self.py_tree = py_tree |
| 32 | + |
| 33 | + def _span_for_node(self, node, lazy=False): |
| 34 | + def calculate_span(): |
| 35 | + try: |
| 36 | + # For some reason RedBaron does not create absolute_bounding_box |
| 37 | + # attributes for some content passed during unit test so we have |
| 38 | + # to catch AttributeError here and return invalid data |
| 39 | + box = node.absolute_bounding_box |
| 40 | + # Column numbers start at 1 where-as we want to start at 0. Also |
| 41 | + # column 0 is used to indicate end before start of line. |
| 42 | + return { |
| 43 | + 'start': box.top_left.line, |
| 44 | + 'startChar': max(0, box.top_left.column - 1), |
| 45 | + 'end': box.bottom_right.line, |
| 46 | + 'endChar': max(0, box.bottom_right.column), |
| 47 | + } |
| 48 | + except AttributeError: |
| 49 | + return {'start': 0, 'startChar': 0, 'end': 0, 'endChar': 0} |
| 50 | + |
| 51 | + return calculate_span if lazy else calculate_span() |
| 52 | + |
| 53 | + def _iter_step_func_decorators(self): |
| 54 | + """Find functions with step decorator in parsed file.""" |
| 55 | + for node in self.py_tree.find_all('def'): |
| 56 | + for decorator in node.decorators: |
| 57 | + try: |
| 58 | + if decorator.name.value == 'step': |
| 59 | + yield node, decorator |
| 60 | + break |
| 61 | + except AttributeError: |
| 62 | + continue |
| 63 | + |
| 64 | + def _step_decorator_args(self, decorator): |
22 | 65 | """
|
23 |
| - Select default parser for loading and refactoring steps. Passing `redbaron` as argument |
24 |
| - will select the old paring engine from v0.3.3 |
25 |
| -
|
26 |
| - Replacing the redbaron parser was necessary to support Python 3 syntax. We have tried our |
27 |
| - best to make sure there is no user impact on users. However, there may be regressions with |
28 |
| - new parser backend. |
29 |
| -
|
30 |
| - To revert to the old parser implementation, add `GETGAUGE_USE_0_3_3_PARSER=true` property |
31 |
| - to the `python.properties` file in the `<PROJECT_DIR>/env/default directory. |
32 |
| -
|
33 |
| - This property along with the redbaron parser will be removed in future releases. |
| 66 | + Get arguments passed to step decorators converted to python objects. |
34 | 67 | """
|
35 |
| - if parser == 'redbaron' or os.environ.get('GETGAUGE_USE_0_3_3_PARSER'): |
36 |
| - PythonFile.Class = RedbaronPythonFile |
| 68 | + args = decorator.call.value |
| 69 | + step = None |
| 70 | + if len(args) == 1: |
| 71 | + try: |
| 72 | + step = args[0].value.to_python() |
| 73 | + except (ValueError, SyntaxError): |
| 74 | + pass |
| 75 | + if isinstance(step, str) or isinstance(step, list): |
| 76 | + return step |
| 77 | + logger.error("Decorator step accepts either a string or a list of \ |
| 78 | + strings - {0}".format(self.file_path)) |
37 | 79 | else:
|
38 |
| - PythonFile.Class = ParsoPythonFile |
39 |
| - |
40 |
| - |
41 |
| -# Select the default implementation |
42 |
| -PythonFile.select_python_parser() |
43 |
| - |
| 80 | + logger.error("Decorator step accepts only one argument - {0}".format(self.file_path)) |
44 | 81 |
|
45 |
| -class PythonFileABC(six.with_metaclass(ABCMeta)): |
46 |
| - @staticmethod |
47 |
| - def parse(file_path, content=None): |
48 |
| - """ |
49 |
| - Create a PythonFileABC object with specified file_path and content. If content is None |
50 |
| - then, it is loaded from the file_path method. Otherwise, file_path is only used for |
51 |
| - reporting errors. |
52 |
| - """ |
53 |
| - raise NotImplementedError |
54 |
| - |
55 |
| - @abstractmethod |
56 | 82 | def iter_steps(self):
|
57 |
| - """Iterate over steps in the parsed file""" |
58 |
| - raise NotImplementedError |
| 83 | + """Iterate over steps in the parsed file.""" |
| 84 | + for func, decorator in self._iter_step_func_decorators(): |
| 85 | + step = self._step_decorator_args(decorator) |
| 86 | + if step: |
| 87 | + yield step, func.name, self._span_for_node(func, True) |
| 88 | + |
| 89 | + def _find_step_node(self, step_text): |
| 90 | + """Find the ast node which contains the text.""" |
| 91 | + for func, decorator in self._iter_step_func_decorators(): |
| 92 | + step = self._step_decorator_args(decorator) |
| 93 | + arg_node = decorator.call.value[0].value |
| 94 | + if step == step_text: |
| 95 | + return arg_node, func |
| 96 | + elif isinstance(step, list) and step_text in step: |
| 97 | + step_node = arg_node[step.index(step_text)] |
| 98 | + return step_node, func |
| 99 | + return None, None |
| 100 | + |
| 101 | + def _refactor_step_text(self, step, old_text, new_text): |
| 102 | + step_span = self._span_for_node(step, False) |
| 103 | + step.value = step.value.replace(old_text, new_text) |
| 104 | + return step_span, step.value |
| 105 | + |
| 106 | + def _get_param_name(self, param_nodes, i): |
| 107 | + name = 'arg{}'.format(i) |
| 108 | + if name not in [x.name.value for x in param_nodes]: |
| 109 | + return name |
| 110 | + return self._get_param_name(param_nodes, i + 1) |
| 111 | + |
| 112 | + def _move_params(self, params, move_param_from_idx): |
| 113 | + # If the move list is exactly same as current params |
| 114 | + # list then no need to create a new list. |
| 115 | + if list(range(len(params))) == move_param_from_idx: |
| 116 | + return params |
| 117 | + new_params = [] |
| 118 | + for (new_idx, old_idx) in enumerate(move_param_from_idx): |
| 119 | + if old_idx < 0: |
| 120 | + new_params.append(self._get_param_name(params, new_idx)) |
| 121 | + else: |
| 122 | + new_params.append(params[old_idx].name.value) |
| 123 | + return ', '.join(new_params) |
59 | 124 |
|
60 |
| - @abstractmethod |
61 | 125 | def refactor_step(self, old_text, new_text, move_param_from_idx):
|
62 | 126 | """
|
63 |
| - Find the step with old_text and change it to new_text. The step function |
64 |
| - parameters are also changed according to move_param_from_idx. Each entry in |
65 |
| - this list should specify parameter position from old |
| 127 | + Find the step with old_text and change it to new_text. |
| 128 | + The step function parameters are also changed according |
| 129 | + to move_param_from_idx. Each entry in this list should |
| 130 | + specify parameter position from old |
66 | 131 | """
|
67 |
| - raise NotImplementedError |
| 132 | + diffs = [] |
| 133 | + step, func = self._find_step_node(old_text) |
| 134 | + if step is None: |
| 135 | + return diffs |
| 136 | + step_diff = self._refactor_step_text(step, old_text, new_text) |
| 137 | + diffs.append(step_diff) |
| 138 | + moved_params = self._move_params(func.arguments, move_param_from_idx) |
| 139 | + if func.arguments is not moved_params: |
| 140 | + params_span = self._span_for_node(func.arguments, False) |
| 141 | + func.arguments = moved_params |
| 142 | + diffs.append((params_span, func.arguments.dumps())) |
| 143 | + return diffs |
68 | 144 |
|
69 |
| - @abstractmethod |
70 | 145 | def get_code(self):
|
71 |
| - """Returns current content of the tree.""" |
72 |
| - raise NotImplementedError |
73 |
| - |
74 |
| - |
75 |
| -# Verify that implemetations are subclasses of ABC |
76 |
| -PythonFileABC.register(ParsoPythonFile) |
77 |
| -PythonFileABC.register(RedbaronPythonFile) |
| 146 | + """Return current content of the tree.""" |
| 147 | + return self.py_tree.dumps() |
0 commit comments