diff --git a/HISTORY.md b/HISTORY.md index 688b93ba..27add204 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,8 @@ ## v0.8.0 (Unreleased) - Removed support for Python 3.7. ([PR #102](https://github.com/drivendataorg/erdantic/pull/102)) +- Changed rendering of type names to use the [typenames](https://github.com/jayqi/typenames) library. This should generally produce with same rendered outputs, with the following exception: + - Removed the special case behavior for rendering enum classes. Enums now just show the class name without inheritance information. ## v0.7.0 (2024-02-11) diff --git a/erdantic/base.py b/erdantic/base.py index 9bd56638..683181e2 100644 --- a/erdantic/base.py +++ b/erdantic/base.py @@ -13,8 +13,10 @@ Union, ) +from typenames import REMOVE_ALL_MODULES, typenames + from erdantic.exceptions import InvalidModelAdapterError, ModelAdapterNotFoundError -from erdantic.typing import GenericAlias, repr_type +from erdantic.typing import GenericAlias _row_template = """{name}{type_name}""" @@ -73,7 +75,7 @@ def is_nullable(self) -> bool: # pragma: no cover @property def type_name(self) -> str: # pragma: no cover """String representation of the Python type annotation for this field.""" - return repr_type(self.type_obj) + return typenames(self.type_obj, remove_modules=REMOVE_ALL_MODULES) def dot_row(self) -> str: """Returns the DOT language "HTML-like" syntax specification of a row detailing this field diff --git a/erdantic/typing.py b/erdantic/typing.py index ca4fb261..84410468 100644 --- a/erdantic/typing.py +++ b/erdantic/typing.py @@ -1,11 +1,9 @@ import collections.abc -from enum import Enum from typing import ( Any, ForwardRef, List, Literal, - Type, Union, get_args, get_origin, @@ -79,42 +77,6 @@ def recurse(t): return list(recurse(tp)) -def repr_type(tp: Union[type, GenericAlias]) -> str: - """Return pretty, compact string representation of a type. Principles of behavior: - - - Names without module path - - Generic capitalization matches which was used (`typing` module's aliases vs. builtin types) - - Union[..., None] -> Optional[...] - - Enums show base classes, e.g., `MyEnum(str, Enum)` - """ - origin = get_origin(tp) - if origin: - origin_name = getattr(origin, "__name__", str(origin)) - args = get_args(tp) - # Union[..., None] -> Optional[...] - if origin is Union and args[-1] is type(None): # noqa: E721 - origin_name = "Optional" - args = args[:-1] - # If generic alias from typing module, back out its name - elif isinstance(tp, GenericAlias) and tp.__module__ == "typing": - origin_name = str(tp).split("[")[0].replace("typing.", "") - return f"{origin_name}[{', '.join(repr_type(a) for a in args)}]" - if tp is Ellipsis: - return "..." - if isinstance(tp, type) and issubclass(tp, Enum): - return repr_enum(tp) - if isinstance(tp, ForwardRef): - return tp.__forward_arg__ - return getattr(tp, "__name__", repr(tp).replace("typing.", "")) - - -def repr_enum(tp: Type[Enum]) -> str: - """Return pretty, compact string representation of an Enum type with its depth-1 base - classes, e.g., `MyEnum(str, Enum)`.""" - depth1_bases = get_depth1_bases(tp) - return f"{tp.__name__}({', '.join(b.__name__ for b in depth1_bases)})" - - def repr_type_with_mro(obj: Any) -> str: """Return MRO of object if it has one. Otherwise return its repr.""" diff --git a/pyproject.toml b/pyproject.toml index 51922c5a..79fa29be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "pydantic >= 2", "pydantic-core", "pygraphviz", + "typenames", "typer", ] diff --git a/tests/assets/dataclasses/diagram.dot b/tests/assets/dataclasses/diagram.dot index 6c336d1d..b7b278e6 100644 --- a/tests/assets/dataclasses/diagram.dot +++ b/tests/assets/dataclasses/diagram.dot @@ -10,7 +10,7 @@ digraph "Entity Relationship Diagram" { label="\N", shape=plain ]; - "erdantic.examples.dataclasses.Adventurer" [label=<
Adventurer
namestr
professionstr
levelint
alignmentAlignment(str, Enum)
>, + "erdantic.examples.dataclasses.Adventurer" [label=<
Adventurer
namestr
professionstr
levelint
alignmentAlignment
>, tooltip="erdantic.examples.dataclasses.Adventurer A person often late for dinner but with a tale or two to tell. Attributes:&#\ xA; name (str): Name of this adventurer profession (str): Profession of this adventurer level (int): Level of \ this adventurer alignment (Alignment): Alignment of this adventurer "]; diff --git a/tests/assets/dataclasses/diagram.png b/tests/assets/dataclasses/diagram.png index e276983d..708bf82d 100644 Binary files a/tests/assets/dataclasses/diagram.png and b/tests/assets/dataclasses/diagram.png differ diff --git a/tests/assets/dataclasses/diagram.svg b/tests/assets/dataclasses/diagram.svg index e92da24e..3812df0a 100644 --- a/tests/assets/dataclasses/diagram.svg +++ b/tests/assets/dataclasses/diagram.svg @@ -1,37 +1,37 @@ - - - + + Entity Relationship Diagram - -Created by erdantic vTEST <https://github.com/drivendataorg/erdantic> + +Created by erdantic vTEST <https://github.com/drivendataorg/erdantic> erdantic.examples.dataclasses.Adventurer - -Adventurer - -name - -str - -profession - -str - -level - -int - -alignment - -Alignment(str, Enum) + +Adventurer + +name + +str + +profession + +str + +level + +int + +alignment + +Alignment @@ -39,94 +39,94 @@ erdantic.examples.dataclasses.Party - -Party - -name - -str - -formed_datetime - -datetime - -members - -List[Adventurer] - -active_quest - -Optional[Quest] + +Party + +name + +str + +formed_datetime + +datetime + +members + +List[Adventurer] + +active_quest + +Optional[Quest] erdantic.examples.dataclasses.Party:e->erdantic.examples.dataclasses.Adventurer:w - - - + + + erdantic.examples.dataclasses.Quest - -Quest - -name - -str - -giver - -QuestGiver - -reward_gold - -int + +Quest + +name + +str + +giver + +QuestGiver + +reward_gold + +int erdantic.examples.dataclasses.Party:e->erdantic.examples.dataclasses.Quest:w - - - - - + + + + + erdantic.examples.dataclasses.QuestGiver - -QuestGiver - -name - -str - -faction - -Optional[str] - -location - -str + +QuestGiver + +name + +str + +faction + +Optional[str] + +location + +str erdantic.examples.dataclasses.Quest:e->erdantic.examples.dataclasses.QuestGiver:w - - - - - - + + + + + + diff --git a/tests/assets/pydantic/diagram.dot b/tests/assets/pydantic/diagram.dot index 5308c383..a4eacaa3 100644 --- a/tests/assets/pydantic/diagram.dot +++ b/tests/assets/pydantic/diagram.dot @@ -10,7 +10,7 @@ digraph "Entity Relationship Diagram" { label="\N", shape=plain ]; - "erdantic.examples.pydantic.Adventurer" [label=<
Adventurer
namestr
professionstr
levelint
alignmentAlignment(str, Enum)
>, + "erdantic.examples.pydantic.Adventurer" [label=<
Adventurer
namestr
professionstr
levelint
alignmentAlignment
>, tooltip="erdantic.examples.pydantic.Adventurer A person often late for dinner but with a tale or two to tell. Attributes:&#\ xA; name (str): Name of this adventurer profession (str): Profession of this adventurer level (int): Level of \ this adventurer alignment (Alignment): Alignment of this adventurer "]; diff --git a/tests/assets/pydantic/diagram.png b/tests/assets/pydantic/diagram.png index e276983d..708bf82d 100644 Binary files a/tests/assets/pydantic/diagram.png and b/tests/assets/pydantic/diagram.png differ diff --git a/tests/assets/pydantic/diagram.svg b/tests/assets/pydantic/diagram.svg index 5454caad..c9c0d6f6 100644 --- a/tests/assets/pydantic/diagram.svg +++ b/tests/assets/pydantic/diagram.svg @@ -1,37 +1,37 @@ - - - + + Entity Relationship Diagram - -Created by erdantic vTEST <https://github.com/drivendataorg/erdantic> + +Created by erdantic vTEST <https://github.com/drivendataorg/erdantic> erdantic.examples.pydantic.Adventurer - -Adventurer - -name - -str - -profession - -str - -level - -int - -alignment - -Alignment(str, Enum) + +Adventurer + +name + +str + +profession + +str + +level + +int + +alignment + +Alignment @@ -39,94 +39,94 @@ erdantic.examples.pydantic.Party - -Party - -name - -str - -formed_datetime - -datetime - -members - -List[Adventurer] - -active_quest - -Optional[Quest] + +Party + +name + +str + +formed_datetime + +datetime + +members + +List[Adventurer] + +active_quest + +Optional[Quest] erdantic.examples.pydantic.Party:e->erdantic.examples.pydantic.Adventurer:w - - - + + + erdantic.examples.pydantic.Quest - -Quest - -name - -str - -giver - -QuestGiver - -reward_gold - -int + +Quest + +name + +str + +giver + +QuestGiver + +reward_gold + +int erdantic.examples.pydantic.Party:e->erdantic.examples.pydantic.Quest:w - - - - - + + + + + erdantic.examples.pydantic.QuestGiver - -QuestGiver - -name - -str - -faction - -Optional[str] - -location - -str + +QuestGiver + +name + +str + +faction + +Optional[str] + +location + +str erdantic.examples.pydantic.Quest:e->erdantic.examples.pydantic.QuestGiver:w - - - - - - + + + + + + diff --git a/tests/test_typing.py b/tests/test_typing.py index 463ce088..e152ebc6 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,15 +1,9 @@ -from enum import Enum, IntFlag -import sys import typing -from typing import ForwardRef, Literal - -import pytest +from typing import Literal from erdantic.typing import ( get_depth1_bases, get_recursive_args, - repr_enum, - repr_type, repr_type_with_mro, ) @@ -50,66 +44,6 @@ class SubClass(str, A2, B1, C): get_depth1_bases(SubClass) == [str, A2, B1, C] -class MyEnum(Enum): - FOO = "bar" - - -class MyStrEnum(str, Enum): - FOO = "bar" - - -class MyIntFlag(IntFlag): - FOO = 0 - - -def test_repr_enum(): - assert repr_enum(MyEnum) == "MyEnum(Enum)" - assert repr_enum(MyStrEnum) == "MyStrEnum(str, Enum)" - assert repr_enum(MyIntFlag) == "MyIntFlag(IntFlag)" - - -class MyClass: - pass - - -repr_type_cases = [ - (int, "int"), - (typing.List[int], "List[int]"), - (typing.Tuple[str, int], "Tuple[str, int]"), - (typing.Optional[int], "Optional[int]"), - (MyClass, "MyClass"), - (typing.List[MyClass], "List[MyClass]"), - (typing.Optional[typing.List[MyClass]], "Optional[List[MyClass]]"), - (typing.Union[float, int], "Union[float, int]"), - (typing.Dict[str, int], "Dict[str, int]"), - (typing.Optional[MyStrEnum], "Optional[MyStrEnum(str, Enum)]"), - (typing.List[MyIntFlag], "List[MyIntFlag(IntFlag)]"), - (typing.Any, "Any"), - (typing.Dict[str, typing.Any], "Dict[str, Any]"), - (ForwardRef("MyClass"), "MyClass"), -] - -if sys.version_info[:2] >= (3, 9): - # Python 3.9 adds [] support to builtin generics - repr_type_cases.extend( - [ - (list[int], "list[int]"), - (dict[str, list[int]], "dict[str, list[int]]"), - ] - ) - - -@pytest.mark.parametrize("case", repr_type_cases, ids=[c[1] for c in repr_type_cases]) -def test_repr_type(case): - tp, expected = case - assert repr_type(tp) == expected - - -def test_repr_type_literal(): - tp = Literal["batman"] - assert "Literal['batman']" in repr_type(tp) - - def test_repr_type_with_mro(): class FancyInt(int): pass