Skip to content

Commit

Permalink
WIP: error parser draft
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicoretti committed Aug 7, 2023
1 parent f0f80d8 commit 26a4df1
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 4 deletions.
4 changes: 4 additions & 0 deletions exasol/error/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from exasol.error._cli import main

if __name__ == "__main__":
main()
217 changes: 217 additions & 0 deletions exasol/error/_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import argparse
import ast
import json
import sys
from dataclasses import asdict, dataclass, is_dataclass
from pathlib import Path
from typing import List, Optional
from enum import IntEnum


class ExitCode(IntEnum):
SUCCESS = 0
FAILURE = -1


@dataclass(frozen=True)
class Placeholder:
"""
Placeholder according to schema specification.
https://schemas.exasol.com/error_code_report-1.0.0.json
"""
placeholder: str
description: Optional[str]


@dataclass
class ErrorCodeDetails:
"""
Error code details according to schema specification.
https://schemas.exasol.com/error_code_report-1.0.0.json
"""
identifier: str
message: Optional[str]
messagePlaceholders: Optional[List[Placeholder]]
description: Optional[str]
internalDescription: Optional[str]
potentialCauses: Optional[List[str]]
mitigations: Optional[List[str]]
sourceFile: Optional[str]
sourceLine: Optional[str]
contextHash: Optional[str]


class ValidationError(Exception):
""""""


class ErrorCollector(ast.NodeVisitor):
def __init__(self, filename="<Unknown>"):
self._filename = filename
self._errors = list()

@staticmethod
def validate(node):
pass
# self._node = node
# def validate():
# def validate_code(code):
# pass

# def validate_message(message):
# pass

# def validate_mitigations(mitigations):
# pass

# def validate_parameters(params):
# for key in params.keys:
# if not isinstance(key, ast.Constant):
# raise ValueError("Only constant values for keys are allowed")
# for value in params.values:
# if isinstance(value, ast.Call):
# if not isinstance(value.args[1], ast.Constant):
# raise ValueError(
# "Only constant values for descriptions are allowed"
# )

@staticmethod
def _is_exa_error(node):
if not isinstance(node, ast.Call):
return False
name = getattr(node.func, "id", "")
name = getattr(node.func, "attr", "") if name == "" else name
return name == "ExaError"

@property
def errors(self):
return self._errors

def _make_error(self, node):
code, message, mitigations, parameters = node.args

def normalize(params):
for k, v in zip(params.keys, params.keys):
if isinstance(v, ast.Call):
yield k.value, v[1]
else:
yield k.value, ""

return ErrorCodeDetails(
identifier=code.value,
message=message.value,
messagePlaceholders=[
Placeholder(name, description)
for name, description in normalize(parameters)
],
description=None,
internalDescription=None,
potentialCauses=None,
mitigations=[m.value for m in mitigations.elts],
sourceFile=self._filename,
sourceLine=node.lineno,
contextHash=None,
)

def visit(self, node):
if not self._is_exa_error(node):
return

try:
self.validate(node)
except ValidationError:
raise

error = self._make_error(node)
self._errors.append(error)

def generic_visit(self, node):
raise NotImplementedError()


class _JsonEncoder(json.JSONEncoder):
"""Json encoder with dataclass support"""

def default(self, obj):
if is_dataclass(obj):
return asdict(obj)
return super().default(obj)


def parse_command(args) -> ExitCode:
"""Parse errors out one or more python files and report them in the jsonl format."""
errors = list()
for f in args.python_file:
collector = ErrorCollector(f.name)
root_node = ast.parse(f.read())
for n in ast.walk(root_node):
try:
collector.visit(n)
except ValidationError:
pass # add/print validation errors
errors.extend(collector.errors)

for e in errors:
print(json.dumps(e, cls=_JsonEncoder))

return ExitCode.SUCCESS


def generate_command(args) -> ExitCode:
"""Generate an error code file for the specified workspace
first version use root.
future version just get root and get meta information from pyproject.toml
## ec report root(.) project.toml
## parse version from project.toml
## parse packages from pyproject.toml
"""
return ExitCode.FAILURE


def _create_parser():
parser = argparse.ArgumentParser(
prog="ec",
description="Error Crawler",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
subparsers = parser.add_subparsers()

parse = subparsers.add_parser("parse")
parse.add_argument(
"python_file",
metavar="python-file",
type=argparse.FileType("r"),
default=[sys.stdin],
nargs="*",
help="TBD",
)
parser.set_defaults(func=parse_command)

generate = subparsers.add_parser("generate")
generate.add_argument(
"root",
metavar="root",
type=Path,
default=[sys.stdin],
nargs="*",
help="some help",
)
generate.set_defaults(func=generate_command)

return parser


def _report(project_name, project_version, errors):
return {
"$schema": "https://schemas.exasol.com/error_code_report-1.0.0.json",
"projectName": project_name,
"projectVersion": project_version,
"errorCodes": [e for e in errors],
}


def main():
parser = _create_parser()
args = parser.parse_args()
sys.exit(args.func(args))
7 changes: 4 additions & 3 deletions exasol/error/_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ def ExaError(
Potential error scenarios which should taken into account are the following ones:
* ExaError('E-ERP-1', 'Invalid error code provided.', [], []),
* ExaError('E-ERP-2', 'Invalid parameter specification.', [], []),
* ExaError('E-ERP-3', 'Invalid error message specification.', [], []),
* ExaError('E-ERP-4', 'Invalid mitigation specification.', [], []),
* ExaError('E-ERP-2', 'Unkown Exception error code provided.', [], []), # erzeugungs orginal error params, und exception
* ExaError('E-ERP-2', 'Invalid parameter specification.', [], []), parameter, _to_string
* ExaError('E-ERP-3', 'Invalid error message specification.', [], []), # warning by parser
* ExaError('E-ERP-4', 'Invalid mitigation specification.', [], []), # warning by parser type to_to
"""
return Error(name, message, mitigations, parameters)
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ keywords = ['exasol', 'python', 'error-reporting']
[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
prysk = {extras = ["pytest-plugin"], version = "^0.15.1"}

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.scripts]
ec = 'exasol.error._parse:main'
41 changes: 41 additions & 0 deletions test/integration/parse-errors.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Prepare Test

$ export ROOT=$TESTDIR/../../

$ cat > pymodule.py << EOF
> from exasol import error
> from exasol.error import ExaError, Parameter
> # partial path/ns
> error1 = error.ExaError('E-TEST-1', 'this is an error', ['no mitigation available'], Parameter('param', 'value', 'some description'))
> error2 = error.ExaError('E-TEST-2', 'this is an error', ['no mitigation available'], {'param': 'value'})
> # no path/ns
> error3 = ExaError('E-TEST-3', 'this is an error', ['no mitigation available'],
> Parameter('param', 'value', 'some description'))
> error4 = ExaError('E-TEST-4', 'this is an error', ['no mitigation available'], {'param': 'value'})
> # dynamic parameter value
> value = input('please provide parameter value')
> error5 = ExaError('E-TEST-5', 'this is an error', ['no mitigation available'],
> Parameter('param', value, 'some description'))
> error6 = ExaError('E-TEST-6', 'this is an error', ['no mitigation available'], {'param': value})
> error7 = ExaError(
> 'E-TEST-7', 'Not enough space on device {{device}}.',
> [
> "Delete something from {{device}}.",
> "Create larger partition."
> ],
> Parameter('device', '/dev/sda1', 'name of the device')
> )
> EOF

Execute Test scenario

$ python -m exasol.error pymodule.py
E-TEST-1: this is an error no mitigation available
E-TEST-2: this is an error no mitigation available
E-TEST-3: this is an error no mitigation available
E-TEST-4: this is an error no mitigation available
E-TEST-5: this is an error no mitigation available
E-TEST-6: this is an error no mitigation available
E-TEST-7: Not enough space on device '/dev/sda1'. Known mitigations:
* Delete something from '/dev/sda1'.
* Create larger partition.

0 comments on commit 26a4df1

Please sign in to comment.