Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

* Changed `compas_model.models.Model.__from_data__` return type to `Self` so subclasses retain their type when deserialized.

### Removed


Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = ["compas", "shapely"]
dependencies = ["compas", "shapely", "typing_extensions"]

[project.optional-dependencies]
dev = [
"build",
"bump-my-version",
"compas_invocations2",
"invoke >=0.14",
"pyright",
"pytest",
"pytest-dependency",
"ruff",
Expand Down
4 changes: 3 additions & 1 deletion src/compas_model/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import TypeVar
from typing import Union

from typing_extensions import Self

from compas.datastructures import Datastructure
from compas.geometry import Point
from compas.geometry import Transformation
Expand Down Expand Up @@ -59,7 +61,7 @@ def __data__(self) -> dict:
return data

@classmethod
def __from_data__(cls, data: dict) -> "Model":
def __from_data__(cls, data: dict) -> "Self":
model = cls()

model._transformation = data["transformation"]
Expand Down
71 changes: 71 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,79 @@
# assert c_model.tree is not None
# assert len(c_model.tree.elements) == 3

import subprocess
from pathlib import Path

from compas_model.models import Model # noqa: F401


def test_import():
assert True


def test_from_data_roundtrip_preserves_subclass_behavior():
class MyModel(Model):
pass

model = MyModel()
data = model.__data__
restored = MyModel.__from_data__(data)

assert isinstance(restored, MyModel)
assert restored.__data__ == data


def test_self_return_type_with_pyright(tmp_path: Path):
test_file = tmp_path / "typing_case.py"
test_file.write_text(
"""
from typing import assert_type

from compas_model.models import Model


class MyModel(Model):
pass


obj = MyModel.__from_data__(MyModel().__data__)
assert_type(obj, MyModel)
"""
)

result = subprocess.run(
["pyright", str(test_file)],
text=True,
capture_output=True,
cwd=Path(__file__).parents[1],
)

assert result.returncode == 0, result.stdout + result.stderr


def test_from_data_with_overridden_subclass():
class MyModel(Model):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._extra = None

@property
def __data__(self):
data = super().__data__
data["extra"] = self._extra
return data

@classmethod
def __from_data__(cls, data):
model = super().__from_data__(data)
model._extra = data.get("extra")
return model

model = MyModel()
model._extra = "hello"
data = model.__data__

restored = MyModel.__from_data__(data)

assert isinstance(restored, MyModel)
assert restored._extra == "hello"
Loading