Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Commit

Permalink
feat: support inheritance while migrating (#1234)
Browse files Browse the repository at this point in the history
Closes #1233.

### Summary of Changes

- new additional differ, that compares only api elements whose parent
classes are mapped onto each other or with a super- or subclass
- improve flexibility for adding and changing and deleting differs in
`run_migrate.py` by adding a constructor to AbstractDiffer
- new methods for getting all the results or attributes of one api
- remove distance_elements and used edit distance from levenshtein-api
instead
    - hash function for subclasses of AbctractType and Parameter
- fix possible runtime errors (RecursionError, NoneError)

### Testing Instructions
run `migrate` command or `test_inheritance_differ.py`
  • Loading branch information
Aclrian authored Feb 16, 2023
1 parent 7bbafd1 commit 4772cd8
Show file tree
Hide file tree
Showing 28 changed files with 1,230 additions and 200 deletions.
76 changes: 52 additions & 24 deletions package-parser/package_parser/cli/_run_migrate.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import os
from pathlib import Path
from typing import Any

from package_parser.processing.migration import Migration
from package_parser.processing.migration import APIMapping, Migration
from package_parser.processing.migration.model import (
APIMapping,
AbstractDiffer,
InheritanceDiffer,
Mapping,
SimpleDiffer,
StrictDiffer,
)
Expand All @@ -24,26 +27,51 @@ def _run_migrate_command(
apiv1 = _read_api_file(apiv1_file_path)
apiv2 = _read_api_file(apiv2_file_path)
annotationsv1 = _read_annotations_file(annotations_file_path)
differ = SimpleDiffer()
api_mapping = APIMapping(apiv1, apiv2, differ)
mappings = api_mapping.map_api()
enhanced_api_mapping = APIMapping(apiv1, apiv2, StrictDiffer(mappings, differ))
enhanced_mappings = enhanced_api_mapping.map_api()

migration = Migration(annotationsv1, enhanced_mappings)
migration.migrate_annotations()
migration.print(apiv1, apiv2)
migrated_annotations_file = Path(
os.path.join(out_dir_path, "migrated_annotationsv" + apiv2.version + ".json")
)
unsure_migrated_annotations_file = Path(
os.path.join(
out_dir_path, "unsure_migrated_annotationsv" + apiv2.version + ".json"

differ_init_list: list[tuple[type[AbstractDiffer], dict[str, Any]]] = [
(SimpleDiffer, {}),
(StrictDiffer, {}),
(InheritanceDiffer, {}),
]
previous_base_differ = None
previous_mappings: list[Mapping] = []

for differ_init in differ_init_list:
differ_class, additional_parameters = differ_init
differ = differ_class(
previous_base_differ,
previous_mappings,
apiv1,
apiv2,
**additional_parameters
)
api_mapping = APIMapping(apiv1, apiv2, differ)
mappings = api_mapping.map_api()

previous_mappings = mappings
previous_base_differ = (
differ
if differ.get_related_mappings() is None
else differ.previous_base_differ
)

if previous_mappings is not None:
migration = Migration(annotationsv1, previous_mappings)
migration.migrate_annotations()
migration.print(apiv1, apiv2)
migrated_annotations_file = Path(
os.path.join(
out_dir_path, "migrated_annotationsv" + apiv2.version + ".json"
)
)
unsure_migrated_annotations_file = Path(
os.path.join(
out_dir_path, "unsure_migrated_annotationsv" + apiv2.version + ".json"
)
)
_write_annotations_file(
migration.migrated_annotation_store, migrated_annotations_file
)
_write_annotations_file(
migration.unsure_migrated_annotation_store, unsure_migrated_annotations_file
)
)
_write_annotations_file(
migration.migrated_annotation_store, migrated_annotations_file
)
_write_annotations_file(
migration.unsure_migrated_annotation_store, unsure_migrated_annotations_file
)
58 changes: 56 additions & 2 deletions package-parser/package_parser/processing/api/model/_api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Optional
from dataclasses import dataclass, field
from typing import Any, Optional, Union

from black import FileMode, InvalidInput, format_str
from black.brackets import BracketMatchError
from black.linegen import CannotSplit
from black.trans import CannotTransform
from package_parser.utils import parent_id

from ._documentation import ClassDocumentation, FunctionDocumentation
Expand Down Expand Up @@ -79,6 +83,26 @@ def parameters(self) -> dict[str, Parameter]:

return result

def attributes(self) -> dict[str, Attribute]:
result: dict[str, Attribute] = {}

for class_ in self.classes.values():
for attribute in class_.instance_attributes:
attribute_id = f"{class_.id}/{attribute.name}"
result[attribute_id] = attribute

return result

def results(self) -> dict[str, Result]:
result_dict: dict[str, Result] = {}

for function in self.functions.values():
for result in function.results:
result_id = f"{function.id}/{result.name}"
result_dict[result_id] = result

return result_dict

def get_default_value(self, parameter_id: str) -> Optional[str]:
function_id = parent_id(parameter_id)

Expand Down Expand Up @@ -241,6 +265,7 @@ def __init__(
self.documentation: ClassDocumentation = documentation
self.code: str = code
self.instance_attributes = instance_attributes
self.formatted_code: Optional[str] = None

@property
def name(self) -> str:
Expand All @@ -267,16 +292,36 @@ def to_json(self) -> Any:
],
}

def get_formatted_code(self) -> str:
if self.formatted_code is None:
self.formatted_code = _generate_formatted_code(self)
return self.formatted_code

def __repr__(self) -> str:
return "Class(id=" + self.id + ")"


def _generate_formatted_code(api_element: Union[Class, Function]) -> str:
code = api_element.code
try:
code_tmp = format_str(code, mode=FileMode())
except (CannotSplit, CannotTransform, InvalidInput, BracketMatchError):
# As long as the api black has no documentation, we do not know which exceptions are raised
pass
else:
code = code_tmp
return code


@dataclass
class Attribute:
name: str
types: Optional[AbstractType]
class_id: Optional[str] = None

def __hash__(self) -> int:
return hash((self.name, self.class_id, self.types))

def to_json(self) -> dict[str, Any]:
types_json = self.types.to_json() if self.types is not None else None
return {"name": self.name, "types": types_json}
Expand Down Expand Up @@ -312,6 +357,10 @@ class Function:
reexported_by: list[str]
documentation: FunctionDocumentation
code: str
formatted_code: Optional[str] = field(init=False)

def __post_init__(self) -> None:
self.formatted_code = None

@staticmethod
def from_json(json: Any) -> Function:
Expand Down Expand Up @@ -352,6 +401,11 @@ def to_json(self) -> Any:
"code": self.code,
}

def get_formatted_code(self) -> str:
if self.formatted_code is None:
self.formatted_code = _generate_formatted_code(self)
return self.formatted_code

def __repr__(self) -> str:
return "Function(id=" + self.id + ")"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ def from_dict(d: dict) -> ParameterDocumentation:

def to_dict(self) -> dict:
return dataclasses.asdict(self)

def __hash__(self) -> int:
return hash((self.type, self.default_value, self.description))
13 changes: 13 additions & 0 deletions package-parser/package_parser/processing/api/model/_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ def from_json(json: Any) -> Parameter:
ParameterDocumentation.from_dict(json.get("docstring", {})),
)

def __hash__(self) -> int:
return hash(
(
self.id,
self.name,
self.qname,
self.default_value,
self.assigned_by,
self.is_public,
self.documentation,
)
)

def __init__(
self,
id_: str,
Expand Down
14 changes: 14 additions & 0 deletions package-parser/package_parser/processing/api/model/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def __eq__(self, other: object) -> bool:
return self.name == other.name
return False

def __hash__(self) -> int:
return hash(self.name)


@dataclass
class EnumType(AbstractType):
Expand Down Expand Up @@ -105,6 +108,9 @@ def update(self, enum: EnumType) -> None:
def to_json(self) -> dict[str, Any]:
return {"kind": self.__class__.__name__, "values": self.values}

def __hash__(self) -> int:
return hash(frozenset(self.values))


@dataclass
class BoundaryType(AbstractType):
Expand Down Expand Up @@ -201,6 +207,11 @@ def __eq__(self, __o: object) -> bool:
return self.max_inclusive == __o.max_inclusive
return False

def __hash__(self) -> int:
return hash(
(self.base_type, self.min, self.min_inclusive, self.max, self.max_inclusive)
)

def to_json(self) -> dict[str, Any]:
return {
"kind": self.__class__.__name__,
Expand Down Expand Up @@ -234,6 +245,9 @@ def to_json(self) -> dict[str, Any]:

return {"kind": self.__class__.__name__, "types": type_list}

def __hash__(self) -> int:
return hash((frozenset(self.types)))


def create_type(
parameter_documentation: ParameterDocumentation,
Expand Down
12 changes: 1 addition & 11 deletions package-parser/package_parser/processing/migration/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
from package_parser.processing.migration.model import (
AbstractDiffer,
APIMapping,
ManyToManyMapping,
ManyToOneMapping,
Mapping,
OneToManyMapping,
OneToOneMapping,
SimpleDiffer,
)

from ._api_mapping import APIMapping
from ._migrate import Migration
Loading

0 comments on commit 4772cd8

Please sign in to comment.