Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update proposal for annotated types - also enables pydantic annotated types #66

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions py2puml/parsing/compoundtypesplitter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from re import Pattern
from re import compile as re_compile
from re import sub as re_sub
from typing import Tuple

# a class name wrapped by ForwardRef(...)
Expand All @@ -15,6 +16,34 @@
LAST_NONETYPE_IN_UNION: Pattern = re_compile(r'Union\[(?:(?:[^\[\]])*NoneType)')


def replace_typing_annotated_with_type(compound_type_annotation: str) -> str:
"""Replace all occurances of typing.Annotated[type, extraInfo] with type.

This simplifies the compound_type_annotation for further processing, losing the extraInfo.

Even nested replacements are possible, e.g.
typing.Annotated[typing.Annotated[type, extraInfo], extraInfo] is replaced by type.

Since the extraInfo can be a complex string with alot of () and [] when Field from pydantic is used,
the "problematic" strings from the extraInfo are detected and removed first to be able to remove the
whole extraInfo part robustly.
"""
simple_compound_type_annotation = compound_type_annotation

# Remove metadata[....] from extraInfo if present
simple_compound_type_annotation = re_sub(r'metadata=\[(.*?)\]', '', simple_compound_type_annotation)

# Then remove the FieldInfo(...) if present
simple_compound_type_annotation = re_sub(r'FieldInfo\((.*?)\)', '', simple_compound_type_annotation)

# Remove the typing.Annotated (recursively if they are nested)
re_search_rule = r'typing\.Annotated\[(.*?)\,(.*?)]'
while ('typing.Annotated' in simple_compound_type_annotation):
simple_compound_type_annotation = re_sub(re_search_rule, r'\1', simple_compound_type_annotation)

return simple_compound_type_annotation


def remove_forward_references(compound_type_annotation: str, module_name: str) -> str:
'''
Removes the forward reference mention from the string representation of a type annotation.
Expand Down Expand Up @@ -52,8 +81,11 @@ class CompoundTypeSplitter:
Splits the representation of a compound type annotation into a list of:
- its components (that can be resolved against the module where the type annotation was found)
- its structuring characters: '[', ']' and ','

Also removes the typing.Annotated[type, extraInfo] string to directly get the acutal type.
'''
def __init__(self, compound_type_annotation: str, module_name: str):
compound_type_annotation = replace_typing_annotated_with_type(compound_type_annotation)
resolved_type_annotations = remove_forward_references(compound_type_annotation, module_name)
resolved_type_annotations = replace_nonetype_occurrences_in_union_types(resolved_type_annotations)
if (resolved_type_annotations is None) or not IS_COMPOUND_TYPE.match(resolved_type_annotations):
Expand Down
27 changes: 10 additions & 17 deletions tests/py2puml/parsing/test_compoundtypesplitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@

@mark.parametrize(
'type_annotation', [
'int',
'str',
'_ast.Name',
'Tuple[str, withenum.TimeUnit]',
'List[datetime.date]',
'modules.withenum.TimeUnit',
'Dict[str, Dict[str,builtins.float]]',
'int', 'str', '_ast.Name', 'Tuple[str, withenum.TimeUnit]', 'List[datetime.date]', 'modules.withenum.TimeUnit',
'Dict[str, Dict[str,builtins.float]]'
]
)
def test_CompoundTypeSplitter_from_valid_types(type_annotation: str):
Expand All @@ -37,25 +32,23 @@ def test_CompoundTypeSplitter_from_invalid_types(type_annotation: str):

@mark.parametrize(
['type_annotation', 'expected_parts'], [
('int', ('int', )),
('str', ('str', )),
('_ast.Name', ('_ast.Name', )),
('int', ('int', )), ('str', ('str', )), ('_ast.Name', ('_ast.Name', )),
('Tuple[str, withenum.TimeUnit]', ('Tuple', '[', 'str', ',', 'withenum.TimeUnit', ']')),
('List[datetime.date]', ('List', '[', 'datetime.date', ']')),
('List[IPv6]', ('List', '[', 'IPv6', ']')),
('List[datetime.date]', ('List', '[', 'datetime.date', ']')), ('List[IPv6]', ('List', '[', 'IPv6', ']')),
('modules.withenum.TimeUnit', ('modules.withenum.TimeUnit', )),
(
'Dict[str, Dict[str,builtins.float]]',
('Dict', '[', 'str', ',', 'Dict', '[', 'str', ',', 'builtins.float', ']', ']')
),
('typing.List[Package]', ('typing.List', '[', 'Package', ']')),
), ('typing.List[Package]', ('typing.List', '[', 'Package', ']')),
("typing.List[ForwardRef('Package')]", ('typing.List', '[', 'py2puml.domain.package.Package', ']')),
(
'typing.List[py2puml.domain.umlclass.UmlAttribute]',
('typing.List', '[', 'py2puml.domain.umlclass.UmlAttribute', ']')
),
('int|float', ('int', '|', 'float')),
('int | None', ('int', '|', 'None')),
), ('int|float', ('int', '|', 'float')), ('int | None', ('int', '|', 'None')),
(
'typing.Annotated[float, FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0.0), Le(le=1.0)])]',
('float', )
), ('typing.Annotated[int, Gt(gt=0)]', ('int', ))
]
)
def test_CompoundTypeSplitter_get_parts(type_annotation: str, expected_parts: Tuple[str]):
Expand Down