diff --git a/poetry.lock b/poetry.lock index 70e6c905..2ec5b235 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1227,6 +1227,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "stringcase" +version = "1.2.0" +description = "String case converter." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, +] + [[package]] name = "termcolor" version = "2.1.1" @@ -1747,4 +1758,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.7.2, <4.0" -content-hash = "cf8d8ae156d93c7a12ec96e876570e02de64f8763d14b4a2263cffdf98651c00" +content-hash = "08a9cab6af36721f5cf65834281b641f93e60978fc54340f222c833a88aa5bb3" diff --git a/pyproject.toml b/pyproject.toml index bfe25a19..47cc2781 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ appdirs = "^1.4.4" gql = {extras = ["requests", "websockets"], version = "^3.3.0"} ujson = "^5.3.0" Deprecated = "^1.2.13" +stringcase = "^1.2.0" [tool.poetry.group.dev.dependencies] black = "^22.8.0" diff --git a/src/specklepy/objects/base.py b/src/specklepy/objects/base.py index 11150682..a56bc847 100644 --- a/src/specklepy/objects/base.py +++ b/src/specklepy/objects/base.py @@ -15,6 +15,8 @@ ) from warnings import warn +from stringcase import pascalcase + from specklepy.logging.exceptions import SpeckleException from specklepy.objects.units import Units, get_units_from_string from specklepy.transports.memory import MemoryTransport @@ -92,6 +94,7 @@ class _RegisteringBase: speckle_type: ClassVar[str] _speckle_type_override: ClassVar[Optional[str]] = None + _speckle_namespace: ClassVar[Optional[str]] = None _type_registry: ClassVar[Dict[str, Type["Base"]]] = {} _attr_types: ClassVar[Dict[str, Type]] = {} # dict of chunkable props and their max chunk size @@ -103,7 +106,11 @@ class _RegisteringBase: @classmethod def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]: """Get the registered type from the protected mapping via the `speckle_type`""" - return cls._type_registry.get(speckle_type, None) + for full_name in reversed(speckle_type.split(":")): + maybe_type = cls._type_registry.get(full_name, None) + if maybe_type: + return maybe_type + return None @classmethod def _determine_speckle_type(cls) -> str: @@ -122,12 +129,29 @@ def _determine_speckle_type(cls) -> str: return base_name bases = [ - b._speckle_type_override if b._speckle_type_override else b.__name__ + b._full_name() for b in reversed(cls.mro()) if issubclass(b, Base) and b.__name__ != base_name ] return ":".join(bases) + @classmethod + def _full_name(cls) -> str: + base_name = "Base" + if cls.__name__ == base_name: + return base_name + + if cls._speckle_type_override: + return cls._speckle_type_override + + # convert the module names to PascalCase to match c# namespace naming convention + # also drop specklepy from the beginning + namespace = ".".join( + pascalcase(m) + for m in filter(lambda name: name != "specklepy", cls.__module__.split(".")) + ) + return f"{namespace}.{cls.__name__}" + def __init_subclass__( cls, speckle_type: Optional[str] = None, @@ -145,13 +169,13 @@ def __init_subclass__( """ cls._speckle_type_override = speckle_type cls.speckle_type = cls._determine_speckle_type() - if cls.speckle_type in cls._type_registry: + if cls._full_name() in cls._type_registry: raise ValueError( f"The speckle_type: {speckle_type} is already registered for type: " - f"{cls._type_registry[cls.speckle_type].__name__}. " + f"{cls._type_registry[cls._full_name()].__name__}. " "Please choose a different type name." ) - cls._type_registry[cls.speckle_type] = cls # type: ignore + cls._type_registry[cls._full_name()] = cls # type: ignore try: cls._attr_types = get_type_hints(cls) except Exception: diff --git a/src/specklepy/objects/structural/__init__.py b/src/specklepy/objects/structural/__init__.py index c7bdae99..96907542 100644 --- a/src/specklepy/objects/structural/__init__.py +++ b/src/specklepy/objects/structural/__init__.py @@ -34,7 +34,7 @@ LoadNode, LoadType, ) -from specklepy.objects.structural.material import ( +from specklepy.objects.structural.materials import ( Concrete, MaterialType, Steel, diff --git a/src/specklepy/objects/structural/material.py b/src/specklepy/objects/structural/materials.py similarity index 94% rename from src/specklepy/objects/structural/material.py rename to src/specklepy/objects/structural/materials.py index a18e8d1f..ab0abcda 100644 --- a/src/specklepy/objects/structural/material.py +++ b/src/specklepy/objects/structural/materials.py @@ -40,7 +40,7 @@ class StructuralMaterial( materialSafetyFactor: float = 0.0 -class Concrete(StructuralMaterial, speckle_type=STRUCTURAL_MATERIALS + ".Concrete"): +class Concrete(StructuralMaterial): compressiveStrength: float = 0.0 tensileStrength: float = 0.0 flexuralStrength: float = 0.0 diff --git a/src/specklepy/objects/structural/properties.py b/src/specklepy/objects/structural/properties.py index 2d10780c..24021636 100644 --- a/src/specklepy/objects/structural/properties.py +++ b/src/specklepy/objects/structural/properties.py @@ -3,7 +3,7 @@ from specklepy.objects.base import Base from specklepy.objects.structural.axis import Axis -from specklepy.objects.structural.material import StructuralMaterial +from specklepy.objects.structural.materials import StructuralMaterial STRUCTURAL_PROPERTY = "Objectives.Structural.Properties" diff --git a/tests/test_registering_base.py b/tests/test_registering_base.py index b3f30552..d914a7e6 100644 --- a/tests/test_registering_base.py +++ b/tests/test_registering_base.py @@ -22,9 +22,9 @@ class Baz(Bar): "klass, speckle_type", [ (Base, "Base"), - (Foo, "Foo"), - (Bar, "Foo:Custom.Bar"), - (Baz, "Foo:Custom.Bar:Baz"), + (Foo, "Tests.TestRegisteringBase.Foo"), + (Bar, "Tests.TestRegisteringBase.Foo:Custom.Bar"), + (Baz, "Tests.TestRegisteringBase.Foo:Custom.Bar:Tests.TestRegisteringBase.Baz"), ( Concrete, "Objects.Structural.Materials.StructuralMaterial:Objects.Structural.Materials.Concrete", @@ -33,3 +33,15 @@ class Baz(Bar): ) def test_determine_speckle_type(klass: Type[Base], speckle_type: str): assert klass._determine_speckle_type() == speckle_type + + +@pytest.mark.parametrize( + "klass, fully_qualified_name", + [ + (Base, "Base"), + (Foo, "Tests.TestRegisteringBase.Foo"), + (Concrete, "Objects.Structural.Materials.Concrete"), + ], +) +def test_full_name(klass: Type[Base], fully_qualified_name: str): + assert klass._full_name() == fully_qualified_name diff --git a/tests/test_structural.py b/tests/test_structural.py index 95ea1b00..84de472f 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -11,7 +11,7 @@ Restraint, ) from specklepy.objects.structural.loading import LoadGravity -from specklepy.objects.structural.material import StructuralMaterial +from specklepy.objects.structural.materials import StructuralMaterial from specklepy.objects.structural.properties import ( MemberType, Property1D, diff --git a/tests/test_traverse_value.py b/tests/test_traverse_value.py index 0068204d..35d3ad22 100644 --- a/tests/test_traverse_value.py +++ b/tests/test_traverse_value.py @@ -16,7 +16,7 @@ def test_traverse_value(): object_id, object_dict = serializer.traverse_base(base) assert object_dict == { "id": object_id, - "speckle_type": "TestBase", + "speckle_type": "Tests.TestTraverseValue.TestBase", "applicationId": None, "foo": [None], "units": None,