Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c217a335
|
||
|
|
5b3e87afcb
|
||
|
|
894d5a7196
|
||
|
|
eb809c6341
|
||
|
|
bd68d1003f
|
||
|
|
72c9236650
|
||
|
|
90051c7981
|
||
|
|
dd1e2e693c
|
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
from midas.ast.location import Location
|
||||
from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.reporter import FileReporter
|
||||
from midas.checker.types import (
|
||||
ColumnType,
|
||||
DataFrameType,
|
||||
Function,
|
||||
Type,
|
||||
UnknownType,
|
||||
unfold_type,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from midas.checker.python import PythonTyper, TypedExpr
|
||||
|
||||
|
||||
@staticmethod
|
||||
def frame_method(*names: str):
|
||||
def wrapper(func):
|
||||
names_: tuple[str, ...] = names
|
||||
if len(names_) == 0:
|
||||
names_ = (func.__name__,)
|
||||
setattr(func, "__method_names__", names_)
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class Call:
|
||||
location: Location
|
||||
frame: DataFrameType
|
||||
positional: list[TypedExpr]
|
||||
keywords: dict[str, TypedExpr]
|
||||
|
||||
|
||||
class _MethodRegistryMeta(type):
|
||||
_methods: dict[str, Callable] = {}
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
name: str,
|
||||
bases: tuple[type, ...],
|
||||
namespace: dict[str, Any],
|
||||
):
|
||||
new_class = super().__new__(cls, name, bases, namespace)
|
||||
new_class._methods = {}
|
||||
for attr in namespace.values():
|
||||
if callable(attr) and hasattr(attr, "__method_names__"):
|
||||
for name in attr.__method_names__: # type: ignore
|
||||
new_class._methods[name] = attr
|
||||
return new_class
|
||||
|
||||
|
||||
class MethodRegistry(metaclass=_MethodRegistryMeta):
|
||||
def __init__(self, typer: PythonTyper) -> None:
|
||||
self.typer: PythonTyper = typer
|
||||
|
||||
@property
|
||||
def reporter(self) -> FileReporter:
|
||||
return self.typer.reporter
|
||||
|
||||
@property
|
||||
def types(self) -> TypesRegistry:
|
||||
return self.typer.types
|
||||
|
||||
def call(
|
||||
self,
|
||||
method: str,
|
||||
call: Call,
|
||||
) -> Type:
|
||||
func: Optional[Callable] = self._methods.get(method)
|
||||
if func is None:
|
||||
self.reporter.error(call.location, f"Unknown method {method}")
|
||||
return UnknownType()
|
||||
return func(self, call)
|
||||
|
||||
@frame_method("add", "__add__")
|
||||
def add(
|
||||
self,
|
||||
call: Call,
|
||||
) -> Type:
|
||||
new_columns: list[DataFrameType.Column] = []
|
||||
|
||||
by_name: dict[str, DataFrameType.Column] = {}
|
||||
frame2: Optional[DataFrameType] = None
|
||||
if len(call.positional) != 0:
|
||||
other: Type = call.positional[0][1]
|
||||
unfolded_other: Type = unfold_type(other)
|
||||
if isinstance(unfolded_other, DataFrameType):
|
||||
frame2 = unfolded_other
|
||||
by_name = {
|
||||
col.name: col for col in frame2.columns if col.name is not None
|
||||
}
|
||||
|
||||
in_frame1: set[str] = set()
|
||||
for column in call.frame.columns:
|
||||
if column.name is not None:
|
||||
in_frame1.add(column.name)
|
||||
|
||||
col_type1: Type = column.type
|
||||
col_type: Type = ColumnType(type=UnknownType())
|
||||
if column.name in by_name:
|
||||
column2 = by_name[column.name]
|
||||
col_type2: Type = column2.type
|
||||
if self.types.are_equivalent(col_type2, col_type1):
|
||||
col_type = col_type1
|
||||
|
||||
new_column = DataFrameType.Column(
|
||||
index=column.index,
|
||||
name=column.name,
|
||||
type=col_type,
|
||||
)
|
||||
new_columns.append(new_column)
|
||||
|
||||
if frame2 is not None:
|
||||
for column in frame2.columns:
|
||||
if column.name in in_frame1:
|
||||
continue
|
||||
new_columns.append(
|
||||
DataFrameType.Column(
|
||||
index=len(new_columns),
|
||||
name=column.name,
|
||||
type=ColumnType(type=UnknownType()),
|
||||
)
|
||||
)
|
||||
|
||||
signature = Function(
|
||||
args=[
|
||||
Function.Argument(
|
||||
pos=0,
|
||||
name="other",
|
||||
type=DataFrameType(columns=[]),
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
returns=DataFrameType(columns=new_columns),
|
||||
)
|
||||
|
||||
return (
|
||||
self.typer._get_call_result(
|
||||
location=call.location,
|
||||
callee=signature,
|
||||
positional=call.positional,
|
||||
keywords=call.keywords,
|
||||
)
|
||||
or UnknownType()
|
||||
)
|
||||
+26
-5
@@ -1,11 +1,15 @@
|
||||
from typing import Optional, TypeGuard, cast
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, TypeGuard, cast
|
||||
|
||||
import midas.ast.python as p
|
||||
from midas.ast.location import Location
|
||||
from midas.checker.registry import TypesRegistry
|
||||
from midas.checker.frame_methods import Call, MethodRegistry
|
||||
from midas.checker.reporter import FileReporter
|
||||
from midas.checker.types import ColumnType, DataFrameType, TupleType, Type, UnknownType
|
||||
|
||||
import midas.ast.python as p
|
||||
if TYPE_CHECKING:
|
||||
from midas.checker.python import PythonTyper, TypedExpr
|
||||
|
||||
|
||||
def is_list_of_literals(exprs: list[p.Expr]) -> TypeGuard[list[p.LiteralExpr]]:
|
||||
@@ -13,8 +17,9 @@ def is_list_of_literals(exprs: list[p.Expr]) -> TypeGuard[list[p.LiteralExpr]]:
|
||||
|
||||
|
||||
class FrameManager:
|
||||
def __init__(self, types: TypesRegistry) -> None:
|
||||
self.types: TypesRegistry = types
|
||||
def __init__(self, typer: PythonTyper) -> None:
|
||||
self.typer: PythonTyper = typer
|
||||
self.method_resolver: MethodRegistry = MethodRegistry(self.typer)
|
||||
|
||||
def assign(
|
||||
self,
|
||||
@@ -131,3 +136,19 @@ class FrameManager:
|
||||
cls, frame: DataFrameType, names: list[str]
|
||||
) -> list[Optional[ColumnType]]:
|
||||
return [cls._get_column(frame, name) for name in names]
|
||||
|
||||
def call(
|
||||
self,
|
||||
method: str,
|
||||
location: Location,
|
||||
frame: DataFrameType,
|
||||
positional: list[TypedExpr],
|
||||
keywords: dict[str, TypedExpr],
|
||||
) -> Type:
|
||||
call: Call = Call(
|
||||
location=location,
|
||||
frame=frame,
|
||||
positional=positional,
|
||||
keywords=keywords,
|
||||
)
|
||||
return self.method_resolver.call(method, call)
|
||||
|
||||
+19
-5
@@ -75,7 +75,7 @@ class PythonTyper(
|
||||
self.logger: logging.Logger = logging.getLogger("PythonTyper")
|
||||
self.reporter: FileReporter = reporter.for_file(None)
|
||||
self.types: TypesRegistry = types
|
||||
self.frame_mgr: FrameManager = FrameManager(self.types)
|
||||
self.frame_mgr: FrameManager = FrameManager(self)
|
||||
self.global_env: Environment = Preamble(self.types)
|
||||
self.env: Environment = self.global_env
|
||||
self.locals: dict[p.Expr, int] = {}
|
||||
@@ -515,13 +515,27 @@ class PythonTyper(
|
||||
case p.VariableExpr(name="TypeVar"):
|
||||
return self.define_typevar(expr) or UnknownType()
|
||||
|
||||
callee: Type = self.type_of(expr.callee)
|
||||
positional: list[TypedExpr] = [
|
||||
(arg, self.type_of(arg)) for arg in expr.arguments
|
||||
]
|
||||
keywords: dict[str, TypedExpr] = {
|
||||
name: (arg, self.type_of(arg)) for name, arg in expr.keywords.items()
|
||||
}
|
||||
|
||||
match expr.callee:
|
||||
case p.GetExpr(object=obj, name=method):
|
||||
obj_type: Type = self.type_of(obj)
|
||||
unfolded: Type = unfold_type(obj_type)
|
||||
if isinstance(unfolded, DataFrameType):
|
||||
return self.frame_mgr.call(
|
||||
method,
|
||||
expr.location,
|
||||
unfolded,
|
||||
positional,
|
||||
keywords,
|
||||
)
|
||||
|
||||
callee: Type = self.type_of(expr.callee)
|
||||
return (
|
||||
self._get_call_result(
|
||||
location=expr.location,
|
||||
@@ -626,7 +640,7 @@ class PythonTyper(
|
||||
return self.types.apply_generic(list_type, [item_type])
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Heterogeneous list items: {item_types}",
|
||||
f"Heterogeneous list items: [{', '.join(map(str, item_types))}]",
|
||||
)
|
||||
return self.types.apply_generic(list_type, [UnknownType()])
|
||||
|
||||
@@ -658,7 +672,7 @@ class PythonTyper(
|
||||
else:
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Heterogeneous dict keys: {key_types}",
|
||||
f"Heterogeneous dict keys: [{', '.join(map(str, key_types))}]",
|
||||
)
|
||||
|
||||
if len(value_types) == 1:
|
||||
@@ -666,7 +680,7 @@ class PythonTyper(
|
||||
else:
|
||||
self.reporter.error(
|
||||
expr.location,
|
||||
f"Heterogeneous dict values: {value_types}",
|
||||
f"Heterogeneous dict values: [{', '.join(map(str, value_types))}]",
|
||||
)
|
||||
return self.types.apply_generic(dict_type, [key_type, value_type])
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ from midas.checker.types import (
|
||||
AliasType,
|
||||
AppliedType,
|
||||
BaseType,
|
||||
ColumnType,
|
||||
ComplexType,
|
||||
ConstraintType,
|
||||
DataFrameType,
|
||||
ExtensionType,
|
||||
Function,
|
||||
GenericType,
|
||||
@@ -157,6 +159,24 @@ class TypesRegistry:
|
||||
return False
|
||||
return True
|
||||
|
||||
case (DataFrameType(columns=columns1), DataFrameType(columns=columns2)):
|
||||
# TODO: check order?
|
||||
by_name1: dict[str, DataFrameType.Column] = {
|
||||
col.name: col for col in columns1 if col.name is not None
|
||||
}
|
||||
for col2 in columns2:
|
||||
if col2.name not in by_name1:
|
||||
return False
|
||||
if not self.is_subtype(by_name1[col2.name].type, col2.type):
|
||||
return False
|
||||
return True
|
||||
|
||||
case (ColumnType(type=inner1), ColumnType(type=inner2)):
|
||||
# TODO: invariant, replace ColumnType with simple GenericType
|
||||
if not self.are_equivalent(inner1, inner2):
|
||||
return False
|
||||
return True
|
||||
|
||||
case (Function(), Function()):
|
||||
return self.is_func_subtype(type1, type2)
|
||||
|
||||
@@ -187,6 +207,9 @@ class TypesRegistry:
|
||||
|
||||
return False
|
||||
|
||||
def are_equivalent(self, type1: Type, type2: Type) -> bool:
|
||||
return self.is_subtype(type1, type2) and self.is_subtype(type2, type1)
|
||||
|
||||
# TODO: verify the logic in here
|
||||
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
||||
"""Check whether a function is a subtype of another
|
||||
|
||||
+25
-1
@@ -68,7 +68,7 @@ class DiagnosticPrinter:
|
||||
|
||||
loc: Location = diagnostic.location
|
||||
if loc.lineno != loc.end_lineno:
|
||||
print(diagnostic)
|
||||
self.print_multiline(lines, diagnostic, indent)
|
||||
return
|
||||
|
||||
start_offset: int = loc.col_offset
|
||||
@@ -95,3 +95,27 @@ class DiagnosticPrinter:
|
||||
print(indent_str + before + subject + after)
|
||||
print(indent_str + cursor)
|
||||
print()
|
||||
|
||||
def print_multiline(
|
||||
self, all_lines: list[str], diagnostic: Diagnostic, indent: int = 4
|
||||
):
|
||||
loc: Location = diagnostic.location
|
||||
lines: list[str] = all_lines[loc.lineno - 1 : loc.end_lineno]
|
||||
|
||||
start_offset: int = loc.col_offset
|
||||
end_offset: int = loc.end_col_offset or (start_offset + 1)
|
||||
|
||||
indent_str: str = " " * indent
|
||||
color: int = self.COLORS.get(diagnostic.type, Ansi.WHITE)
|
||||
res: str = indent_str + lines[0][:start_offset]
|
||||
res += Ansi.FG(color) + lines[0][start_offset:]
|
||||
for line in lines[1:-1]:
|
||||
res += "\n" + indent_str + line
|
||||
res += "\n" + indent_str + lines[-1][:end_offset]
|
||||
res += Ansi.RESET + lines[-1][end_offset:]
|
||||
|
||||
print(diagnostic.location_str + ":")
|
||||
print(res)
|
||||
print()
|
||||
print(Ansi.FG(color) + diagnostic.message + Ansi.RESET)
|
||||
print()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from typing import Generic, TypeVar
|
||||
from typing import cast as typing_cast
|
||||
|
||||
cast = typing_cast
|
||||
@@ -32,3 +33,20 @@ This operation is unsound, use at your own risk!
|
||||
|
||||
_**Internal Python documentation**_
|
||||
"""
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Frame(Generic[T]):
|
||||
"""A `Frame` is the abstract type implemented by `DataFrame`
|
||||
|
||||
A frame contains any number of named columns (see :class:`Column`)
|
||||
"""
|
||||
|
||||
|
||||
class Column(Generic[T]):
|
||||
"""A `Column` is the abstract type implemented by `Series`
|
||||
|
||||
A column contains a any number of values of the same type
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user