3 Commits
8 changed files with 2056 additions and 278 deletions
+164 -31
View File
@@ -7,7 +7,7 @@ import midas.ast.python as p
from midas.ast.location import Location from midas.ast.location import Location
from midas.checker.dispatcher import CallResult from midas.checker.dispatcher import CallResult
from midas.checker.frames.utils import MethodRegistry, method from midas.checker.frames.utils import MethodRegistry, method
from midas.checker.types import ColumnGroupBy, Function, Type from midas.checker.types import ColumnGroupBy, ColumnType, Function, TopType, Type
if TYPE_CHECKING: if TYPE_CHECKING:
from midas.checker.python import TypedExpr from midas.checker.python import TypedExpr
@@ -28,37 +28,46 @@ class Call:
class ColumnGroupByMethodRegistry(MethodRegistry[Call]): class ColumnGroupByMethodRegistry(MethodRegistry[Call]):
@method() NAMED_ARGS: dict[str, str] = {
def mean(self, call: Call) -> Type: "numeric_only": "bool",
bool_ = self.types.get_type("bool") "skipna": "bool",
"engine": "str",
"engine_kwargs": "dict",
}
def _aggregate(
self,
call: Call,
args: list[str | tuple[str, str, bool]] = [],
*,
preserve_inner_type: bool = False,
) -> Type:
real_args: list[Function.Argument] = []
for i, arg in enumerate(args):
match arg:
case str() as name:
arg = Function.Argument(
pos=i,
name=name,
type=self.types.get_type(self.NAMED_ARGS[name]),
required=False,
)
case (name, type, required):
arg = Function.Argument(
pos=i,
name=name,
type=self.types.get_type(type),
required=required,
)
real_args.append(arg)
signature = Function( signature = Function(
args=[ args=real_args,
Function.Argument( returns=(
pos=0, call.groupby.column
name="numeric_only", if preserve_inner_type
type=bool_, else ColumnType(type=TopType())
required=False, ),
),
Function.Argument(
pos=1,
name="skipna",
type=bool_,
required=False,
),
Function.Argument(
pos=2,
name="engine",
type=self.types.get_type("str"),
required=False,
),
Function.Argument(
pos=3,
name="engine_kwargs",
type=self.types.get_type("dict"),
required=False,
),
],
returns=call.groupby.column,
) )
result: CallResult = self.dispatcher.get_result( result: CallResult = self.dispatcher.get_result(
@@ -68,3 +77,127 @@ class ColumnGroupByMethodRegistry(MethodRegistry[Call]):
keywords=call.keywords, keywords=call.keywords,
) )
return result.result return result.result
@method()
def kurt(self, call: Call) -> Type:
return self._aggregate(
call,
["skipna", "numeric_only"],
)
@method()
def max(self, call: Call) -> Type:
return self._aggregate(
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
"engine",
"engine_kwargs",
],
preserve_inner_type=True,
)
@method()
def mean(self, call: Call) -> Type:
return self._aggregate(
call,
["numeric_only", "skipna", "engine", "engine_kwargs"],
)
@method()
def median(self, call: Call) -> Type:
return self._aggregate(
call,
["numeric_only", "skipna"],
preserve_inner_type=True,
)
@method()
def min(self, call: Call) -> Type:
return self._aggregate(
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
"engine",
"engine_kwargs",
],
preserve_inner_type=True,
)
@method()
def prod(self, call: Call) -> Type:
return self._aggregate(
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
],
)
@method()
def std(self, call: Call) -> Type:
return self._aggregate(
call,
[
(
"ddof",
"int",
False,
),
"engine",
"engine_kwargs",
"numeric_only",
"skipna",
],
)
@method()
def sum(self, call: Call) -> Type:
return self._aggregate(
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
"engine",
"engine_kwargs",
],
)
@method()
def var(self, call: Call) -> Type:
return self._aggregate(
call,
[
(
"var",
"int",
False,
),
"engine",
"engine_kwargs",
"numeric_only",
"skipna",
],
)
+18 -12
View File
@@ -160,7 +160,13 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
def eq(self, call: Call) -> Type: def eq(self, call: Call) -> Type:
return self._element_wise(call, "__eq__") return self._element_wise(call, "__eq__")
def _statistical(self, call: Call, kwargs: list[Function.Argument] = []) -> Type: def _aggregate(
self,
call: Call,
kwargs: list[Function.Argument] = [],
*,
preserve_inner_type: bool = False,
) -> Type:
signature = Function( signature = Function(
kw_args=[ kw_args=[
Function.Argument( Function.Argument(
@@ -171,7 +177,7 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
), ),
*kwargs, *kwargs,
], ],
returns=ColumnType(type=TopType()), returns=call.column if preserve_inner_type else ColumnType(type=TopType()),
) )
result: CallResult = self.dispatcher.get_result( result: CallResult = self.dispatcher.get_result(
@@ -184,35 +190,35 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
@method("kurtosis", "kurt") @method("kurtosis", "kurt")
def kurtosis(self, call: Call) -> Type: def kurtosis(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call)
@method() @method()
def max(self, call: Call) -> Type: def max(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call, preserve_inner_type=True)
@method() @method()
def mean(self, call: Call) -> Type: def mean(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call)
@method() @method()
def median(self, call: Call) -> Type: def median(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call, preserve_inner_type=True)
@method() @method()
def min(self, call: Call) -> Type: def min(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call, preserve_inner_type=True)
@method() @method()
def mode(self, call: Call) -> Type: def mode(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call, preserve_inner_type=True)
@method("product", "prod") @method("product", "prod")
def product(self, call: Call) -> Type: def product(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call)
@method() @method()
def std(self, call: Call) -> Type: def std(self, call: Call) -> Type:
return self._statistical( return self._aggregate(
call, call,
[ [
Function.Argument( Function.Argument(
@@ -226,11 +232,11 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
@method() @method()
def sum(self, call: Call) -> Type: def sum(self, call: Call) -> Type:
return self._statistical(call) return self._aggregate(call)
@method() @method()
def var(self, call: Call) -> Type: def var(self, call: Call) -> Type:
return self._statistical( return self._aggregate(
call, call,
[ [
Function.Argument( Function.Argument(
+39 -131
View File
@@ -5,9 +5,15 @@ from typing import TYPE_CHECKING
import midas.ast.python as p import midas.ast.python as p
from midas.ast.location import Location from midas.ast.location import Location
from midas.checker.dispatcher import CallResult
from midas.checker.frames.utils import MethodRegistry, method from midas.checker.frames.utils import MethodRegistry, method
from midas.checker.types import FrameGroupBy, Function, Type from midas.checker.types import (
ColumnGroupBy,
ColumnType,
DataFrameType,
FrameGroupBy,
Type,
UnknownType,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from midas.checker.python import TypedExpr from midas.checker.python import TypedExpr
@@ -35,161 +41,63 @@ class FrameGroupByMethodRegistry(MethodRegistry[Call]):
"engine_kwargs": "dict", "engine_kwargs": "dict",
} }
def _aggregate( def _aggregate(self, call: Call, method: str) -> Type:
self, call: Call, args: list[str | tuple[str, str, bool]] = [] new_columns: list[DataFrameType.Column] = []
) -> Type:
real_args: list[Function.Argument] = []
for i, arg in enumerate(args):
match arg:
case str() as name:
arg = Function.Argument(
pos=i,
name=name,
type=self.types.get_type(self.NAMED_ARGS[name]),
required=False,
)
case (name, type, required):
arg = Function.Argument(
pos=i,
name=name,
type=self.types.get_type(type),
required=required,
)
real_args.append(arg)
signature = Function( for column in call.groupby.frame.columns:
args=real_args, column_groupby: ColumnGroupBy = ColumnGroupBy(column=column.type)
returns=call.groupby.frame, result_type: Type = self.typer.call_method(
) location=call.location,
call_expr=call.call_expr,
obj=(call.groupby_expr, column_groupby),
method_name=method,
positional=call.positional,
keywords=call.keywords,
)
if not isinstance(result_type, ColumnType):
result_type = ColumnType(type=UnknownType())
new_columns.append(
DataFrameType.Column(
index=column.index,
name=column.name,
type=result_type,
)
)
result: CallResult = self.dispatcher.get_result( return DataFrameType(columns=new_columns)
location=call.location,
callee=signature,
positional=call.positional,
keywords=call.keywords,
)
return result.result
@method() @method()
def kurt(self, call: Call) -> Type: def kurt(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "kurt")
call,
[
"skipna",
"numeric_only",
],
)
@method() @method()
def max(self, call: Call) -> Type: def max(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "max")
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
"engine",
"engine_kwargs",
],
)
@method() @method()
def mean(self, call: Call) -> Type: def mean(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "mean")
call,
["numeric_only", "skipna", "engine", "engine_kwargs"],
)
@method() @method()
def median(self, call: Call) -> Type: def median(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "median")
call,
["numeric_only", "skipna"],
)
@method() @method()
def min(self, call: Call) -> Type: def min(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "min")
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
"engine",
"engine_kwargs",
],
)
@method() @method()
def prod(self, call: Call) -> Type: def prod(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "prod")
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
],
)
@method() @method()
def std(self, call: Call) -> Type: def std(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "std")
call,
[
(
"ddof",
"int",
False,
),
"engine",
"engine_kwargs",
"numeric_only",
"skipna",
],
)
@method() @method()
def sum(self, call: Call) -> Type: def sum(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "sum")
call,
[
"numeric_only",
(
"min_count",
"int",
False,
),
"skipna",
"engine",
"engine_kwargs",
],
)
@method() @method()
def var(self, call: Call) -> Type: def var(self, call: Call) -> Type:
return self._aggregate( return self._aggregate(call, "var")
call,
[
(
"var",
"int",
False,
),
"engine",
"engine_kwargs",
"numeric_only",
"skipna",
],
)
+10 -19
View File
@@ -222,7 +222,7 @@ class PythonTyper(
method_name: str, method_name: str,
positional: list[TypedExpr], positional: list[TypedExpr],
keywords: dict[str, TypedExpr], keywords: dict[str, TypedExpr],
) -> Optional[Type]: ) -> Type:
unfolded: Type = unfold_type(obj[1]) unfolded: Type = unfold_type(obj[1])
match unfolded: match unfolded:
case DataFrameType(): case DataFrameType():
@@ -580,9 +580,8 @@ class PythonTyper(
right: TypedExpr, right: TypedExpr,
method: str, method: str,
) -> Type: ) -> Type:
result: Optional[Type]
try: try:
result = self.call_method( return self.call_method(
location=location, location=location,
call_expr=expr, call_expr=expr,
obj=left, obj=left,
@@ -597,8 +596,6 @@ class PythonTyper(
) )
return UnknownType() return UnknownType()
return result or UnknownType()
def visit_unary_expr(self, expr: p.UnaryExpr) -> Type: def visit_unary_expr(self, expr: p.UnaryExpr) -> Type:
method: Optional[str] = PY_UNARY_METHODS.get(expr.operator.__class__) method: Optional[str] = PY_UNARY_METHODS.get(expr.operator.__class__)
if method is None: if method is None:
@@ -610,9 +607,8 @@ class PythonTyper(
operand: Type = self.type_of(expr.right) operand: Type = self.type_of(expr.right)
result: Optional[Type]
try: try:
result = self.call_method( return self.call_method(
location=expr.location, location=expr.location,
call_expr=expr, call_expr=expr,
obj=(expr.right, operand), obj=(expr.right, operand),
@@ -627,8 +623,6 @@ class PythonTyper(
) )
return UnknownType() return UnknownType()
return result or UnknownType()
def visit_call_expr(self, expr: p.CallExpr) -> Type: def visit_call_expr(self, expr: p.CallExpr) -> Type:
match expr.callee: match expr.callee:
case p.VariableExpr(name="TypeVar"): case p.VariableExpr(name="TypeVar"):
@@ -644,16 +638,13 @@ class PythonTyper(
match expr.callee: match expr.callee:
case p.GetExpr(object=obj, name=method): case p.GetExpr(object=obj, name=method):
obj_type: Type = self.type_of(obj) obj_type: Type = self.type_of(obj)
return ( return self.call_method(
self.call_method( location=expr.location,
location=expr.location, call_expr=expr,
call_expr=expr, obj=(obj, obj_type),
obj=(obj, obj_type), method_name=method,
method_name=method, positional=positional,
positional=positional, keywords=keywords,
keywords=keywords,
)
or UnknownType()
) )
callee: Type = self.type_of(expr.callee) callee: Type = self.type_of(expr.callee)
+43
View File
@@ -0,0 +1,43 @@
from typing import Type
from midas.cli.ansi import Ansi
from tests.base import Tester
from tests.checker import CheckerTester
from tests.generator import GeneratorTester
from tests.midas import MidasTester
from tests.python import PythonTester
def print_banner(name: str):
horizontal: str = "+" + "-" * (len(name) + 2) + "+"
print(horizontal)
print(f"| {name} |")
print(horizontal)
def run_tests(tester_cls: Type[Tester]) -> bool:
print_banner(tester_cls.__name__)
tester: Tester = tester_cls()
success: bool = tester.run_all_tests()
print()
return success
def main():
testers: list[Type[Tester]] = [
PythonTester,
MidasTester,
CheckerTester,
GeneratorTester,
]
success: bool = all(map(run_tests, testers))
if success:
print(Ansi.FG(Ansi.BRIGHT_GREEN) + "All tests passed!" + Ansi.RESET)
else:
print(Ansi.FG(Ansi.BRIGHT_RED) + "Some tests failed!" + Ansi.RESET)
if __name__ == "__main__":
main()
+9 -3
View File
@@ -7,6 +7,8 @@ from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import Iterator, Protocol from typing import Iterator, Protocol
from midas.cli.ansi import Ansi
class CaseResult(Protocol): class CaseResult(Protocol):
def dumps(self) -> str: ... def dumps(self) -> str: ...
@@ -44,8 +46,11 @@ class Tester(ABC):
print(rule) print(rule)
for i, test in enumerate(tests): for i, test in enumerate(tests):
print(f"Case {i+1}/{n}: {test.resolve().relative_to(self.CASES_DIR)}") path: Path = test.resolve().relative_to(self.CASES_DIR)
print(f"{Ansi.FG(Ansi.BRIGHT_CYAN)}Case {i+1}/{n}: {path}{Ansi.RESET}")
print(Ansi.DIM, end="")
success: bool = self._run_test(test) success: bool = self._run_test(test)
print(Ansi.RESET, end="")
if success: if success:
successes += 1 successes += 1
else: else:
@@ -146,8 +151,9 @@ class Tester(ABC):
if not success: if not success:
sys.exit(1) sys.exit(1)
case None: case None:
print("No subcommand provided. Available subcommands: run, update") success: bool = tester.run_all_tests()
sys.exit(1) if not success:
sys.exit(1)
case _: case _:
print(f"Unknown subcommand '{args.subcommand}'") print(f"Unknown subcommand '{args.subcommand}'")
sys.exit(1) sys.exit(1)
+60 -10
View File
@@ -38,14 +38,64 @@ _ = df1.sum()
_ = df1.var() _ = df1.var()
# Groupby # Groupby
gb = df1.groupby(by="a") df_gb = df1.groupby(by="a")
_ = gb.kurt() _ = df_gb.kurt()
_ = gb.max() _ = df_gb.max()
_ = gb.mean() _ = df_gb.mean()
_ = gb.median() _ = df_gb.median()
_ = gb.min() _ = df_gb.min()
_ = gb.prod() _ = df_gb.prod()
_ = gb.std() _ = df_gb.std()
_ = gb.sum() _ = df_gb.sum()
_ = gb.var() _ = df_gb.var()
# Columns
col1 = df1["a"]
col2 = df1["a"]
# Arithmetic
_ = col1 + col2
_ = col1 - col2
_ = col1 * col2
_ = col1 / col2
_ = col1 // col2
_ = col1 % col2
_ = col1**col2
# Comparisons
_ = col1 < col2
_ = col1 > col2
_ = col1 <= col2
_ = col1 >= col2
_ = col1 != col2
_ = col1 == col2
# Aggregate
_ = col1.kurt()
_ = col1.kurtosis()
_ = col1.max()
_ = col1.mean()
_ = col1.median()
_ = col1.min()
_ = col1.mode()
_ = col1.prod()
_ = col1.product()
_ = col1.std()
_ = col1.sum()
_ = col1.var()
# Groupby
col_gb = col1.groupby(level=0)
_ = col_gb.kurt()
_ = col_gb.max()
_ = col_gb.mean()
_ = col_gb.median()
_ = col_gb.min()
_ = col_gb.prod()
_ = col_gb.std()
_ = col_gb.sum()
_ = col_gb.var()
File diff suppressed because it is too large Load Diff