Skip to content

Commit

Permalink
Consolidate error translation and highlighting; Refactor error templa…
Browse files Browse the repository at this point in the history
…tes #5240
  • Loading branch information
boryanagoncharenko committed Mar 18, 2024
1 parent 6d0a524 commit b5548b8
Show file tree
Hide file tree
Showing 63 changed files with 2,008 additions and 1,903 deletions.
104 changes: 5 additions & 99 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import hedy_translation
import hedyweb
import utils
from hedy_error import get_error_text
from safe_format import safe_format
from config import config
from website.flask_helpers import render_template, proper_tojson, JinjaCompatibleJsonProvider
Expand Down Expand Up @@ -551,7 +552,7 @@ def parse():
DATABASE.increase_user_run_count(username)
ACHIEVEMENTS.increase_count("run")
except hedy.exceptions.WarningException as ex:
translated_error = translate_error(ex.error_code, ex.arguments, keyword_lang)
translated_error = get_error_text(ex, keyword_lang)
if isinstance(ex, hedy.exceptions.InvalidSpaceException):
response['Warning'] = translated_error
elif isinstance(ex, hedy.exceptions.UnusedVariableException):
Expand All @@ -562,7 +563,7 @@ def parse():
transpile_result = ex.fixed_result
exception = ex
except hedy.exceptions.UnquotedEqualityCheckException as ex:
response['Error'] = translate_error(ex.error_code, ex.arguments, keyword_lang)
response['Error'] = get_error_text(ex, keyword_lang)
response['Location'] = ex.error_location
exception = ex

Expand All @@ -572,11 +573,7 @@ def parse():

for i, mapping in source_map_result.items():
if mapping['error'] is not None:
source_map_result[i]['error'] = translate_error(
source_map_result[i]['error'].error_code,
source_map_result[i]['error'].arguments,
keyword_lang
)
source_map_result[i]['error'] = get_error_text(source_map_result[i]['error'], keyword_lang)

response['source_map'] = source_map_result

Expand Down Expand Up @@ -841,102 +838,11 @@ def get_class_name(i):
def hedy_error_to_response(ex):
keyword_lang = current_keyword_language()["lang"]
return {
"Error": translate_error(ex.error_code, ex.arguments, keyword_lang),
"Error": get_error_text(ex, keyword_lang),
"Location": ex.error_location
}


def translate_error(code, arguments, keyword_lang):
arguments_that_require_translation = [
'allowed_types',
'invalid_type',
'invalid_type_2',
'offending_keyword',
'character_found',
'concept',
'tip',
'else',
'command',
'incomplete_command',
'missing_command',
'print',
'ask',
'echo',
'is',
'if',
'repeat']
arguments_that_require_highlighting = [
'command',
'incomplete_command',
'missing_command',
'guessed_command',
'invalid_argument',
'invalid_argument_2',
'offending_keyword',
'variable',
'invalid_value',
'print',
'else',
'ask',
'echo',
'is',
'if',
'repeat',
'[]']

# Todo TB -> We have to find a more delicate way to fix this: returns some gettext() errors
error_template = gettext('' + str(code))

# Fetch tip if it exists and merge into template, since it can also contain placeholders
# that need to be translated/highlighted

if 'tip' in arguments:
error_template = error_template.replace("{tip}", gettext('' + str(arguments['tip'])))
# TODO, FH Oct 2022 -> Could we do this with a format even though we don't have all fields?

# adds keywords to the dictionary so they can be translated if they occur in the error text

# FH Oct 2022: this could be optimized by only adding them when they occur in the text
# (either with string matching or with a list of placeholders for each error)
arguments["print"] = "print"
arguments["ask"] = "ask"
arguments["echo"] = "echo"
arguments["else"] = "else"
arguments["repeat"] = "repeat"
arguments["is"] = "is"
arguments["if"] = "if"

# some arguments like allowed types or characters need to be translated in the error message
for k, v in arguments.items():
if k in arguments_that_require_translation:
if isinstance(v, list):
arguments[k] = translate_list(v)
else:
arguments[k] = gettext('' + str(v))

if k in arguments_that_require_highlighting:
if k in arguments_that_require_translation:
local_keyword = hedy_translation.translate_keyword_from_en(v, keyword_lang)
arguments[k] = hedy.style_command(local_keyword)
else:
arguments[k] = hedy.style_command(v)

return safe_format(error_template, **arguments)


def translate_list(args):
translated_args = [gettext('' + str(a)) for a in args]
# Deduplication is needed because diff values could be translated to the
# same value, e.g. int and float => a number
translated_args = list(dict.fromkeys(translated_args))

if len(translated_args) > 1:
return f"{', '.join(translated_args[0:-1])}" \
f" {gettext('or')} " \
f"{translated_args[-1]}"
return ''.join(translated_args)


@app.route('/report_error', methods=['POST'])
def report_error():
post_body = request.json
Expand Down
3 changes: 3 additions & 0 deletions content/error-messages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ gettext('Incomplete Repeat')
gettext('Unsupported String Value')
gettext('Pressit Missing Else')
gettext('Lonely Text')
gettext('Runtime Value Error')
gettext('Runtime Values Error')
gettext('Runtime Index Error')
gettext('ask_needs_var')
gettext('no_more_flat_if')
gettext('echo_out')
Expand Down
15 changes: 15 additions & 0 deletions exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,4 +361,19 @@ def __init__(self):
super().__init__('Invalid Error Skipped')


class RuntimeValueException(HedyException):
def __init__(self, command, value, tip):
super().__init__('Runtime Value Error', command=command, value=value, tip=tip)


class RuntimeValuesException(HedyException):
def __init__(self, command, value, tip):
super().__init__('Runtime Values Error', command=command, value=value, tip=tip)


class RuntimeIndexException(HedyException):
def __init__(self, name):
super().__init__('Runtime Index Error', name=name)


HEDY_EXCEPTIONS = {name: cls for name, cls in globals().items() if inspect.isclass(cls)}
62 changes: 26 additions & 36 deletions hedy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import warnings
import hedy
import hedy_error
import hedy_translation
from utils import atomic_write_file
from hedy_content import ALL_KEYWORD_LANGUAGES
Expand Down Expand Up @@ -161,11 +162,11 @@

# Let's retrieve all keywords dynamically from the cached KEYWORDS dictionary
indent_keywords = {}
for lang, keywords in KEYWORDS.items():
indent_keywords[lang] = []
for lang_, keywords in KEYWORDS.items():
indent_keywords[lang_] = []
for keyword in ['if', 'elif', 'for', 'repeat', 'while', 'else', 'define', 'def']:
indent_keywords[lang].append(keyword) # always also check for En
indent_keywords[lang].append(keywords.get(keyword))
indent_keywords[lang_].append(keyword) # always also check for En
indent_keywords[lang_].append(keywords.get(keyword))


# These are the preprocessor rules that we use to specify changes in the rules that
Expand All @@ -184,28 +185,17 @@ def needs_colon(rule):
}


def translate_value_error(command, value, suggested_type):
return translate_error(gettext('catch_value_exception'), [
('{command}', command, 1),
('{value}', value, 1),
('{suggestion}', translate_suggestion(suggested_type), 0)
])
def make_value_error(command, value, tip, lang):
return make_error_text(exceptions.RuntimeValueException(command=command, value=value, tip=tip), lang)


def translate_values_error(command, suggested_type):
return translate_error(gettext("catch_multiple_values_exception"), [
('{command}', command, 1),
('{value}', '{}', 1),
('{suggestion}', translate_suggestion(suggested_type), 0)
])
def make_values_error(command, tip, lang):
return make_error_text(exceptions.RuntimeValuesException(command=command, value='{}', tip=tip), lang)


def translate_error(exception_text, variables):
for template, value, is_highlighted in variables:
result = style_command(value) if is_highlighted else value
exception_text = exception_text.replace(template, result)
# The error is transpiled in f-strings with ", ' and ''' quotes. The only option is to use """.
return '"""' + exception_text + '"""'
def make_error_text(ex, lang):
# The error text is transpiled in f-strings with ", ' and ''' quotes. The only option is to use """.
return f'"""{hedy_error.get_error_text(ex, lang)}"""'


def translate_suggestion(suggestion_type):
Expand Down Expand Up @@ -1214,7 +1204,7 @@ def error_invalid(self, meta, args):
if sug_exists is None: # there is no suggestion
raise exceptions.MissingCommandException(level=self.level, line_number=meta.line)
if not sug_exists: # the suggestion is invalid, i.e. identical to the command
invalid_command_en = hedy_translation.translate_keyword_to_en(invalid_command, lang)
invalid_command_en = hedy_translation.translate_keyword_to_en(invalid_command, self.lang)
if invalid_command_en == Command.turn:
arg = args[0][0]
raise hedy.exceptions.InvalidArgumentException(
Expand Down Expand Up @@ -1507,7 +1497,7 @@ def check_var_usage_when_quotes_are_required(self, arg, meta):
def code_to_ensure_variable_type(self, arg, expected_type, command, suggested_type):
if not self.is_variable(arg):
return ""
exception = translate_value_error(command, f'{{{arg}}}', suggested_type)
exception = make_value_error(command, f'{{{arg}}}', suggested_type, self.language)
return textwrap.dedent(f"""\
try:
{expected_type}({arg})
Expand Down Expand Up @@ -1663,7 +1653,7 @@ def make_forward(self, parameter):
return self.make_turtle_command(parameter, Command.forward, 'forward', True, 'int')

def make_play(self, note, meta):
exception_text = translate_value_error('play', note, 'note')
exception_text = make_value_error('play', note, 'suggestion_note', self.language)

return textwrap.dedent(f"""\
if '{note}' not in notes_mapping.keys() and '{note}' not in notes_mapping.values():
Expand All @@ -1672,7 +1662,7 @@ def make_play(self, note, meta):
time.sleep(0.5)""")

def make_play_var(self, note, meta):
exception_text = translate_value_error('play', note, 'note')
exception_text = make_value_error('play', note, 'suggestion_note', self.language)
self.check_var_usage([note], meta.line)
chosen_note = note.children[0] if isinstance(note, Tree) else note

Expand All @@ -1691,7 +1681,7 @@ def make_turtle_command(self, parameter, command, command_text, add_sleep, type)
if isinstance(parameter, str):
exception = self.make_index_error_check_if_list([parameter])
variable = self.get_fresh_var('__trtl')
exception_text = translate_value_error(command, variable, 'number')
exception_text = make_value_error(command, variable, 'suggestion_number', self.language)
transpiled = exception + textwrap.dedent(f"""\
{variable} = {parameter}
try:
Expand All @@ -1711,7 +1701,7 @@ def make_turtle_color_command(self, parameter, command, command_text, language):
# coming from a random list or ask

color_dict = {hedy_translation.translate_keyword_from_en(x, language): x for x in english_colors}
exception_text = translate_value_error(command, parameter, 'color')
exception_text = make_value_error(command, parameter, 'suggestion_color', self.language)
return textwrap.dedent(f"""\
{variable} = f'{parameter}'
color_dict = {color_dict}
Expand Down Expand Up @@ -1744,7 +1734,7 @@ def make_index_error_check_if_list(self, args):
return ''.join(errors)

def make_index_error(self, code, list_name):
exception_text = translate_error(gettext('catch_index_exception'), [('{list_name}', list_name, 1)])
exception_text = make_error_text(exceptions.RuntimeIndexException(name=list_name), self.language)
return textwrap.dedent(f"""\
try:
{code}
Expand Down Expand Up @@ -1890,7 +1880,7 @@ def sleep(self, meta, args):
self.add_variable_access_location(value, meta.line)
exceptions = self.make_index_error_check_if_list(args)
try_prefix = "try:\n" + textwrap.indent(exceptions, " ")
exception_text = translate_value_error(Command.sleep, value, 'number')
exception_text = make_value_error(Command.sleep, value, 'suggestion_number', self.language)
code = try_prefix + textwrap.dedent(f"""\
time.sleep(int({value})){self.add_debug_breakpoint()}
except ValueError:
Expand Down Expand Up @@ -2153,7 +2143,7 @@ def sleep(self, meta, args):

exceptions = self.make_index_error_check_if_list(args)
try_prefix = "try:\n" + textwrap.indent(exceptions, " ")
exception_text = translate_value_error(Command.sleep, value, 'number')
exception_text = make_value_error(Command.sleep, value, 'suggestion_number', self.language)
code = try_prefix + textwrap.dedent(f"""\
time.sleep(int({value}))
except ValueError:
Expand Down Expand Up @@ -2218,7 +2208,7 @@ def process_token_or_tree_for_calculation(self, argument, command, meta):
latin_numeral = int(argument)
return f'int({latin_numeral})'
self.add_variable_access_location(argument, meta.line)
exception_text = translate_value_error(command, argument, 'number')
exception_text = make_value_error(command, argument, 'suggestion_number', self.language)
return f'int_with_error({argument}, {exception_text})'

def process_calculation(self, args, operator, meta):
Expand Down Expand Up @@ -2295,7 +2285,7 @@ def make_repeat(self, meta, args, multiline):
body = "\n".join([self.indent(x) for x in args[1:]])

body = add_sleep_to_command(body, indent=True, is_debug=self.is_debug, location="after")
type_check = self.code_to_ensure_variable_type(times, 'int', Command.repeat, 'number')
type_check = self.code_to_ensure_variable_type(times, 'int', Command.repeat, 'suggestion_number')
return f"{type_check}for {var_name} in range(int({times})):{self.add_debug_breakpoint()}\n{body}"

def repeat(self, meta, args):
Expand Down Expand Up @@ -2608,7 +2598,7 @@ def process_token_or_tree_for_calculation(self, argument, command, meta):
else:
# this is a variable, add to the table
self.add_variable_access_location(argument, meta.line)
exception_text = translate_value_error(command, argument, 'number')
exception_text = make_value_error(command, argument, 'suggestion_number', self.language)
return f'number_with_error({argument}, {exception_text})'

def process_token_or_tree_for_addition(self, argument, meta):
Expand All @@ -2625,8 +2615,8 @@ def addition(self, meta, args):
if all([self.is_int(a) or self.is_float(a) for a in args]):
return Tree('sum', [f'{args[0]} + {args[1]}'])
else:
exception_text = translate_values_error(Command.addition, 'numbers_or_strings')
return Tree('sum', [f'sum_with_error({args[0]}, {args[1]}, {exception_text})'])
ex_text = make_values_error(Command.addition, 'suggestion_numbers_or_strings', self.language)
return Tree('sum', [f'sum_with_error({args[0]}, {args[1]}, {ex_text})'])

def division(self, meta, args):
return self.process_calculation(args, '/', meta)
Expand Down
Loading

0 comments on commit b5548b8

Please sign in to comment.