Compare commits

...
7 Commits
6 changed files with 231 additions and 2 deletions

No files matched your search

@@ -21,7 +21,7 @@ lat + lon # Invalid operation
# Registered operations are permitted # Registered operations are permitted
lat1: Latitude = lat[0] lat1: Latitude = lat[0]
lat2: Latitude = lat[1] lat2: Latitude = lat[1]
lat_diff: LatitudeDiff = lat2 - lat1 # Valid operation lat_diff: Difference[Latitude] = lat2 - lat1 # Valid operation
# In addition to the type, a column can have one or more constraints, either defined inline or in a separate file # In addition to the type, a column can have one or more constraints, either defined inline or in a separate file
df2: Frame[ df2: Frame[
+31
View File
@@ -1,11 +1,13 @@
from __future__ import annotations from __future__ import annotations
import ast
import io import io
from contextlib import contextmanager from contextlib import contextmanager
from enum import Enum, auto from enum import Enum, auto
from typing import Generator, Generic, Optional, Protocol, TypeVar from typing import Generator, Generic, Optional, Protocol, TypeVar
import midas.ast.midas as m import midas.ast.midas as m
import midas.ast.python as p
class _Level(Enum): class _Level(Enum):
@@ -346,3 +348,32 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]):
def visit_type_expr(self, expr: m.TypeExpr): def visit_type_expr(self, expr: m.TypeExpr):
template: str = expr.template.accept(self) if expr.template is not None else "" template: str = expr.template.accept(self) if expr.template is not None else ""
return f"{expr.name.lexeme}{template}{'?' if expr.optional else ''}" return f"{expr.name.lexeme}{template}{'?' if expr.optional else ''}"
class PythonAstPrinter(AstPrinter, p.MidasType.Visitor[None]):
def visit_base_type(self, node: p.BaseType) -> None:
self._write_line("BaseType")
with self._child_level():
self._write_line(f"base: {node.base}")
self._write_optional_child("param", node.param)
constraint_str: str = "None"
if node.constraint is not None:
constraint_str = ast.unparse(node.constraint)
self._write_line(f"constraint: {constraint_str}", last=True)
def visit_frame_column(self, node: p.FrameColumn) -> None:
self._write_line("FrameColumn")
with self._child_level():
self._write_line(f"name: {node.name}")
self._write_optional_child("type", node.type, last=True)
def visit_frame_type(self, node: p.FrameType) -> None:
self._write_line("FrameType")
with self._child_level():
self._write_line("columns", last=True)
with self._child_level():
for i, col in enumerate(node.columns):
self._idx = i
if i == len(node.columns) - 1:
self._mark_last()
col.accept(self)
+51
View File
@@ -0,0 +1,51 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import ast
from dataclasses import dataclass
from typing import Generic, Optional, TypeVar
T = TypeVar("T")
@dataclass(frozen=True)
class MidasType(ABC):
@abstractmethod
def accept(self, visitor: Visitor[T]) -> T: ...
class Visitor(ABC, Generic[T]):
@abstractmethod
def visit_base_type(self, node: BaseType) -> T: ...
@abstractmethod
def visit_frame_column(self, node: FrameColumn) -> T: ...
@abstractmethod
def visit_frame_type(self, node: FrameType) -> T: ...
@dataclass(frozen=True)
class BaseType(MidasType):
base: str
param: Optional[MidasType]
constraint: Optional[ast.expr] = None
def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_base_type(self)
@dataclass(frozen=True)
class FrameColumn(MidasType):
name: Optional[str]
type: Optional[MidasType]
def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_frame_column(self)
@dataclass(frozen=True)
class FrameType(MidasType):
columns: list[FrameColumn]
def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_frame_type(self)
+48 -1
View File
@@ -1,6 +1,53 @@
import ast
from typing import Optional, TextIO
import click import click
from midas.ast.printer import PythonAstPrinter
from midas.parser.python import PythonParser
@click.command()
@click.group()
def midas(): def midas():
click.echo("Welcome to Midas!") click.echo("Welcome to Midas!")
@midas.command()
@click.argument("file", type=click.File("r"))
def compile(file: TextIO):
raise NotImplementedError
@midas.group()
def utils():
pass
@utils.command()
@click.option("-o", "--output", type=click.File("w"))
@click.option("-p", "--parse", is_flag=True)
@click.argument("file", type=click.File("r"))
def dump_ast(output: Optional[TextIO], parse: bool, file: TextIO):
source: str = file.read()
tree: ast.Module = ast.parse(source, filename=file.name)
dump: str
if parse:
parser = PythonParser()
parser.visit(tree)
printer = PythonAstPrinter()
dump = ""
for name, annotation in parser.annotations:
dump += f"{name} = "
if annotation is None:
dump += "None"
else:
dump += printer.print(annotation)
dump += "\n"
else:
dump = ast.dump(tree, indent=4)
if output is None:
click.echo(dump)
else:
output.write(dump)
+92
View File
@@ -0,0 +1,92 @@
import ast
from typing import Any, Optional
from midas.ast.python import BaseType, FrameColumn, FrameType, MidasType
class InvalidSyntaxError(Exception):
pass
class UnsupportedSyntaxError(Exception):
def __init__(self, expr: ast.expr) -> None:
super().__init__(f"Unsupported syntax: {ast.unparse(expr)}")
class PythonParser(ast.NodeVisitor):
def __init__(self) -> None:
super().__init__()
self.annotations: list[tuple[str, Optional[MidasType]]] = []
def visit_AnnAssign(self, node: ast.AnnAssign) -> Any:
match node:
case ast.AnnAssign(
target=ast.Name(id=target), annotation=annotation, simple=1
):
self.annotations.append(
(target, self._parse_type(annotation, root=True))
)
case _:
print(f"Unsupported annotation: {ast.unparse(node)}")
def _parse_type(
self, type_expr: ast.expr, root: bool = False
) -> Optional[MidasType]:
match type_expr:
case ast.Subscript(value=ast.Name(id="Frame"), slice=schema):
return self._parse_frame_type(schema)
case ast.Subscript(value=ast.Name(id=name), slice=param):
return BaseType(
base=name, param=self._parse_type(param), constraint=None
)
case ast.Name(id=name):
return BaseType(base=name, param=None, constraint=None)
case ast.BinOp(left=left_expr, op=ast.Add(), right=right_expr):
print("Constraints not implemented yet")
return None
case _:
if root:
return None
raise UnsupportedSyntaxError(type_expr)
def _parse_frame_type(self, schema: ast.expr) -> FrameType:
columns: list[FrameColumn] = []
match schema:
case ast.Tuple(elts=cols):
for col in cols:
columns.append(self._parse_frame_column(col))
case ast.Slice() | ast.Name():
columns.append(self._parse_frame_column(schema))
case _:
raise UnsupportedSyntaxError(schema)
return FrameType(columns=columns)
def _parse_frame_column(self, column: ast.expr) -> FrameColumn:
match column:
case ast.Name():
return FrameColumn(name=None, type=self._parse_type(column))
case ast.Slice(lower=ast.Name(id=name), upper=type_expr):
if name == "_":
name = None
type: Optional[MidasType] = None
match type_expr:
case None:
raise InvalidSyntaxError("Missing column type")
case ast.Name(id="_"):
type = None
case ast.expr():
type = self._parse_type(type_expr)
case _:
raise UnsupportedSyntaxError(type_expr)
return FrameColumn(name=name, type=type)
case _:
raise UnsupportedSyntaxError(column)
+8
View File
@@ -4,8 +4,16 @@ version = "0.1.0"
description = "A static-first type checking framework for Python data-frames" description = "A static-first type checking framework for Python data-frames"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
authors = [
{ name = "Louis Heredero", email = "louis.heredero@students.hevs.ch" },
]
classifiers = ["Programming Language :: Python :: 3"]
dependencies = ["click>=8.4.1"] dependencies = ["click>=8.4.1"]
[project.urls]
Homepage = "https://git.kbk28.ch/HEL/midas"
Repository = "https://git.kbk28.ch/HEL/midas"
[project.scripts] [project.scripts]
midas = "midas.cli.main:midas" midas = "midas.cli.main:midas"