Files
Zamil Majdyandmajdyz 92416fadac [AGPT-556] Overhaul code-validation design + Add PyRight in commented mode (#139)
This is a large but a much needed change to allow more sophisticated static code analysis to be added.

PyRight (used by pylance) will allow us to detect:

Misuse of async functions.
Misuse of optional type and other annotated types.
Wrong usage of pydantic and prisma model.
Wrong usage of third-party library.
The overhaul is required to allow us retrying while fixing the code as a project (not only as a function) and allow us to keep adding non-LLM fixes to it so that we can avoid wasting the retry.

The scope of this PR is still a refactoring and the adding of the PyRight linter, so this PR try to keep the existing behaviour.
The enablement and more tuning on the linter will be done in the followup PR.

---------

Co-authored-by: majdyz <[email protected]>
2024-03-21 13:28:17 +04:00

166 lines
4.7 KiB
Python

from typing import List, Tuple
from prisma.models import ObjectType
OPEN_BRACES = {"{": "Dict", "[": "List", "(": "Tuple"}
CLOSE_BRACES = {"}": "Dict", "]": "List", ")": "Tuple"}
RENAMED_TYPES = {
"dict": "Dict",
"list": "List",
"tuple": "Tuple",
"set": "Set",
"frozenset": "FrozenSet",
"type": "Type",
}
def unwrap_object_type(type: str) -> Tuple[str, List[str]]:
"""
Get the type and children of a composite type.
Args:
type (str): The type to parse.
Returns:
str: The type.
[str]: The children types.
"""
type = type.replace(" ", "")
if not type:
return "", []
def split_outer_level(type: str, separator: str) -> List[str]:
brace_count = 0
last_index = 0
splits = []
for i, c in enumerate(type):
if c in OPEN_BRACES:
brace_count += 1
elif c in CLOSE_BRACES:
brace_count -= 1
elif c == separator and brace_count == 0:
splits.append(type[last_index:i])
last_index = i + 1
splits.append(type[last_index:])
return splits
# Unwrap primitive union types
union_split = split_outer_level(type, "|")
if len(union_split) > 1:
if len(union_split) == 2 and "None" in union_split:
return "Optional", [v for v in union_split if v != "None"]
return "Union", union_split
# Unwrap primitive dict/list/tuple types
if type[0] in OPEN_BRACES and type[-1] in CLOSE_BRACES:
type_name = OPEN_BRACES[type[0]]
type_children = split_outer_level(type[1:-1], ",")
return type_name, type_children
brace_pos = type.find("[")
if brace_pos != -1 and type[-1] == "]":
# Unwrap normal composite types
type_name = type[:brace_pos]
type_children = split_outer_level(type[brace_pos + 1 : -1], ",")
else:
# Non-composite types, no need to unwrap
type_name = type
type_children = []
return RENAMED_TYPES.get(type_name, type_name), type_children
def is_type_equal(type1: str | None, type2: str | None) -> bool:
"""
Check if two types are equal.
This function handle composite types like list, dict, and tuple.
group similar types like list[str], List[str], and [str] as equal.
"""
if type1 is None and type2 is None:
return True
if type1 is None or type2 is None:
return False
evaluated_type1, children1 = unwrap_object_type(type1)
evaluated_type2, children2 = unwrap_object_type(type2)
# Compare the class name of the types (ignoring the module)
# TODO(majdyz): compare the module name as well.
if evaluated_type1.split(".")[-1] != evaluated_type2.split(".")[-1]:
return False
if len(children1) != len(children2):
return False
if len(children1) == len(children2) == 0:
return True
for c1, c2 in zip(children1, children2):
if not is_type_equal(c1, c2):
return False
return True
def extract_field_type(field_type: str | None) -> set[str]:
"""
Extract the field type from a composite type.
e.g. tuple[str, dict[str, int]] -> {tuple, dict, str, int}
Args:
field_type (str): The field type to parse.
Returns:
list[str]: The extracted field types.
"""
if field_type is None:
return set()
parent_type, children = unwrap_object_type(field_type)
result = {parent_type}
for child in children:
result |= extract_field_type(child)
return result
def normalize_type(type: str, renamed_types: dict[str, str] = {}) -> str:
"""
Normalize the type to a standard format.
e.g. list[str] -> List[str], dict[str, int | float] -> Dict[str, Union[int, float]]
Args:
type (str): The type to normalize.
Returns:
str: The normalized type.
"""
parent_type, children = unwrap_object_type(type)
if parent_type in renamed_types:
parent_type = renamed_types[parent_type]
if len(children) == 0:
return parent_type
return f"{parent_type}[{', '.join([normalize_type(c, renamed_types) for c in children])}]"
def get_related_types(
type: str, available_objects: dict[str, ObjectType]
) -> list[ObjectType]:
"""
Get the related types of a composite type.
e.g. tuple[Obj1, dict[Obj2, Obj3]] with {Obj1, Obj3} -> [Obj1, Obj3]
Args:
type (str): The type to parse.
available_objects (dict[str, ObjectType]): The available objects.
Returns:
list[ObjectType]: The related types.
"""
return [
available_objects[related_type]
for related_type in extract_field_type(type)
if related_type in available_objects
]