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

Add type checker #407

Merged
merged 2 commits into from
Aug 9, 2023
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
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:
...
Dismissed Show dismissed Hide dismissed


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


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