Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f9e118f
Added the possibility for using the programming_language key
BrentBlanckaert Mar 31, 2025
3836d62
Fixed some linting issues
BrentBlanckaert Mar 31, 2025
537f0e8
Made some changes
BrentBlanckaert Apr 3, 2025
b482816
Fixed merge conflict
BrentBlanckaert Apr 7, 2025
6675fa8
Fixed tests
BrentBlanckaert Apr 7, 2025
6f19925
Merge branch 'master' into feat/add-programming-language-tag
BrentBlanckaert Apr 7, 2025
23dafd3
Fixed dsl tests and added warning
BrentBlanckaert Apr 7, 2025
214c5ee
fix linting
BrentBlanckaert Apr 7, 2025
d98aa82
removed default
BrentBlanckaert Apr 10, 2025
a57d932
Merge branch 'feat/add-natural-translation' into feat/add-programming…
BrentBlanckaert Apr 14, 2025
202168b
added extra bit to the schema
BrentBlanckaert Apr 14, 2025
e35f474
Merge branch 'feat/add-natural-translation' into feat/add-programming…
BrentBlanckaert Apr 14, 2025
05f1e88
Merge branch 'feat/add-natural-translation' into feat/add-programming…
BrentBlanckaert Apr 15, 2025
051ba09
merged with main translator branch
BrentBlanckaert Apr 28, 2025
c4c2fe4
using messages instead of boolean in parser
BrentBlanckaert Apr 28, 2025
8d5f700
hotfix
BrentBlanckaert Apr 28, 2025
ffc1d3a
revert changes in tests
BrentBlanckaert Apr 28, 2025
c562b91
added the tests for programming_language tag in the parser
BrentBlanckaert Apr 28, 2025
71dfcc4
fix linting
BrentBlanckaert Apr 29, 2025
e9ba1ef
changed some of the names
BrentBlanckaert May 2, 2025
47df8a6
merge with main translation branch
BrentBlanckaert May 2, 2025
8be8e83
merged messages and made extra test
BrentBlanckaert May 2, 2025
0f2e397
Fixed crash
BrentBlanckaert May 2, 2025
d8b39eb
Use Data class instead of tuple
BrentBlanckaert May 9, 2025
544d4df
changed a few variable names + few small changes
BrentBlanckaert May 12, 2025
4e9b9bd
changed variable names
BrentBlanckaert May 12, 2025
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
1 change: 0 additions & 1 deletion tested/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"--translate",
type=str,
help="Specifies the language to translate translate the dsl to.",
default="-",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this change do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actualy a change that already should be in the other branch. This default will cause the translator to run when the option -t isn't even provided.

)
parser = parser.parse_args()

Expand Down
21 changes: 21 additions & 0 deletions tested/dsl/dsl_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,24 @@ def build_preprocessor_messages(
)
for key in translations_missing_key
]


def build_translate_parser_messages(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't take a boolean argument here, nor would I return a list, as it is a single message
And rename it to build_deprecated_language_message or something like that

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote it like this such that this function can easily be expanded when extra deprecation warnings pop-up about using file instead output_files.

using_deprecated_prog_languages: bool,
) -> list[ExtendedMessage]:
"""
Build the translate parser messages from the missing keys.

:param translations_missing_key: The missing keys.
:return: The translate parser messages.
"""
messages = []

if using_deprecated_prog_languages:
messages.append(
ExtendedMessage(
f"WARNING: You are using YAML syntax to specify statements or expressions in multiple programming languages without the `!programming_language` tag. This usage is deprecated!",
permission=Permission.STAFF,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bmesuere, this gave me the idea that we might need to add a Permission.CREATOR or something like that in the future. Some messages just aren't very actionable for teachers using exercises that are not their own.

We might also want to mark exercises with unresolved warnings/errors in dodona and create a page to review these for content creators

)
)
return messages
77 changes: 77 additions & 0 deletions tested/dsl/schema-strict-nat-translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,32 @@
"type" : "string",
"description" : "A language-specific literal, which will be used verbatim."
}
},
{
"type" : "object",
"required": [
"__tag__",
"value"
],
"properties" : {
"__tag__": {
"type" : "string",
"description" : "The tag used in the yaml",
"const": "!programming_language"
},
"value":{
"type": "object",
"description" : "Programming-language-specific statement or expression.",
"minProperties" : 1,
"propertyNames" : {
"$ref" : "#/definitions/programmingLanguage"
},
"items" : {
"type" : "string",
"description" : "A language-specific literal, which will be used verbatim."
}
}
}
}
]
},
Expand Down Expand Up @@ -1313,6 +1339,57 @@
]
}
},
{
"type" : "object",
"required": [
"__tag__",
"value"
],
"properties" : {
"__tag__": {
"type" : "string",
"description" : "The tag used in the yaml",
"const": "!programming_language"
},
"value":{
"type": "object",
"description" : "Programming-language-specific statement or expression.",
"minProperties" : 1,
"propertyNames" : {
"$ref" : "#/definitions/programmingLanguage"
},
"items" : {
"oneOf" : [
{
"type" : "string",
"description" : "A language-specific literal, which will be used verbatim."
},
{
"type" : "object",
"required": [
"__tag__",
"value"
],
"properties" : {
"__tag__": {
"type" : "string",
"description" : "The tag used in the yaml",
"const": "!natural_language"
},
"value":{
"type": "object",
"additionalProperties": {
"type" : "string",
"description" : "A language-specific literal, which will be used verbatim."
}
}
}
}
]
}
}
}
},
{
"type" : "object",
"required": [
Expand Down
11 changes: 10 additions & 1 deletion tested/dsl/schema-strict.json
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,14 @@
},
{
"description" : "Programming-language-specific statement or expression.",
"anyOf": [
{
"type": "object"
},
{
"type": "programming_language"
}
],
"type" : "object",
"minProperties" : 1,
"propertyNames" : {
Expand Down Expand Up @@ -873,7 +881,8 @@
"not" : {
"type" : [
"oracle",
"expression"
"expression",
"programming_language"
]
}
},
Expand Down
114 changes: 86 additions & 28 deletions tested/dsl/translate_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,22 @@
pass


class ProgrammingLanguageMap(dict):
pass


OptionDict = dict[str, int | bool]
YamlObject = (
YamlDict | list | bool | float | int | str | None | ExpressionString | ReturnOracle
YamlDict
| list
| bool
| float
| int
| str
| None
| ExpressionString
| ReturnOracle
| ProgrammingLanguageMap
)


Expand Down Expand Up @@ -136,6 +149,16 @@
return ReturnOracle(result)


def _return_programming_language_map(
loader: yaml.Loader, node: yaml.Node
) -> ProgrammingLanguageMap:
result = _parse_yaml_value(loader, node)
assert isinstance(

Check warning on line 156 in tested/dsl/translate_parser.py

View check run for this annotation

Codecov / codecov/patch

tested/dsl/translate_parser.py#L155-L156

Added lines #L155 - L156 were not covered by tests
result, dict
), f"A programming language map must be an object, got {result} which is a {type(result)}."
return ProgrammingLanguageMap(result)

Check warning on line 159 in tested/dsl/translate_parser.py

View check run for this annotation

Codecov / codecov/patch

tested/dsl/translate_parser.py#L159

Added line #L159 was not covered by tests


def _parse_yaml(yaml_stream: str) -> YamlObject:
"""
Parse a string or stream to YAML.
Expand All @@ -146,6 +169,9 @@
yaml.add_constructor("!" + actual_type, _custom_type_constructors, loader)
yaml.add_constructor("!expression", _expression_string, loader)
yaml.add_constructor("!oracle", _return_oracle, loader)
yaml.add_constructor(
"!programming_language", _return_programming_language_map, loader
)

try:
return yaml.load(yaml_stream, loader)
Expand All @@ -161,6 +187,10 @@
return isinstance(instance, ExpressionString)


def is_programming_language_map(_checker: TypeChecker, instance: Any) -> bool:
return isinstance(instance, ProgrammingLanguageMap)


def load_schema_validator(
dsl_object: YamlObject = None, file: str = "schema-strict.json"
) -> Validator:
Expand Down Expand Up @@ -189,9 +219,11 @@
schema_object = json.load(schema_file)

original_validator: Type[Validator] = validator_for(schema_object)
type_checker = original_validator.TYPE_CHECKER.redefine(
"oracle", is_oracle
).redefine("expression", is_expression)
type_checker = (
original_validator.TYPE_CHECKER.redefine("oracle", is_oracle)
.redefine("expression", is_expression)
.redefine("programming_language", is_programming_language_map)
)
format_checker = original_validator.FORMAT_CHECKER
format_checker.checks("tested-dsl-expression", SyntaxError)(
validate_tested_dsl_expression
Expand Down Expand Up @@ -495,7 +527,8 @@
raise ValueError("A statement cannot have an expected return value.")


def _convert_testcase(testcase: YamlDict, context: DslContext) -> Testcase:
def _convert_testcase(testcase: YamlDict, context: DslContext) -> tuple[Testcase, bool]:
using_deprecated_prog_lang = False
context = context.deepen_context(testcase)

# This is backwards compatability to some extend.
Expand All @@ -506,11 +539,16 @@
line_comment = ""
_validate_testcase_combinations(testcase)
if (expr_stmt := testcase.get("statement", testcase.get("expression"))) is not None:
if isinstance(expr_stmt, dict) or context.language != "tested":
if (
isinstance(expr_stmt, dict | ProgrammingLanguageMap)
or context.language != "tested"
):
if isinstance(expr_stmt, str):
the_dict = {context.language: expr_stmt}
else:
assert isinstance(expr_stmt, dict)
if not isinstance(expr_stmt, ProgrammingLanguageMap):
using_deprecated_prog_lang = True
the_dict = expr_stmt
the_dict = {SupportedLanguage(l): cast(str, v) for l, v in the_dict.items()}
if "statement" in testcase:
Expand Down Expand Up @@ -587,24 +625,31 @@
else:
the_description = None

return Testcase(
description=the_description,
input=the_input,
output=output,
link_files=context.files,
line_comment=line_comment,
return (
Testcase(
description=the_description,
input=the_input,
output=output,
link_files=context.files,
line_comment=line_comment,
),
using_deprecated_prog_lang,
)


def _convert_context(context: YamlDict, dsl_context: DslContext) -> Context:
def _convert_context(
context: YamlDict, dsl_context: DslContext
) -> tuple[Context, bool]:
dsl_context = dsl_context.deepen_context(context)
raw_testcases = context.get("script", context.get("testcases"))
assert isinstance(raw_testcases, list)
testcases = _convert_dsl_list(raw_testcases, dsl_context, _convert_testcase)
return Context(testcases=testcases)
testcases, deprecated_prog = _convert_dsl_list(
raw_testcases, dsl_context, _convert_testcase
)
return Context(testcases=testcases), deprecated_prog


def _convert_tab(tab: YamlDict, context: DslContext) -> Tab:
def _convert_tab(tab: YamlDict, context: DslContext) -> tuple[Tab, bool]:
"""
Translate a DSL tab to a full test suite tab.
Expand All @@ -615,45 +660,54 @@
context = context.deepen_context(tab)
name = tab.get("unit", tab.get("tab"))
assert isinstance(name, str)
deprecated_prog = False

# The tab can have testcases or contexts.
if "contexts" in tab:
assert isinstance(tab["contexts"], list)
contexts = _convert_dsl_list(tab["contexts"], context, _convert_context)
contexts, val = _convert_dsl_list(tab["contexts"], context, _convert_context)

elif "cases" in tab:
assert "unit" in tab
# We have testcases N.S. / contexts O.S.
assert isinstance(tab["cases"], list)
contexts = _convert_dsl_list(tab["cases"], context, _convert_context)
contexts, val = _convert_dsl_list(tab["cases"], context, _convert_context)
deprecated_prog = deprecated_prog or val
elif "testcases" in tab:
# We have scripts N.S. / testcases O.S.
assert "tab" in tab
assert isinstance(tab["testcases"], list)
testcases = _convert_dsl_list(tab["testcases"], context, _convert_testcase)
testcases, val = _convert_dsl_list(tab["testcases"], context, _convert_testcase)
contexts = [Context(testcases=[t]) for t in testcases]
else:
assert "scripts" in tab
assert isinstance(tab["scripts"], list)
testcases = _convert_dsl_list(tab["scripts"], context, _convert_testcase)
testcases, val = _convert_dsl_list(tab["scripts"], context, _convert_testcase)
contexts = [Context(testcases=[t]) for t in testcases]

return Tab(name=name, contexts=contexts)
deprecated_prog = deprecated_prog or val
return Tab(name=name, contexts=contexts), deprecated_prog


T = TypeVar("T")


def _convert_dsl_list(
dsl_list: list, context: DslContext, converter: Callable[[YamlDict, DslContext], T]
) -> list[T]:
dsl_list: list,
context: DslContext,
converter: Callable[[YamlDict, DslContext], tuple[T, bool]],
) -> tuple[list[T], bool]:
"""
Convert a list of YAML objects into a test suite object.
"""
deprecated_prog = False
objects = []
for dsl_object in dsl_list:
assert isinstance(dsl_object, dict)
objects.append(converter(dsl_object, context))
return objects
obj, val = converter(dsl_object, context)
deprecated_prog = deprecated_prog or val
objects.append(obj)
return objects, deprecated_prog


def _convert_dsl(dsl_object: YamlObject) -> Suite:
Expand All @@ -679,13 +733,17 @@
if (language := dsl_object.get("language", "tested")) != "tested":
language = SupportedLanguage(language)
context = evolve(context, language=language)
tabs = _convert_dsl_list(tab_list, context, _convert_tab)
tabs, deprecated_prog = _convert_dsl_list(tab_list, context, _convert_tab)

if namespace:
assert isinstance(namespace, str)
return Suite(tabs=tabs, namespace=namespace)
return Suite(
tabs=tabs,
namespace=namespace,
using_deprecated_prog_languages=deprecated_prog,
)
else:
return Suite(tabs=tabs)
return Suite(tabs=tabs, using_deprecated_prog_languages=deprecated_prog)


def parse_dsl(dsl_string: str) -> Suite:
Expand Down
Loading