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

[QoL] Pretty print - main package version #2032

Open
wants to merge 5 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
12 changes: 11 additions & 1 deletion linkml/generators/pythongen.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ class PythonGenerator(Generator):
gen_slots: bool = True
genmeta: bool = False
emit_metadata: bool = True
dataclass_repr: bool = False
"""
Whether generated dataclasses should also generate a default __repr__ method.

Default ``False`` so that the parent :class:`linkml_runtime.utils.yamlutils.YAMLRoot` 's
``__repr__`` method is inherited for model pretty printing.

References:
- https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
"""

def __post_init__(self) -> None:
self.sourcefile = self.schema
Expand Down Expand Up @@ -395,7 +405,7 @@ def gen_classdef(self, cls: ClassDefinition) -> str:
return f"\n{self.class_or_type_name(cls.name)} = Any"

cd_str = (
("\n@dataclass" if slotdefs else "")
(f"\n@dataclass(repr={self.dataclass_repr})" if slotdefs else "")
+ f"\nclass {self.class_or_type_name(cls.name)}{parentref}:{wrapped_description}"
+ f"{self.gen_inherited_slots(cls)}"
+ f"{self.gen_class_meta(cls)}"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_compliance/test_core_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def test_attributes(framework, description, object, is_valid):
},
"_mappings": {
PYDANTIC: f"class C({PYDANTIC_ROOT_CLASS}):",
PYTHON_DATACLASSES: f"@dataclass\nclass C({PYTHON_DATACLASSES_ROOT_CLASS}):",
PYTHON_DATACLASSES: f"@dataclass(repr=False)\nclass C({PYTHON_DATACLASSES_ROOT_CLASS}):",
JSON_SCHEMA: {
"$defs": {
"C": {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_compliance/test_inheritance_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def test_basic_class_inheritance(framework, description, cls: str, object, is_va
},
"_mappings": {
PYDANTIC: "class C(D):",
PYTHON_DATACLASSES: "@dataclass\nclass C(D):",
PYTHON_DATACLASSES: "@dataclass(repr=False)\nclass C(D):",
JSON_SCHEMA: {"$defs": json_schema_defs},
},
},
Expand Down Expand Up @@ -279,7 +279,7 @@ def test_mixins(framework, description, cls, object, is_valid):
},
"_mappings": {
PYDANTIC: "class C(MC2, MC1):",
PYTHON_DATACLASSES: "@dataclass\nclass C(YAMLRoot):", # DC rolls up
PYTHON_DATACLASSES: "@dataclass(repr=False)\nclass C(YAMLRoot):", # DC rolls up
JSON_SCHEMA: json_schema_defs,
},
},
Expand Down
10 changes: 5 additions & 5 deletions tests/test_enhancements/test_enumeration.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,22 @@ def test_notebook_model_1(self):
module = compile_python(env.expected_path(python_name))
c1 = module.PositionalRecord("my location", "a")
self.assertEqual(
"PositionalRecord(id='my location', position=(text='a', description='top'))",
str(c1),
module.PositionalRecord(id='my location', position=module.OpenEnum('a')),
c1,
)
self.assertEqual("a", str(c1.position))
self.assertEqual("(text='a', description='top')", repr(c1.position))
self.assertEqual("OpenEnum(text='a', description='top')", repr(c1.position))
try:
module.PositionalRecord("your location", "z")
except ValueError as e:
self.assertEqual("Unknown OpenEnum enumeration code: z", str(e))
x = module.PositionalRecord("117493", "c")
self.assertEqual("c", str(x.position))
self.assertEqual(
"PositionalRecord(id='117493', position=(text='c', description='bottom'))",
"PositionalRecord({'id': '117493', 'position': OpenEnum(text='c', description='bottom')})",
repr(x),
)
self.assertEqual("(text='c', description='bottom')", repr(x.position))
self.assertEqual("OpenEnum(text='c', description='bottom')", repr(x.position))

def test_notebook_model_2(self):
file = "notebook_model_2"
Expand Down
6 changes: 4 additions & 2 deletions tests/test_enhancements/test_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ def test_pattern_1(self):
d1 = yaml.load(StringIO(d1_test), yaml.loader.SafeLoader)
dev1 = module.DiskDevice(**d1)
self.assertEqual(
"DiskDevice(label='AbCd0123-1111-FF10-AAF1-A1B2C3D4A1B2C3D4A1B2C3D4', "
"device='/dev/tty.Bluetooth-Incoming-Port')",
"""DiskDevice({
'label': 'AbCd0123-1111-FF10-AAF1-A1B2C3D4A1B2C3D4A1B2C3D4',
'device': '/dev/tty.Bluetooth-Incoming-Port'
})""",
str(dev1),
)

Expand Down
47 changes: 37 additions & 10 deletions tests/test_enhancements/test_python_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,42 @@ def __str__(self) -> str:
[
("s1", "s2", "s3", "s4"),
{},
"Strings(mand_string='s1', mand_multi_string=['s2'], opt_string='s3', opt_multi_string=['s4'])",
"""Strings({
'mand_string': 's1',
'mand_multi_string': ['s2'],
'opt_string': 's3',
'opt_multi_string': ['s4']
})""",
None,
],
[
("s1", ["s21", "s22"], "s3", ["s41", "s42"]),
{},
(
"Strings(mand_string='s1', mand_multi_string=['s21', 's22'], "
"opt_string='s3', opt_multi_string=['s41', 's42'])"
"""Strings({
'mand_string': 's1',
'mand_multi_string': ['s21', 's22'],
'opt_string': 's3',
'opt_multi_string': ['s41', 's42']
})"""
),
None,
],
[
("s1", ["s21", "s22"], None, None),
{},
"Strings(mand_string='s1', mand_multi_string=['s21', 's22'], opt_string=None, opt_multi_string=[])",
"Strings({'mand_string': 's1', 'mand_multi_string': ['s21', 's22']})",
None,
],
[
(NonStr("s1"), NonStr("s2"), NonStr("s3"), NonStr("s4")),
{},
"Strings(mand_string='s1', mand_multi_string=['s2'], opt_string='s3', opt_multi_string=['s4'])",
"""Strings({
'mand_string': 's1',
'mand_multi_string': ['s2'],
'opt_string': 's3',
'opt_multi_string': ['s4']
})""",
None,
],
[
Expand All @@ -55,8 +69,12 @@ def __str__(self) -> str:
),
{},
(
"Strings(mand_string='s1', mand_multi_string=['s21', 's22'], "
"opt_string='s3', opt_multi_string=['s41', 's42'])"
"""Strings({
'mand_string': 's1',
'mand_multi_string': ['s21', 's22'],
'opt_string': 's3',
'opt_multi_string': ['s41', 's42']
})"""
),
None,
],
Expand All @@ -69,8 +87,12 @@ def __str__(self) -> str:
("True", "false", 1, [1, 0, True, False]),
{},
(
"Booleans(mand_boolean=True, mand_multi_boolean=[False], opt_boolean=True, "
"opt_multi_boolean=[True, False, True, False])"
"""Booleans({
'mand_boolean': True,
'mand_multi_boolean': [False],
'opt_boolean': True,
'opt_multi_boolean': [True, False, True, False]
})"""
),
None,
]
Expand All @@ -79,7 +101,12 @@ def __str__(self) -> str:
[
("17", -2, 12 + 3, [42, "17"]),
{},
"Integers(mand_integer=17, mand_multi_integer=[-2], opt_integer=15, opt_multi_integer=[42, 17])",
"""Integers({
'mand_integer': 17,
'mand_multi_integer': [-2],
'opt_integer': 15,
'opt_multi_integer': [42, 17]
})""",
None,
],
[
Expand Down
66 changes: 46 additions & 20 deletions tests/test_generators/test_pythongen.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from types import ModuleType

from linkml_runtime.loaders import json_loader
Expand All @@ -16,12 +17,9 @@ def test_pythongen(kitchen_sink_path):
"""python"""
kitchen_module = make_python(kitchen_sink_path)
c = kitchen_module.Company("ROR:1")
assert str(c) == "Company(id='ROR:1', name=None, aliases=[], ceo=None)"
assert str(c) == "Company({'id': 'ROR:1'})"
h = kitchen_module.EmploymentEvent(employed_at=c.id)
assert str(h) == (
"EmploymentEvent(started_at_time=None, ended_at_time=None, is_current=None, "
"metadata=None, employed_at='ROR:1', type=None)"
)
assert str(h) == "EmploymentEvent({'employed_at': 'ROR:1'})"
p = kitchen_module.Person("P:1", has_employment_history=[h])
assert p.id == "P:1"
assert p.has_employment_history[0] is not None
Expand All @@ -37,26 +35,21 @@ def test_pythongen(kitchen_sink_path):
# however, inline in a non-list context does not
p2dict = {"id": "P:2", "has_birth_event": {"started_at_time": "1981-01-01"}}
json_loader.loads(p2dict, kitchen_module.Person)
assert str(p) == (
"Person(id='P:1', name=None, has_employment_history=[EmploymentEvent(started_at_time=None, "
"ended_at_time=None, is_current=None, metadata=None, employed_at='ROR:1', type=None)], "
"has_familial_relationships=[], has_medical_history=[], age_in_years=None, addresses=[], "
"has_birth_event=None, species_name=None, stomach_count=None, is_living=None, aliases=[])"
)
assert str(p) == "Person({'id': 'P:1', 'has_employment_history': [EmploymentEvent({'employed_at': 'ROR:1'})]})"

f = kitchen_module.FamilialRelationship(related_to="me", type="SIBLING_OF", cordialness="heartfelt")
assert str(f) == (
"FamilialRelationship(started_at_time=None, ended_at_time=None, related_to='me', "
"type='SIBLING_OF', cordialness=(text='heartfelt', description='warm and hearty friendliness'))"
)
assert str(f) == """FamilialRelationship({
'related_to': 'me',
'type': 'SIBLING_OF',
'cordialness': CordialnessEnum(text='heartfelt', description='warm and hearty friendliness')
})"""

diagnosis = kitchen_module.DiagnosisConcept(id="CODE:D0001", name="headache")
event = kitchen_module.MedicalEvent(in_location="GEO:1234", diagnosis=diagnosis)
assert str(event) == (
"MedicalEvent(started_at_time=None, ended_at_time=None, is_current=None, "
"metadata=None, in_location='GEO:1234', diagnosis=DiagnosisConcept(id='CODE:D0001', "
"name='headache', in_code_system=None), procedure=None)"
)
assert str(event) == """MedicalEvent({
'in_location': 'GEO:1234',
'diagnosis': DiagnosisConcept({'id': 'CODE:D0001', 'name': 'headache'})
})"""


def test_multiline_stuff(input_path):
Expand Down Expand Up @@ -96,3 +89,36 @@ def test_head():

output = PythonGenerator(yaml, format="py", metadata=False).serialize()
assert output.startswith("\n# id: https://w3id.org/biolink/metamodel")


def test_repr(kitchen_sink_path):
"""
Be default, don't create __repr__ for dataclasses, but do if requested!
"""
parentclass = """
class ParentClass:
def __repr__(self):
return "overridden"

def __post_init__(self, *args, **kwargs):
pass
"""


pstr = str(PythonGenerator(kitchen_sink_path).serialize())
pstr = parentclass + pstr
pstr = re.sub(r'\(YAMLRoot\)', '(ParentClass)', pstr)
kitchen_module = compile_python(pstr)

# if a dataclass has `repr=False`, it shouldn't override the parent class's
friend = kitchen_module.Friend(name='bestie')
assert repr(friend) == 'overridden'

# but we should be able to make pythongenerator do `repr=True`, where the dataclasses _do_ override
pstr = str(PythonGenerator(kitchen_sink_path, dataclass_repr=True).serialize())
pstr = parentclass + pstr
pstr = re.sub(r'\(YAMLRoot\)', '(ParentClass)', pstr)
kitchen_module = compile_python(pstr)
friend = kitchen_module.Friend(name='bestie')
assert repr(friend) != 'overridden'

4 changes: 2 additions & 2 deletions tests/test_issues/__snapshots__/issue_106.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class String(str):



@dataclass
@dataclass(repr=False)
class C1(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -70,7 +70,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class C2(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand Down
2 changes: 1 addition & 1 deletion tests/test_issues/__snapshots__/issue_113.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class NamedThing(YAMLRoot):
class_model_uri: ClassVar[URIRef] = URIRef("https://microbiomedata/schema/NamedThing")


@dataclass
@dataclass(repr=False)
class TestClass(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand Down
4 changes: 2 additions & 2 deletions tests/test_issues/__snapshots__/issue_120.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class String(str):



@dataclass
@dataclass(repr=False)
class Student(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -71,7 +71,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class Course(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand Down
2 changes: 1 addition & 1 deletion tests/test_issues/__snapshots__/issue_121.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@



@dataclass
@dataclass(repr=False)
class Biosample(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand Down
12 changes: 6 additions & 6 deletions tests/test_issues/__snapshots__/issue_134.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class EId(D1Id):
pass


@dataclass
@dataclass(repr=False)
class A(YAMLRoot):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -88,7 +88,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class B(A):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -112,7 +112,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class C(B):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -136,7 +136,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class D1(C):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -160,7 +160,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class D2(C):
_inherited_slots: ClassVar[List[str]] = []

Expand All @@ -180,7 +180,7 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
@dataclass(repr=False)
class E(D1):
_inherited_slots: ClassVar[List[str]] = []

Expand Down
Loading
Loading