Skip to content

Commit 79788dc

Browse files
author
Vinay Shankar Shukla
authored
Fix parser for python 3.9
* Fix parser for python 3.9 Signed-off-by: BugDiver <[email protected]> * Bump version Signed-off-by: BugDiver <[email protected]>
1 parent 9b43609 commit 79788dc

14 files changed

+171
-554
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ on: [push, pull_request]
44

55
jobs:
66
test:
7-
name: UTs ${{ matrix.os }}
7+
name: UTs ${{ matrix.os }} ${{ matrix.python-version }}
88
runs-on: ${{ matrix.os }}
99
strategy:
1010
matrix:
1111
os: [ubuntu-latest, windows-latest, macos-latest]
12+
python-version: [3.7, 3.9]
1213

1314
steps:
1415
- uses: actions/checkout@v1
1516

16-
- name: Set up Python 3.7
17+
- name: Set up Python ${{ matrix.python-version }}
1718
uses: actions/setup-python@v1
1819
with:
19-
python-version: 3.7
20+
python-version: ${{ matrix.python-version }}
2021

2122
- name: Install dependencies
2223
run: |
@@ -53,15 +54,6 @@ jobs:
5354
with:
5455
java-version: 12.x.x
5556

56-
- name: Clone gauge
57-
run: |
58-
git clone --depth=1 https://github.com/getgauge/gauge
59-
60-
- name: Build gauge
61-
run: |
62-
cd gauge
63-
go run -mod=vendor build/make.go --verbose
64-
6557
- uses: getgauge/setup-gauge@master
6658
with:
6759
gauge-version: master

build.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,22 @@
2424
def install():
2525
plugin_zip = create_zip()
2626
call(['gauge', 'uninstall', 'python', '-v', get_version()])
27-
exit_code = call(['gauge', 'install', 'python', '-f', os.path.join(BIN, plugin_zip)])
27+
exit_code = call(['gauge', 'install', 'python', '-f',
28+
os.path.join(BIN, plugin_zip)])
2829
generate_package()
2930
p = os.listdir("dist")[0]
3031
print("Installing getgauge package using pip: \n\tpip install dist/{}".format(p))
31-
call([sys.executable, "-m", "pip", "install", "dist/{}".format(p), "--upgrade", "--user"])
32+
call([sys.executable, "-m", "pip", "install",
33+
"dist/{}".format(p), "--upgrade", "--user"])
3234
sys.exit(exit_code)
3335

3436

3537
def create_setup_file():
3638
tmpl = open("setup.tmpl", "r")
3739
setup = open("setup.py", "w+")
3840
v = get_version()
39-
setup.write(tmpl.read().format(v, "{\n\t\t':python_version == \"2.7\"': ['futures']\n\t}"))
41+
setup.write(tmpl.read().format(
42+
v, "{\n\t\t':python_version == \"2.7\"': ['futures']\n\t}"))
4043
setup.close()
4144
tmpl.close()
4245

@@ -100,7 +103,8 @@ def copy(src, dest):
100103

101104
def run_tests():
102105
pp = "PYTHONPATH"
103-
os.environ[pp] = "{0}{1}{2}".format( os.environ.get(pp), os.pathsep, os.path.abspath(os.path.curdir))
106+
os.environ[pp] = "{0}{1}{2}".format(os.environ.get(
107+
pp), os.pathsep, os.path.abspath(os.path.curdir))
104108
test_dir = os.path.join(os.path.curdir, "tests")
105109
exit_code = 0
106110
for root, _, files in os.walk(test_dir):
@@ -127,6 +131,5 @@ def main():
127131
install()
128132

129133

130-
131134
if __name__ == '__main__':
132135
main()

getgauge/parser.py

Lines changed: 129 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,147 @@
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
63

74

8-
class PythonFile(object):
9-
Class = None
5+
class Parser(object):
106

117
@staticmethod
128
def parse(file_path, content=None):
139
"""
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.
1713
"""
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):
2265
"""
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.
3467
"""
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))
3779
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))
4481

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
5682
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)
59124

60-
@abstractmethod
61125
def refactor_step(self, old_text, new_text, move_param_from_idx):
62126
"""
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
66131
"""
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
68144

69-
@abstractmethod
70145
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

Comments
 (0)