Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪲 Fix error translation and highlighting #5275

Merged
merged 7 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -555,7 +556,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 @@ -566,7 +567,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 @@ -576,11 +577,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 @@ -869,102 +866,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
Loading