diff --git a/dist/js/utils/class.js b/dist/js/utils/class.js index 29b12abc..bbc6c2f6 100644 --- a/dist/js/utils/class.js +++ b/dist/js/utils/class.js @@ -71,7 +71,7 @@ function extendThis(childClass, parentClass, config) { Object.defineProperty(childClass.prototype, prop, { get, set }); } else { - childClass.prototype[prop] = parentClass.prototype[prop]; + childClass.prototype[prop] = protos[prop]; } seen.push(prop); // don't override with older definition in hierarchy return null; diff --git a/src/js/utils/class.js b/src/js/utils/class.js index 6dc7ebc1..5082906e 100644 --- a/src/js/utils/class.js +++ b/src/js/utils/class.js @@ -66,7 +66,7 @@ export function extendThis(childClass, parentClass, config) { if (get || set) { Object.defineProperty(childClass.prototype, prop, { get, set }); } else { - childClass.prototype[prop] = parentClass.prototype[prop]; + childClass.prototype[prop] = protos[prop]; } seen.push(prop); // don't override with older definition in hierarchy return null; diff --git a/src/py/mat3ra/code/mixins/__init__.py b/src/py/mat3ra/code/mixins/__init__.py index 564362af..9c93fdc8 100644 --- a/src/py/mat3ra/code/mixins/__init__.py +++ b/src/py/mat3ra/code/mixins/__init__.py @@ -4,6 +4,7 @@ from mat3ra.esse.models.system.description import DescriptionSchema from mat3ra.esse.models.system.metadata import MetadataSchema from mat3ra.esse.models.system.name import NameEntitySchema +from .hashed_entity import HashedEntityMixin class DefaultableMixin(DefaultableEntitySchema): diff --git a/src/py/mat3ra/code/mixins/hashed_entity.py b/src/py/mat3ra/code/mixins/hashed_entity.py new file mode 100644 index 00000000..746e5523 --- /dev/null +++ b/src/py/mat3ra/code/mixins/hashed_entity.py @@ -0,0 +1,23 @@ +from typing import Any, Dict + +from mat3ra.utils.object import calculate_hash_from_object + + +class HashedEntityMixin: + """ + Mixin for entities that compute a deterministic hash from "meaningful fields". + + Mirrors the JS `HashedEntityMixin`: child classes override `get_hash_object()`, + while `calculate_hash()` hashes that object. + """ + + def get_hash_object(self) -> Dict[str, Any]: # pragma: no cover + return {} + + def calculate_hash(self) -> str: + return calculate_hash_from_object(self.get_hash_object()) + + @property + def hash(self) -> str: + return self.calculate_hash() + diff --git a/tests/js/utils/class.tests.ts b/tests/js/utils/class.tests.ts index a4ee4afd..77bd4f65 100644 --- a/tests/js/utils/class.tests.ts +++ b/tests/js/utils/class.tests.ts @@ -18,6 +18,15 @@ class BaseEntity extends RuntimeItemsMixin(InMemoryEntity) { } } +const PrototypeMethodMixin = (superclass: any) => + class extends superclass { + methodFromMixinPrototype() { + return "from-mixin"; + } + }; + +class BaseEntityWithPrototypeMethodFromMixin extends PrototypeMethodMixin(BaseEntity) {} + class ExtendClassEntity extends NamedInMemoryEntity { declare results: unknown; @@ -75,6 +84,13 @@ class ExtendThisEntity extends BetweenEntity { } } +class ExtendThisEntityFromMixedBase extends BetweenEntity { + constructor(config: object) { + super(config); + extendThis(ExtendThisEntityFromMixedBase, BaseEntityWithPrototypeMethodFromMixin, config); + } +} + defaultableEntityStaticMixin(ExtendThisEntity); defaultableEntityMixin(ExtendThisEntity.prototype); @@ -106,6 +122,11 @@ describe("extendThis", () => { expect(JSON.stringify(obj.results)).to.be.equal(JSON.stringify([{ name: "test" }])); }); + it("copies prototype methods defined by mixins", () => { + const obj = new ExtendThisEntityFromMixedBase({}); + expect((obj as any).methodFromMixinPrototype()).to.be.equal("from-mixin"); + }); + it("remembers intermediate methods", () => { const base = new BaseBetweenEntity({}); expect(base.betweenMethod()).to.be.equal("base"); diff --git a/tests/py/unit/test_mixins.py b/tests/py/unit/test_mixins.py index 64daaf01..ccd18e39 100644 --- a/tests/py/unit/test_mixins.py +++ b/tests/py/unit/test_mixins.py @@ -1,6 +1,7 @@ from typing import Optional -from mat3ra.code.mixins import DefaultableMixin, NamedMixin +from mat3ra.code.mixins import DefaultableMixin, HashedEntityMixin, NamedMixin +from mat3ra.utils.object import calculate_hash_from_object def test_defaultable_mixin(): @@ -60,3 +61,13 @@ class ExampleComplex(DefaultableMixin, NamedMixin): assert instance.key is None assert instance.number is None assert hasattr(instance, "isDefault") + + +def test_hashed_entity_mixin(): + class ExampleHashed(HashedEntityMixin): + def get_hash_object(self): + return {"b": 1, "a": 2} + + instance = ExampleHashed() + assert instance.calculate_hash() == calculate_hash_from_object({"b": 1, "a": 2}) + assert instance.hash == instance.calculate_hash()