Compare commits
4
Commits
4d343a61ff
...
c3b243288d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b243288d
|
||
|
|
cd9b80d22b
|
||
|
|
21b648e18f
|
||
|
|
742693fa38
|
+844
-807
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -624,7 +624,7 @@ For example:
|
|||||||
|
|
||||||
== Control flow
|
== Control flow
|
||||||
|
|
||||||
Some control flow features are supported. For the limited code of this project, not all constructs are supported. The following are those currently handled and typ checked by Midas.
|
Some control flow features are supported. For the limited code of this project, not all constructs are supported. The following are those currently handled and type checked by Midas.
|
||||||
|
|
||||||
=== `if` / `elif` / `else` <if-else>
|
=== `if` / `elif` / `else` <if-else>
|
||||||
|
|
||||||
@@ -757,7 +757,7 @@ If the value passed to `cast` or `unsafe_cast` is a literal (e.g. an integer, a
|
|||||||
|
|
||||||
Vanilla Python already lets you use type hints to specify the type of variables and function parameters.
|
Vanilla Python already lets you use type hints to specify the type of variables and function parameters.
|
||||||
|
|
||||||
Midas use them to type check your code. Additionally, it allows you to use a special syntax to define a `Frame` types directly in these annotations.
|
Midas use them to type check your code. Additionally, it allows you to use a special syntax to define a `Frame` type directly in these annotations.
|
||||||
|
|
||||||
Because these annotations are not interpretable by Python, your integrated type checker might complain loudly about them being invalid.
|
Because these annotations are not interpretable by Python, your integrated type checker might complain loudly about them being invalid.
|
||||||
A workaround is to silence it by adding a type comment at the end of the line, as shown in @silence-errors.
|
A workaround is to silence it by adding a type comment at the end of the line, as shown in @silence-errors.
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
set document(
|
set document(
|
||||||
title: title,
|
title: title,
|
||||||
author: author,
|
author: author,
|
||||||
|
date: none,
|
||||||
)
|
)
|
||||||
set text(
|
set text(
|
||||||
font: "Source Sans 3",
|
font: "Source Sans 3",
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ Circular dependencies and diamond inheritance MUST be avoided
|
|||||||
|
|
||||||
|
|
||||||
def define_builtins(reg: TypesRegistry):
|
def define_builtins(reg: TypesRegistry):
|
||||||
"""Define builtin types and operations"""
|
"""Define builtin types and operations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reg (TypesRegistry): the types registry
|
||||||
|
"""
|
||||||
any = reg.define_type("Any", TopType())
|
any = reg.define_type("Any", TopType())
|
||||||
unit = reg.define_type("None", UnitType())
|
unit = reg.define_type("None", UnitType())
|
||||||
object = reg.define_type("object", BaseType(name="object"))
|
object = reg.define_type("object", BaseType(name="object"))
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ class CallDispatcher(Generic[E]):
|
|||||||
self.logger: logging.Logger = logging.getLogger("CallDispatcher")
|
self.logger: logging.Logger = logging.getLogger("CallDispatcher")
|
||||||
|
|
||||||
def set_reporter(self, reporter: FileReporter):
|
def set_reporter(self, reporter: FileReporter):
|
||||||
|
"""Set the current reporter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the new file reporter
|
||||||
|
"""
|
||||||
self.reporter = reporter
|
self.reporter = reporter
|
||||||
|
|
||||||
def get_result(
|
def get_result(
|
||||||
@@ -123,8 +128,8 @@ class CallDispatcher(Generic[E]):
|
|||||||
Args:
|
Args:
|
||||||
location (Location): the call location
|
location (Location): the call location
|
||||||
callee (Type): the called function
|
callee (Type): the called function
|
||||||
positional (list[TypedExpr]): the list of positional arguments
|
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||||
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
keywords (dict[str, TypedExpr[E]]): the map of keyword arguments
|
||||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -250,7 +255,7 @@ class CallDispatcher(Generic[E]):
|
|||||||
"""Check whether the passed argument types correspond to their matched parameter definitions
|
"""Check whether the passed argument types correspond to their matched parameter definitions
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
arguments (list[MappedArgument]): the list of argument/parameter pairs
|
arguments (list[MappedArgument[E]]): the list of argument/parameter pairs
|
||||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -286,8 +291,8 @@ class CallDispatcher(Generic[E]):
|
|||||||
Args:
|
Args:
|
||||||
overloads (list[Type]): the list of possible overloads
|
overloads (list[Type]): the list of possible overloads
|
||||||
location (Location): the call location
|
location (Location): the call location
|
||||||
positional (list[TypedExpr]): the list of positional arguments
|
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||||
keywords (dict[str, TypedExpr]): the map of keywords arguments
|
keywords (dict[str, TypedExpr[E]]): the map of keywords arguments
|
||||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -385,8 +390,8 @@ class CallDispatcher(Generic[E]):
|
|||||||
Args:
|
Args:
|
||||||
function (Function): the function definition
|
function (Function): the function definition
|
||||||
location (Location): the call location
|
location (Location): the call location
|
||||||
positional (list[TypedExpr]): the list of positional arguments
|
positional (list[TypedExpr[E]]): the list of positional arguments
|
||||||
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
keywords (dict[str, TypedExpr[E]]): the map of keyword arguments
|
||||||
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
report_errors (bool, optional): whether type errors should be reported as diagnostics. Defaults to True.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -514,8 +519,8 @@ class CallDispatcher(Generic[E]):
|
|||||||
function / a subtype of another.
|
function / a subtype of another.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mapped1 (list[MappedArgument]): the first argument mappings (subtype)
|
mapped1 (list[MappedArgument[E]]): the first argument mappings (subtype)
|
||||||
mapped2 (list[MappedArgument]): the second argument mappings (supertype)
|
mapped2 (list[MappedArgument[E]]): the second argument mappings (supertype)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: `True` if `mapped1` is a subtype of `mapped2`, `False` otherwise
|
bool: `True` if `mapped1` is a subtype of `mapped2`, `False` otherwise
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ class Evaluator(m.Expr.Visitor[Any]):
|
|||||||
"""Evaluate a predicate function call
|
"""Evaluate a predicate function call
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
location (Location): the location of the call expression
|
||||||
predicate (Predicate): the predicate to evaluate
|
predicate (Predicate): the predicate to evaluate
|
||||||
args (list[Any]): a list of positional arguments
|
args (list[Any]): a list of positional arguments
|
||||||
kwargs (dict[str, Any]): a map of keyword arguments
|
kwargs (dict[str, Any]): a map of keyword arguments
|
||||||
@@ -234,6 +235,7 @@ class Evaluator(m.Expr.Visitor[Any]):
|
|||||||
is set in the context using :func:`set_value` with the parameter's name
|
is set in the context using :func:`set_value` with the parameter's name
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
location (Location): the location of the call expression
|
||||||
function (Function): the called function
|
function (Function): the called function
|
||||||
args (list[Any]): a list of positional arguments
|
args (list[Any]): a list of positional arguments
|
||||||
kwargs (dict[str, Any]): a map of keyword arguments
|
kwargs (dict[str, Any]): a map of keyword arguments
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ColumnManager:
|
|||||||
Args:
|
Args:
|
||||||
reporter (FileReporter): the file reporter to use for diagnostics
|
reporter (FileReporter): the file reporter to use for diagnostics
|
||||||
location (Location): the subscript's location
|
location (Location): the subscript's location
|
||||||
column (DataFrameType): the column type
|
column (ColumnType): the column type
|
||||||
index (TypedExpr): the index
|
index (TypedExpr): the index
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -353,10 +353,12 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
|
|||||||
call (Call): the call object
|
call (Call): the call object
|
||||||
kwargs (list[Function.Parameter], optional): a list of extra
|
kwargs (list[Function.Parameter], optional): a list of extra
|
||||||
keyword-only parameters. Defaults to [].
|
keyword-only parameters. Defaults to [].
|
||||||
formula (Callable[[Type], Formula], optional): optional formula
|
formula (Optional[Callable[[Type], Formula]], optional):
|
||||||
builder function to compute the return type. If set, the function
|
optional formula builder function to compute the return type.<br>
|
||||||
should accept the inner column type and return a formula.
|
If set, the function should accept the inner column type and
|
||||||
If `None`, the result is typed as `Column[Any]`. Defaults to None.
|
return a formula.<br>
|
||||||
|
If `None`, the result is typed as `Column[Any]`.
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Type: the result type
|
Type: the result type
|
||||||
|
|||||||
+27
-9
@@ -486,11 +486,10 @@ class PythonTyper(
|
|||||||
self._assign_sub(location, var, index, value_type)
|
self._assign_sub(location, var, index, value_type)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
if not isinstance(target, p.VariableExpr):
|
self.logger.warning(f"Unsupported assignment to {target}")
|
||||||
self.logger.warning(f"Unsupported assignment to {target}")
|
self.reporter.warning(
|
||||||
self.reporter.warning(
|
target.location, f"Unsupported assignment to {target}"
|
||||||
target.location, f"Unsupported assignment to {target}"
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def _assign_var(self, location: Location, target: p.VariableExpr, value_type: Type):
|
def _assign_var(self, location: Location, target: p.VariableExpr, value_type: Type):
|
||||||
"""Type check assignment to the given target
|
"""Type check assignment to the given target
|
||||||
@@ -518,11 +517,12 @@ class PythonTyper(
|
|||||||
def _assign_attr(
|
def _assign_attr(
|
||||||
self, location: Location, object: p.Expr, name: str, value_type: Type
|
self, location: Location, object: p.Expr, name: str, value_type: Type
|
||||||
):
|
):
|
||||||
"""Type check assignment to the given target
|
"""Type check assignment to the given attribute target
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
location (Location): the location of the assignment
|
location (Location): the location of the assignment
|
||||||
target (p.VariableExpr): the assignment's target
|
object (p.Expr): the target attribute's owner object
|
||||||
|
name (str): the target attribute's name
|
||||||
value_type (Type): the value to be assigned
|
value_type (Type): the value to be assigned
|
||||||
"""
|
"""
|
||||||
object_type: Type = self.type_of(object)
|
object_type: Type = self.type_of(object)
|
||||||
@@ -544,11 +544,15 @@ class PythonTyper(
|
|||||||
index: p.Expr,
|
index: p.Expr,
|
||||||
value_type: Type,
|
value_type: Type,
|
||||||
):
|
):
|
||||||
"""Type check assignment to the given target
|
"""Type check assignment to the given subscript target
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
location (Location): the location of the assignment
|
location (Location): the location of the assignment
|
||||||
target (p.VariableExpr): the assignment's target
|
var (p.VariableExpr): the target subscript's owner. We only allow
|
||||||
|
a variable expression here because we might modify its type (for
|
||||||
|
example when assigning a column to a dataframe) and reference
|
||||||
|
types are not implemented
|
||||||
|
index (p.Expr): the target subscript's index expression
|
||||||
value_type (Type): the value to be assigned
|
value_type (Type): the value to be assigned
|
||||||
"""
|
"""
|
||||||
var_type: Type = self.type_of(var)
|
var_type: Type = self.type_of(var)
|
||||||
@@ -690,6 +694,20 @@ class PythonTyper(
|
|||||||
right: TypedExpr,
|
right: TypedExpr,
|
||||||
method: str,
|
method: str,
|
||||||
) -> Type:
|
) -> Type:
|
||||||
|
"""Compute the result type of a binary operation method call
|
||||||
|
|
||||||
|
This method is called for dunder methods called by binary operators
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the operation
|
||||||
|
expr (p.Expr): the expression which triggered this resolution
|
||||||
|
left (TypedExpr): the left operand
|
||||||
|
right (TypedExpr): the right operand
|
||||||
|
method (str): the method name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the result type
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self.call_method(
|
return self.call_method(
|
||||||
location=location,
|
location=location,
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ class FileReporter:
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def with_context(self, ctx: str):
|
def with_context(self, ctx: str):
|
||||||
|
"""Push given context for reports inside this manager and pop it on exit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx (str): the context to temporarily push on the stack
|
||||||
|
"""
|
||||||
self._context.append(ctx)
|
self._context.append(ctx)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import ast
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArgDoc:
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
optional: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Param:
|
||||||
|
name: str
|
||||||
|
annotation: Optional[str]
|
||||||
|
optional: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Checker(ast.NodeVisitor):
|
||||||
|
def _get_args(self, docstring: str) -> list[ArgDoc]:
|
||||||
|
args: list[ArgDoc] = []
|
||||||
|
|
||||||
|
in_args: bool = False
|
||||||
|
for line in docstring.splitlines():
|
||||||
|
if not in_args:
|
||||||
|
if line == "Args:":
|
||||||
|
in_args = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# End of args
|
||||||
|
if not line.startswith(" "):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Continuation line
|
||||||
|
if line.startswith(" "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
line = line.strip()
|
||||||
|
m = re.match(r"(?P<name>\w+) \((?P<type>.*?)(?P<opt>, optional)?\):", line)
|
||||||
|
if m is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
args.append(
|
||||||
|
ArgDoc(
|
||||||
|
name=m.group("name"),
|
||||||
|
type=m.group("type"),
|
||||||
|
optional=m.group("opt") is not None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def log(self, node: ast.FunctionDef, msg: str):
|
||||||
|
loc: str = f"{node.name} L{node.lineno}:{node.col_offset+1}"
|
||||||
|
print(f" ({loc}) {msg}")
|
||||||
|
|
||||||
|
def _is_ignored(self, node: ast.FunctionDef) -> bool:
|
||||||
|
name: str = node.name
|
||||||
|
if name.startswith("visit_") or name.startswith("_visit_"):
|
||||||
|
return True
|
||||||
|
if name.startswith("parse_") or name.startswith("_parse_"):
|
||||||
|
return True
|
||||||
|
if name.startswith("_print"):
|
||||||
|
return True
|
||||||
|
if name.startswith("_write"):
|
||||||
|
return True
|
||||||
|
if name.startswith("__") and name.endswith("__"):
|
||||||
|
return True
|
||||||
|
if name == "accept":
|
||||||
|
return True
|
||||||
|
node.decorator_list
|
||||||
|
match node:
|
||||||
|
case ast.FunctionDef(
|
||||||
|
decorator_list=[
|
||||||
|
ast.Call(
|
||||||
|
func=ast.Name(id="method"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
|
||||||
|
docstring: Optional[str] = ast.get_docstring(node)
|
||||||
|
func_name: str = node.name
|
||||||
|
if docstring is None:
|
||||||
|
if not self._is_ignored(node):
|
||||||
|
self.log(node, f"Missing docstring for function {func_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
args_doc: list[ArgDoc] = self._get_args(docstring)
|
||||||
|
by_name: dict[str, ArgDoc] = {}
|
||||||
|
for doc in args_doc:
|
||||||
|
if doc.name in by_name:
|
||||||
|
self.log(node, f"Multiple documentation lines for argument {doc.name}")
|
||||||
|
by_name[doc.name] = doc
|
||||||
|
|
||||||
|
all_params: list[Param] = []
|
||||||
|
|
||||||
|
pos_args: list[ast.arg] = node.args.posonlyargs
|
||||||
|
mixed_args: list[ast.arg] = node.args.args
|
||||||
|
kw_args: list[ast.arg] = node.args.kwonlyargs
|
||||||
|
|
||||||
|
def add_param(arg: ast.arg, optional: bool):
|
||||||
|
all_params.append(
|
||||||
|
Param(
|
||||||
|
name=arg.arg,
|
||||||
|
annotation=(
|
||||||
|
ast.unparse(arg.annotation)
|
||||||
|
if arg.annotation is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
optional=optional,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
n_pos: int = len(pos_args) + len(mixed_args)
|
||||||
|
for i, arg in enumerate(pos_args):
|
||||||
|
j: int = n_pos - i - 1
|
||||||
|
optional: bool = j < len(node.args.defaults)
|
||||||
|
add_param(arg, optional)
|
||||||
|
|
||||||
|
for i, arg in enumerate(mixed_args):
|
||||||
|
j: int = len(mixed_args) - i - 1
|
||||||
|
optional: bool = j < len(node.args.defaults)
|
||||||
|
add_param(arg, optional)
|
||||||
|
|
||||||
|
for arg, default in zip(kw_args, node.args.kw_defaults):
|
||||||
|
optional: bool = default is not None
|
||||||
|
add_param(arg, optional)
|
||||||
|
|
||||||
|
for param in all_params:
|
||||||
|
doc: Optional[ArgDoc] = by_name.get(param.name, None)
|
||||||
|
if doc is None:
|
||||||
|
if param.name not in {"self", "cls"}:
|
||||||
|
self.log(
|
||||||
|
node, f"Missing documentation for parameter '{param.name}'"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if doc.name != param.name:
|
||||||
|
self.log(node, f"Documentation mismatch for '{param.name}': wrong name")
|
||||||
|
|
||||||
|
if doc.type != param.annotation:
|
||||||
|
self.log(node, f"Documentation mismatch for '{param.name}': wrong type")
|
||||||
|
|
||||||
|
if doc.optional != param.optional:
|
||||||
|
self.log(
|
||||||
|
node,
|
||||||
|
f"Documentation mismatch for '{param.name}': wrong optionality",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_file(path: Path):
|
||||||
|
source: str = path.read_text()
|
||||||
|
tree = ast.parse(source)
|
||||||
|
checker = Checker()
|
||||||
|
checker.visit(tree)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
folder: Path = (Path(__file__).parent.parent / "midas").resolve()
|
||||||
|
all_files = folder.rglob("*.py")
|
||||||
|
for f in all_files:
|
||||||
|
print(f.relative_to(folder))
|
||||||
|
check_file(f)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user