Skip to content

Commit

Permalink
Merge pull request #407 from dodona-edu/feature/type-checker
Browse files Browse the repository at this point in the history
Add type checker
  • Loading branch information
niknetniko committed Aug 9, 2023
2 parents 555f2bd + f5d4272 commit 411f5b1
Show file tree
Hide file tree
Showing 64 changed files with 842 additions and 604 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ jobs:
with:
version: "~= 23.0"
src: "./tested ./tests"
types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.11.2
cache: 'pipenv'
- run: pip install pipenv
- run: pipenv install --dev
- run: echo "$(pipenv --venv)/bin" >> $GITHUB_PATH
- uses: jakebailey/pyright-action@v1
with:
version: '1.1.316'
warnings: true
working-directory: tested/
10 changes: 6 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@
typing-inspect
pyyaml
pygments
python-i18n
# For Pycharm
setuptools
python-i18n
];
python-env = python.withPackages(ps: (core-packages ps) ++ [
ps.pylint
ps.pytest
ps.pytest-mock
ps.pytest-cov
# For Pycharm
ps.setuptools
ps.isort
ps.black
]);
core-deps = [
(python.withPackages(ps: (core-packages ps) ++ [ps.pylint]))
Expand Down Expand Up @@ -115,7 +117,7 @@
default = tested;
tested = pkgs.devshell.mkShell {
name = "TESTed";
packages = [python-env] ++ haskell-deps ++ node-deps ++ bash-deps ++ c-deps ++ java-deps ++ kotlin-deps ++ csharp-deps;
packages = [python-env pkgs.nodePackages.pyright] ++ haskell-deps ++ node-deps ++ bash-deps ++ c-deps ++ java-deps ++ kotlin-deps ++ csharp-deps;
devshell.startup.link.text = ''
mkdir -p "$PRJ_DATA_DIR/current"
ln -sfn "${python-env}/${python-env.sitePackages}" "$PRJ_DATA_DIR/current/python-packages"
Expand Down
84 changes: 0 additions & 84 deletions flake.old.nix

This file was deleted.

8 changes: 4 additions & 4 deletions tested/datatypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@

NumericTypes = Union[BasicNumericTypes, AdvancedNumericTypes]
StringTypes = Union[BasicStringTypes, AdvancedStringTypes]
BooleanTypes = Union[BasicBooleanTypes]
BooleanTypes = BasicBooleanTypes
NothingTypes = Union[BasicNothingTypes, AdvancedNothingTypes]
SequenceTypes = Union[BasicSequenceTypes, AdvancedSequenceTypes]
ObjectTypes = Union[BasicObjectTypes]
ObjectTypes = BasicObjectTypes

SimpleTypes = Union[NumericTypes, StringTypes, BooleanTypes, NothingTypes]
ComplexTypes = Union[SequenceTypes, ObjectTypes]
Expand All @@ -56,10 +56,10 @@ def resolve_to_basic(type_: AllTypes) -> BasicTypes:
"""
Resolve a type to its basic type. Basic types are returned unchanged.
"""
if isinstance(type_, get_args(BasicTypes)):
if isinstance(type_, BasicTypes):
return type_

assert isinstance(type_, get_args(AdvancedTypes))
assert isinstance(type_, AdvancedTypes)
return type_.base_type


Expand Down
14 changes: 7 additions & 7 deletions tested/description_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,14 @@ def create_description_instance_from_template(
judge_directory = Path(__file__).parent.parent
global_config = GlobalConfig(
dodona=DodonaConfig(
resources="",
source="",
resources="", # type: ignore
source="", # type: ignore
time_limit=0,
memory_limit=0,
natural_language=natural_language,
programming_language=programming_language,
workdir="",
judge=str(judge_directory),
workdir="", # type: ignore
judge=judge_directory,
test_suite="suite.yaml",
),
context_separator_secret="",
Expand Down Expand Up @@ -185,7 +185,7 @@ def get_variable(var_name: str, is_global: bool = True):
if is_html:
namespace = html.escape(namespace)

return template.render(
return template.render( # type: ignore
function=partial(description_generator.get_function_name, is_html=is_html),
property=partial(description_generator.get_property_name, is_html=is_html),
variable=get_variable,
Expand Down Expand Up @@ -224,9 +224,9 @@ def create_description_instance(
if not language_exists(programming_language):
raise ValueError(f"Language {programming_language} doesn't exists")

template = prepare_template(template, is_html)
template = prepare_template(template, is_html) # type: ignore
return create_description_instance_from_template(
template, programming_language, natural_language, namespace, is_html
template, programming_language, natural_language, namespace, is_html # type: ignore
)


Expand Down
8 changes: 5 additions & 3 deletions tested/dodona.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import dataclasses
import json
from enum import StrEnum, auto, unique
from typing import IO, Literal, Optional, Type, Union
from typing import IO, Literal, Optional, Union

from pydantic import BaseModel
from pydantic.dataclasses import dataclass
Expand Down Expand Up @@ -138,7 +138,7 @@ class AnnotateCode:

row: Index
text: str
externalUrl: str = None
externalUrl: Optional[str] = None
column: Optional[Index] = None
type: Optional[Severity] = None
rows: Optional[Index] = None
Expand Down Expand Up @@ -227,7 +227,9 @@ class CloseJudgment:
}


def close_for(type_: str) -> Type[Update]:
def close_for(
type_: str,
) -> type[CloseJudgment | CloseTab | CloseContext | CloseTestcase | CloseTest]:
return _mapping[type_]


Expand Down
44 changes: 32 additions & 12 deletions tested/dsl/ast_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
- Collection and datastructure literals
- Negation operator
- Function calls
- Keyword arguments (ie. named arguments)
- Properties (ie. attributes)
- Keyword arguments (i.e. named arguments)
- Properties (i.e. attributes)
"""

import ast
import dataclasses
from typing import Optional
from typing import Literal, Optional, cast, overload

from _decimal import Decimal
from pydantic import ValidationError

from tested.datatypes import (
Expand All @@ -51,6 +52,7 @@
ObjectKeyValuePair,
ObjectType,
SequenceType,
SpecialNumbers,
Statement,
Value,
VariableType,
Expand All @@ -70,11 +72,9 @@ def _is_and_get_allowed_empty(node: ast.Call) -> Optional[Value]:
"""
assert isinstance(node.func, ast.Name)
if node.func.id in AdvancedSequenceTypes.__members__.values():
# noinspection PyTypeChecker
return SequenceType(type=node.func.id, data=[])
return SequenceType(type=cast(AdvancedSequenceTypes, node.func.id), data=[])
elif node.func.id in BasicSequenceTypes.__members__.values():
# noinspection PyTypeChecker
return SequenceType(type=node.func.id, data=[])
return SequenceType(type=cast(BasicSequenceTypes, node.func.id), data=[])
elif node.func.id in BasicObjectTypes.__members__.values():
return ObjectType(type=BasicObjectTypes.MAP, data=[])
else:
Expand All @@ -96,6 +96,7 @@ def _is_type_cast(node: ast.expr) -> bool:
def _convert_ann_assignment(node: ast.AnnAssign) -> Assignment:
if not isinstance(node.target, ast.Name):
raise InvalidDslError("You can only assign to simple variables")
assert node.value
value = _convert_expression(node.value, False)
if isinstance(node.annotation, ast.Name):
type_ = node.annotation.id
Expand All @@ -109,7 +110,11 @@ def _convert_ann_assignment(node: ast.AnnAssign) -> Assignment:
if not is_our_type:
type_ = VariableType(data=type_)

return Assignment(variable=node.target.id, expression=value, type=type_)
return Assignment(
variable=node.target.id,
expression=value,
type=cast(VariableType | AllTypes, type_),
)


def _convert_assignment(node: ast.Assign) -> Assignment:
Expand All @@ -125,7 +130,7 @@ def _convert_assignment(node: ast.Assign) -> Assignment:

# Support a few obvious ones, such as constructor calls or literal values.
type_ = None
if isinstance(value, get_args(Value)):
if isinstance(value, Value):
type_ = value.type
elif isinstance(value, FunctionCall) and value.type == FunctionType.CONSTRUCTOR:
type_ = VariableType(data=value.name)
Expand All @@ -135,6 +140,7 @@ def _convert_assignment(node: ast.Assign) -> Assignment:
f"Could not deduce the type of variable {variable.id}: add a type annotation."
)

assert isinstance(type_, AllTypes | VariableType)
return Assignment(variable=variable.id, expression=value, type=type_)


Expand Down Expand Up @@ -162,7 +168,8 @@ def _convert_call(node: ast.Call) -> FunctionCall:
for keyword in node.keywords:
arguments.append(
NamedArgument(
name=keyword.arg, value=_convert_expression(keyword.value, False)
name=cast(str, keyword.arg),
value=_convert_expression(keyword.value, False),
)
)

Expand All @@ -184,6 +191,7 @@ def _convert_constant(node: ast.Constant) -> Value:
def _convert_expression(node: ast.expr, is_return: bool) -> Expression:
if _is_type_cast(node):
assert isinstance(node, ast.Call)
assert isinstance(node.func, ast.Name)

# "Casts" of sequence types can also be used a constructor for an empty sequence.
# For example, "set()", "map()", ...
Expand All @@ -210,7 +218,7 @@ def _convert_expression(node: ast.expr, is_return: bool) -> Expression:
subexpression = node.args[0]
value = _convert_expression(subexpression, is_return)

if not isinstance(value, get_args(Value)):
if not isinstance(value, Value):
raise InvalidDslError(
"The argument of a cast function must resolve to a value."
)
Expand Down Expand Up @@ -254,7 +262,8 @@ def _convert_expression(node: ast.expr, is_return: bool) -> Expression:
elif isinstance(node, ast.Dict):
elements = [
ObjectKeyValuePair(
_convert_expression(k, is_return), _convert_expression(v, is_return)
key=_convert_expression(cast(ast.expr, k), is_return),
value=_convert_expression(v, is_return),
)
for k, v in zip(node.keys, node.values)
]
Expand All @@ -269,6 +278,7 @@ def _convert_expression(node: ast.expr, is_return: bool) -> Expression:
value = _convert_constant(node.operand)
if not isinstance(value, NumberType):
raise InvalidDslError("'-' is only supported on literal numbers")
assert isinstance(value.data, Decimal | int | float)
return NumberType(type=value.type, data=-value.data)
else:
raise InvalidDslError(f"Unsupported expression type: {type(node)}")
Expand Down Expand Up @@ -303,6 +313,16 @@ def _translate_to_ast(node: ast.Interactive, is_return: bool) -> Statement:
return _convert_statement(statement_or_expression)


@overload
def parse_string(code: str, is_return: Literal[True]) -> Value:
...


@overload
def parse_string(code: str, is_return: Literal[False] = False) -> Statement:
...


def parse_string(code: str, is_return=False) -> Statement:
"""
Parse a string with Python code into our AST.
Expand Down
Loading

0 comments on commit 411f5b1

Please sign in to comment.