Skip to content

Commit

Permalink
Fix parser for python 3.9
Browse files Browse the repository at this point in the history
* Fix parser for python 3.9

Signed-off-by: BugDiver <[email protected]>

* Bump version

Signed-off-by: BugDiver <[email protected]>
  • Loading branch information
Vinay Shankar Shukla authored Dec 26, 2020
1 parent 9b43609 commit 79788dc
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 554 deletions.
16 changes: 4 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ on: [push, pull_request]

jobs:
test:
name: UTs ${{ matrix.os }}
name: UTs ${{ matrix.os }} ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: [3.7, 3.9]

steps:
- uses: actions/checkout@v1

- name: Set up Python 3.7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
Expand Down Expand Up @@ -53,15 +54,6 @@ jobs:
with:
java-version: 12.x.x

- name: Clone gauge
run: |
git clone --depth=1 https://github.com/getgauge/gauge
- name: Build gauge
run: |
cd gauge
go run -mod=vendor build/make.go --verbose
- uses: getgauge/setup-gauge@master
with:
gauge-version: master
Expand Down
13 changes: 8 additions & 5 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@
def install():
plugin_zip = create_zip()
call(['gauge', 'uninstall', 'python', '-v', get_version()])
exit_code = call(['gauge', 'install', 'python', '-f', os.path.join(BIN, plugin_zip)])
exit_code = call(['gauge', 'install', 'python', '-f',
os.path.join(BIN, plugin_zip)])
generate_package()
p = os.listdir("dist")[0]
print("Installing getgauge package using pip: \n\tpip install dist/{}".format(p))
call([sys.executable, "-m", "pip", "install", "dist/{}".format(p), "--upgrade", "--user"])
call([sys.executable, "-m", "pip", "install",
"dist/{}".format(p), "--upgrade", "--user"])
sys.exit(exit_code)


def create_setup_file():
tmpl = open("setup.tmpl", "r")
setup = open("setup.py", "w+")
v = get_version()
setup.write(tmpl.read().format(v, "{\n\t\t':python_version == \"2.7\"': ['futures']\n\t}"))
setup.write(tmpl.read().format(
v, "{\n\t\t':python_version == \"2.7\"': ['futures']\n\t}"))
setup.close()
tmpl.close()

Expand Down Expand Up @@ -100,7 +103,8 @@ def copy(src, dest):

def run_tests():
pp = "PYTHONPATH"
os.environ[pp] = "{0}{1}{2}".format( os.environ.get(pp), os.pathsep, os.path.abspath(os.path.curdir))
os.environ[pp] = "{0}{1}{2}".format(os.environ.get(
pp), os.pathsep, os.path.abspath(os.path.curdir))
test_dir = os.path.join(os.path.curdir, "tests")
exit_code = 0
for root, _, files in os.walk(test_dir):
Expand All @@ -127,6 +131,5 @@ def main():
install()



if __name__ == '__main__':
main()
188 changes: 129 additions & 59 deletions getgauge/parser.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,147 @@
import os
import six
from abc import ABCMeta, abstractmethod
from getgauge.parser_parso import ParsoPythonFile
from getgauge.parser_redbaron import RedbaronPythonFile
from getgauge import logger
from redbaron import RedBaron


class PythonFile(object):
Class = None
class Parser(object):

@staticmethod
def parse(file_path, content=None):
"""
Create a PythonFileABC object with specified file_path and content. If content is None
then, it is loaded from the file_path method. Otherwise, file_path is only used for
reporting errors.
Create a Parser object with specified file_path and content.
If content is None then, it is loaded from the file_path method.
Otherwise, file_path is only used for reporting errors.
"""
return PythonFile.Class.parse(file_path, content)

@staticmethod
def select_python_parser(parser=None):
try:
if content is None:
with open(file_path) as f:
content = f.read()
py_tree = RedBaron(content)
return Parser(file_path, py_tree)
except Exception as ex:
# Trim parsing error message to only include failure location
msg = str(ex)
marker = "<---- here\n"
marker_pos = msg.find(marker)
if marker_pos > 0:
msg = msg[:marker_pos + len(marker)]
logger.error("Failed to parse {}: {}".format(file_path, msg))

def __init__(self, file_path, py_tree):
self.file_path = file_path
self.py_tree = py_tree

def _span_for_node(self, node, lazy=False):
def calculate_span():
try:
# For some reason RedBaron does not create absolute_bounding_box
# attributes for some content passed during unit test so we have
# to catch AttributeError here and return invalid data
box = node.absolute_bounding_box
# Column numbers start at 1 where-as we want to start at 0. Also
# column 0 is used to indicate end before start of line.
return {
'start': box.top_left.line,
'startChar': max(0, box.top_left.column - 1),
'end': box.bottom_right.line,
'endChar': max(0, box.bottom_right.column),
}
except AttributeError:
return {'start': 0, 'startChar': 0, 'end': 0, 'endChar': 0}

return calculate_span if lazy else calculate_span()

def _iter_step_func_decorators(self):
"""Find functions with step decorator in parsed file."""
for node in self.py_tree.find_all('def'):
for decorator in node.decorators:
try:
if decorator.name.value == 'step':
yield node, decorator
break
except AttributeError:
continue

def _step_decorator_args(self, decorator):
"""
Select default parser for loading and refactoring steps. Passing `redbaron` as argument
will select the old paring engine from v0.3.3
Replacing the redbaron parser was necessary to support Python 3 syntax. We have tried our
best to make sure there is no user impact on users. However, there may be regressions with
new parser backend.
To revert to the old parser implementation, add `GETGAUGE_USE_0_3_3_PARSER=true` property
to the `python.properties` file in the `<PROJECT_DIR>/env/default directory.
This property along with the redbaron parser will be removed in future releases.
Get arguments passed to step decorators converted to python objects.
"""
if parser == 'redbaron' or os.environ.get('GETGAUGE_USE_0_3_3_PARSER'):
PythonFile.Class = RedbaronPythonFile
args = decorator.call.value
step = None
if len(args) == 1:
try:
step = args[0].value.to_python()
except (ValueError, SyntaxError):
pass
if isinstance(step, str) or isinstance(step, list):
return step
logger.error("Decorator step accepts either a string or a list of \
strings - {0}".format(self.file_path))
else:
PythonFile.Class = ParsoPythonFile


# Select the default implementation
PythonFile.select_python_parser()

logger.error("Decorator step accepts only one argument - {0}".format(self.file_path))

class PythonFileABC(six.with_metaclass(ABCMeta)):
@staticmethod
def parse(file_path, content=None):
"""
Create a PythonFileABC object with specified file_path and content. If content is None
then, it is loaded from the file_path method. Otherwise, file_path is only used for
reporting errors.
"""
raise NotImplementedError

@abstractmethod
def iter_steps(self):
"""Iterate over steps in the parsed file"""
raise NotImplementedError
"""Iterate over steps in the parsed file."""
for func, decorator in self._iter_step_func_decorators():
step = self._step_decorator_args(decorator)
if step:
yield step, func.name, self._span_for_node(func, True)

def _find_step_node(self, step_text):
"""Find the ast node which contains the text."""
for func, decorator in self._iter_step_func_decorators():
step = self._step_decorator_args(decorator)
arg_node = decorator.call.value[0].value
if step == step_text:
return arg_node, func
elif isinstance(step, list) and step_text in step:
step_node = arg_node[step.index(step_text)]
return step_node, func
return None, None

def _refactor_step_text(self, step, old_text, new_text):
step_span = self._span_for_node(step, False)
step.value = step.value.replace(old_text, new_text)
return step_span, step.value

def _get_param_name(self, param_nodes, i):
name = 'arg{}'.format(i)
if name not in [x.name.value for x in param_nodes]:
return name
return self._get_param_name(param_nodes, i + 1)

def _move_params(self, params, move_param_from_idx):
# If the move list is exactly same as current params
# list then no need to create a new list.
if list(range(len(params))) == move_param_from_idx:
return params
new_params = []
for (new_idx, old_idx) in enumerate(move_param_from_idx):
if old_idx < 0:
new_params.append(self._get_param_name(params, new_idx))
else:
new_params.append(params[old_idx].name.value)
return ', '.join(new_params)

@abstractmethod
def refactor_step(self, old_text, new_text, move_param_from_idx):
"""
Find the step with old_text and change it to new_text. The step function
parameters are also changed according to move_param_from_idx. Each entry in
this list should specify parameter position from old
Find the step with old_text and change it to new_text.
The step function parameters are also changed according
to move_param_from_idx. Each entry in this list should
specify parameter position from old
"""
raise NotImplementedError
diffs = []
step, func = self._find_step_node(old_text)
if step is None:
return diffs
step_diff = self._refactor_step_text(step, old_text, new_text)
diffs.append(step_diff)
moved_params = self._move_params(func.arguments, move_param_from_idx)
if func.arguments is not moved_params:
params_span = self._span_for_node(func.arguments, False)
func.arguments = moved_params
diffs.append((params_span, func.arguments.dumps()))
return diffs

@abstractmethod
def get_code(self):
"""Returns current content of the tree."""
raise NotImplementedError


# Verify that implemetations are subclasses of ABC
PythonFileABC.register(ParsoPythonFile)
PythonFileABC.register(RedbaronPythonFile)
"""Return current content of the tree."""
return self.py_tree.dumps()
Loading

0 comments on commit 79788dc

Please sign in to comment.