From a9a0ede95b9516347a8a968bb7a510b74f9e2ca7 Mon Sep 17 00:00:00 2001 From: Jacob Lambert Date: Sat, 10 Oct 2026 11:53:35 +0900 Subject: [PATCH] feat(schema): add T4Dataset 1.0 table names and registry --- t4_devkit/schema/tables/registry.py | 20 +++++--- t4_devkit/schema/v1/__init__.py | 2 + t4_devkit/schema/v1/attribute.py | 4 ++ t4_devkit/schema/v1/caption.py | 3 ++ t4_devkit/schema/v1/category.py | 4 ++ t4_devkit/schema/v1/ego_pose.py | 4 ++ t4_devkit/schema/v1/instance.py | 3 ++ t4_devkit/schema/v1/keypoint.py | 3 ++ t4_devkit/schema/v1/log.py | 4 ++ t4_devkit/schema/v1/map.py | 4 ++ t4_devkit/schema/v1/name.py | 60 ++++++++++++++++++++++++ t4_devkit/schema/v1/object_ann.py | 3 ++ t4_devkit/schema/v1/registry.py | 9 ++++ t4_devkit/schema/v1/route.py | 4 ++ t4_devkit/schema/v1/sample.py | 4 ++ t4_devkit/schema/v1/sample_annotation.py | 3 ++ t4_devkit/schema/v1/sample_data.py | 4 ++ t4_devkit/schema/v1/scenario.py | 3 ++ t4_devkit/schema/v1/scene.py | 4 ++ t4_devkit/schema/v1/sensor.py | 4 ++ t4_devkit/schema/v1/traffic_light.py | 3 ++ t4_devkit/schema/v1/vehicle.py | 4 ++ t4_devkit/schema/v1/vehicle_state.py | 4 ++ t4_devkit/schema/v1/visibility.py | 4 ++ tests/schema/v1/test_registry.py | 38 +++++++++++++++ 25 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 t4_devkit/schema/v1/name.py create mode 100644 t4_devkit/schema/v1/registry.py create mode 100644 tests/schema/v1/test_registry.py diff --git a/t4_devkit/schema/tables/registry.py b/t4_devkit/schema/tables/registry.py index 9f910cfb..121ef635 100644 --- a/t4_devkit/schema/tables/registry.py +++ b/t4_devkit/schema/tables/registry.py @@ -1,6 +1,7 @@ from __future__ import annotations import inspect +from enum import Enum from typing import TYPE_CHECKING, Callable, KeysView from ..name import SchemaName @@ -14,9 +15,14 @@ class SchemaRegistry: - """A manager class to register schema tables.""" + """A manager class to register schema tables. - def __init__(self) -> None: + Args: + name_type (type[Enum], optional): Enum of the table names this registry accepts. + """ + + def __init__(self, name_type: type[Enum] = SchemaName) -> None: + self.__name_type = name_type self.__schemas: dict[str, SchemaTable] = {} @property @@ -27,13 +33,13 @@ def __len__(self) -> int: return len(self.__schemas) def __contains__(self, key: str | SchemaName) -> bool: - if isinstance(key, SchemaName): + if isinstance(key, self.__name_type): key = key.value return key in self.keys() def get(self, key: str | SchemaName) -> SchemaTable: - if isinstance(key, SchemaName): + if isinstance(key, self.__name_type): key = key.value if key not in self.__schemas: @@ -44,8 +50,8 @@ def keys(self) -> KeysView[str]: return self.__schemas.keys() def register(self, name: SchemaName, *, force: bool = False) -> Callable: - if not isinstance(name, SchemaName): - raise TypeError(f"name must be SchemaName, but got {type(name)}.") + if not isinstance(name, self.__name_type): + raise TypeError(f"name must be {self.__name_type.__name__}, but got {type(name)}.") def _register_decorator(obj: object) -> object: self._add_module(obj, name=name, force=force) @@ -72,7 +78,7 @@ def build_from_json(self, key: str | SchemaName, filepath: PathLike) -> list[Sch Returns: Instantiated dataclass. """ - if isinstance(key, SchemaName): + if isinstance(key, self.__name_type): key = key.value schema: SchemaTable = self.get(key) diff --git a/t4_devkit/schema/v1/__init__.py b/t4_devkit/schema/v1/__init__.py index 0b6094a0..c7172f29 100644 --- a/t4_devkit/schema/v1/__init__.py +++ b/t4_devkit/schema/v1/__init__.py @@ -8,7 +8,9 @@ from .log import * # noqa from .manifest import * # noqa from .map import * # noqa +from .name import * # noqa from .object_ann import * # noqa +from .registry import * # noqa from .route import * # noqa from .sample import * # noqa from .sample_annotation import * # noqa diff --git a/t4_devkit/schema/v1/attribute.py b/t4_devkit/schema/v1/attribute.py index 6531af12..cc6699d3 100644 --- a/t4_devkit/schema/v1/attribute.py +++ b/t4_devkit/schema/v1/attribute.py @@ -4,10 +4,14 @@ from t4_devkit.schema.tables.base import SchemaBase +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["Attribute"] @define(slots=False) +@SCHEMAS.register(SchemaName.ATTRIBUTE) class Attribute(SchemaBase): """`attribute.json` dataclass. diff --git a/t4_devkit/schema/v1/caption.py b/t4_devkit/schema/v1/caption.py index 4ed64ea1..791dae9d 100644 --- a/t4_devkit/schema/v1/caption.py +++ b/t4_devkit/schema/v1/caption.py @@ -7,6 +7,8 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from .annotation_type import AnnotationType +from .name import SchemaName +from .registry import SCHEMAS __all__ = ["CaptionType", "Caption"] @@ -29,6 +31,7 @@ class CaptionType(str, Enum): @define(slots=False) +@SCHEMAS.register(SchemaName.CAPTION) class Caption(SchemaBase): """`caption.json` dataclass. diff --git a/t4_devkit/schema/v1/category.py b/t4_devkit/schema/v1/category.py index 5a6fbb29..18821109 100644 --- a/t4_devkit/schema/v1/category.py +++ b/t4_devkit/schema/v1/category.py @@ -4,10 +4,14 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["Category"] @define(slots=False) +@SCHEMAS.register(SchemaName.CATEGORY) class Category(SchemaBase): """`category.json` dataclass. diff --git a/t4_devkit/schema/v1/ego_pose.py b/t4_devkit/schema/v1/ego_pose.py index 5d359763..3b4e6da1 100644 --- a/t4_devkit/schema/v1/ego_pose.py +++ b/t4_devkit/schema/v1/ego_pose.py @@ -8,6 +8,9 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from t4_devkit.typing import Quaternion, Vector3, Vector6 +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["EgoPoseSource", "EgoPose"] @@ -21,6 +24,7 @@ class EgoPoseSource(str, Enum): @define(slots=False) +@SCHEMAS.register(SchemaName.EGO_POSE) class EgoPose(SchemaBase): """`ego_pose.json` dataclass. diff --git a/t4_devkit/schema/v1/instance.py b/t4_devkit/schema/v1/instance.py index ee758402..b366802d 100644 --- a/t4_devkit/schema/v1/instance.py +++ b/t4_devkit/schema/v1/instance.py @@ -5,11 +5,14 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from .map import MapElement +from .name import SchemaName +from .registry import SCHEMAS __all__ = ["Instance"] @define(slots=False) +@SCHEMAS.register(SchemaName.INSTANCE) class Instance(SchemaBase): """`instance.json` dataclass. diff --git a/t4_devkit/schema/v1/keypoint.py b/t4_devkit/schema/v1/keypoint.py index 46e24371..e18c4916 100644 --- a/t4_devkit/schema/v1/keypoint.py +++ b/t4_devkit/schema/v1/keypoint.py @@ -6,11 +6,14 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from .annotation_type import AnnotationType +from .name import SchemaName +from .registry import SCHEMAS __all__ = ["Keypoint"] @define(slots=False) +@SCHEMAS.register(SchemaName.KEYPOINT) class Keypoint(SchemaBase): """`keypoint.json` dataclass. diff --git a/t4_devkit/schema/v1/log.py b/t4_devkit/schema/v1/log.py index 3398f31e..3440854a 100644 --- a/t4_devkit/schema/v1/log.py +++ b/t4_devkit/schema/v1/log.py @@ -4,10 +4,14 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["Log"] @define(slots=False) +@SCHEMAS.register(SchemaName.LOG) class Log(SchemaBase): """`log.json` dataclass. diff --git a/t4_devkit/schema/v1/map.py b/t4_devkit/schema/v1/map.py index e0f38464..06a3f81c 100644 --- a/t4_devkit/schema/v1/map.py +++ b/t4_devkit/schema/v1/map.py @@ -6,6 +6,9 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty +from .name import SchemaName +from .registry import SCHEMAS + __all__ = [ "MapFormat", "ProjectorType", @@ -98,6 +101,7 @@ class MapElement: @define(slots=False) +@SCHEMAS.register(SchemaName.MAP) class Map(SchemaBase): """`map.json` dataclass. diff --git a/t4_devkit/schema/v1/name.py b/t4_devkit/schema/v1/name.py new file mode 100644 index 00000000..91d4b34a --- /dev/null +++ b/t4_devkit/schema/v1/name.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from enum import Enum, unique + +__all__ = ["SchemaName"] + + +@unique +class SchemaName(str, Enum): + """Names of the T4Dataset 1.0 tables, one JSON file each in `annotation/`. + + Required tables: sample, sample_data, ego_pose, sensor, scene, log, vehicle. + All other tables are optional; `manifest.json` declares which ones a dataset contains. + """ + + SAMPLE = "sample" + SAMPLE_DATA = "sample_data" + EGO_POSE = "ego_pose" + SENSOR = "sensor" + SCENE = "scene" + LOG = "log" + VEHICLE = "vehicle" + MAP = "map" # optional + VEHICLE_STATE = "vehicle_state" # optional + CATEGORY = "category" # optional + ATTRIBUTE = "attribute" # optional + VISIBILITY = "visibility" # optional + INSTANCE = "instance" # optional + SAMPLE_ANNOTATION = "sample_annotation" # optional + OBJECT_ANN = "object_ann" # optional + KEYPOINT = "keypoint" # optional + TRAFFIC_LIGHT = "traffic_light" # optional + ROUTE = "route" # optional + SCENARIO = "scenario" # optional + CAPTION = "caption" # optional + + @property + def filename(self) -> str: + """Return the annotation json filename. + + Returns: + Annotation json filename. + """ + return f"{self.value}.json" + + def is_optional(self) -> bool: + """Indicates if this table is optional. + + Returns: + Return True if this table is optional. + """ + return self not in ( + SchemaName.SAMPLE, + SchemaName.SAMPLE_DATA, + SchemaName.EGO_POSE, + SchemaName.SENSOR, + SchemaName.SCENE, + SchemaName.LOG, + SchemaName.VEHICLE, + ) diff --git a/t4_devkit/schema/v1/object_ann.py b/t4_devkit/schema/v1/object_ann.py index f8a8b098..fe10a44e 100644 --- a/t4_devkit/schema/v1/object_ann.py +++ b/t4_devkit/schema/v1/object_ann.py @@ -6,11 +6,14 @@ from t4_devkit.schema.tables.object_ann import RLEMask from .annotation_type import AnnotationType +from .name import SchemaName +from .registry import SCHEMAS __all__ = ["ObjectAnn"] @define(slots=False) +@SCHEMAS.register(SchemaName.OBJECT_ANN) class ObjectAnn(SchemaBase): """`object_ann.json` dataclass. diff --git a/t4_devkit/schema/v1/registry.py b/t4_devkit/schema/v1/registry.py new file mode 100644 index 00000000..4a4f48c0 --- /dev/null +++ b/t4_devkit/schema/v1/registry.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from t4_devkit.schema.tables.registry import SchemaRegistry + +from .name import SchemaName + +__all__ = ["SCHEMAS"] + +SCHEMAS = SchemaRegistry(SchemaName) diff --git a/t4_devkit/schema/v1/route.py b/t4_devkit/schema/v1/route.py index 2c2e5ed8..aadc57b8 100644 --- a/t4_devkit/schema/v1/route.py +++ b/t4_devkit/schema/v1/route.py @@ -8,6 +8,9 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from t4_devkit.typing import Quaternion, Vector3 +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["RouteSource", "RoutePose", "RouteSegment", "Route"] @@ -51,6 +54,7 @@ class RouteSegment: @define(slots=False) +@SCHEMAS.register(SchemaName.ROUTE) class Route(SchemaBase): """`route.json` dataclass. diff --git a/t4_devkit/schema/v1/sample.py b/t4_devkit/schema/v1/sample.py index 902e8de7..bcdf0fa3 100644 --- a/t4_devkit/schema/v1/sample.py +++ b/t4_devkit/schema/v1/sample.py @@ -4,10 +4,14 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["Sample"] @define(slots=False) +@SCHEMAS.register(SchemaName.SAMPLE) class Sample(SchemaBase): """`sample.json` dataclass. diff --git a/t4_devkit/schema/v1/sample_annotation.py b/t4_devkit/schema/v1/sample_annotation.py index 0d63deec..97742347 100644 --- a/t4_devkit/schema/v1/sample_annotation.py +++ b/t4_devkit/schema/v1/sample_annotation.py @@ -7,11 +7,14 @@ from t4_devkit.typing import Quaternion, Vector3 from .annotation_type import AnnotationType +from .name import SchemaName +from .registry import SCHEMAS __all__ = ["SampleAnnotation"] @define(slots=False) +@SCHEMAS.register(SchemaName.SAMPLE_ANNOTATION) class SampleAnnotation(SchemaBase): """`sample_annotation.json` dataclass. diff --git a/t4_devkit/schema/v1/sample_data.py b/t4_devkit/schema/v1/sample_data.py index a7d26da9..9ba46772 100644 --- a/t4_devkit/schema/v1/sample_data.py +++ b/t4_devkit/schema/v1/sample_data.py @@ -6,6 +6,9 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["FileFormat", "SampleData"] @@ -33,6 +36,7 @@ class FileFormat(str, Enum): @define(slots=False) +@SCHEMAS.register(SchemaName.SAMPLE_DATA) class SampleData(SchemaBase): """`sample_data.json` dataclass. diff --git a/t4_devkit/schema/v1/scenario.py b/t4_devkit/schema/v1/scenario.py index 7daf304e..c0657409 100644 --- a/t4_devkit/schema/v1/scenario.py +++ b/t4_devkit/schema/v1/scenario.py @@ -8,6 +8,8 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from .annotation_type import AnnotationType +from .name import SchemaName +from .registry import SCHEMAS __all__ = ["ScenarioStatus", "Taxonomy", "Scenario"] @@ -34,6 +36,7 @@ class Taxonomy: @define(slots=False) +@SCHEMAS.register(SchemaName.SCENARIO) class Scenario(SchemaBase): """`scenario.json` dataclass. diff --git a/t4_devkit/schema/v1/scene.py b/t4_devkit/schema/v1/scene.py index 52c25a2f..ba3f5735 100644 --- a/t4_devkit/schema/v1/scene.py +++ b/t4_devkit/schema/v1/scene.py @@ -4,10 +4,14 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["Scene"] @define(slots=False) +@SCHEMAS.register(SchemaName.SCENE) class Scene(SchemaBase): """`scene.json` dataclass. diff --git a/t4_devkit/schema/v1/sensor.py b/t4_devkit/schema/v1/sensor.py index 79b20e2c..2c21f2ef 100644 --- a/t4_devkit/schema/v1/sensor.py +++ b/t4_devkit/schema/v1/sensor.py @@ -10,6 +10,9 @@ from t4_devkit.schema.tables.sensor import SensorModality from t4_devkit.typing import CameraDistortion, CameraIntrinsic, Quaternion, Vector3 +from .name import SchemaName +from .registry import SCHEMAS + __all__ = [ "DistortionModel", "VideoFormat", @@ -199,6 +202,7 @@ class Hardware: @define(slots=False) +@SCHEMAS.register(SchemaName.SENSOR) class Sensor(SchemaBase): """`sensor.json` dataclass. diff --git a/t4_devkit/schema/v1/traffic_light.py b/t4_devkit/schema/v1/traffic_light.py index 1d0dea06..96ef746c 100644 --- a/t4_devkit/schema/v1/traffic_light.py +++ b/t4_devkit/schema/v1/traffic_light.py @@ -7,6 +7,8 @@ from t4_devkit.schema.tables.base import SchemaBase, impossible_empty from .annotation_type import AnnotationType +from .name import SchemaName +from .registry import SCHEMAS __all__ = [ "TrafficLightColor", @@ -79,6 +81,7 @@ class TrafficLightElement: @define(slots=False) +@SCHEMAS.register(SchemaName.TRAFFIC_LIGHT) class TrafficLight(SchemaBase): """`traffic_light.json` dataclass. diff --git a/t4_devkit/schema/v1/vehicle.py b/t4_devkit/schema/v1/vehicle.py index 6cac3f96..980fe0e5 100644 --- a/t4_devkit/schema/v1/vehicle.py +++ b/t4_devkit/schema/v1/vehicle.py @@ -4,10 +4,14 @@ from t4_devkit.schema.tables.base import SchemaBase +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["Vehicle"] @define(slots=False) +@SCHEMAS.register(SchemaName.VEHICLE) class Vehicle(SchemaBase): """`vehicle.json` dataclass. diff --git a/t4_devkit/schema/v1/vehicle_state.py b/t4_devkit/schema/v1/vehicle_state.py index 2f7b7fe7..03694bdf 100644 --- a/t4_devkit/schema/v1/vehicle_state.py +++ b/t4_devkit/schema/v1/vehicle_state.py @@ -7,6 +7,9 @@ from t4_devkit.schema.tables.base import SchemaBase from t4_devkit.schema.tables.vehicle_state import Indicators +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["ShiftState", "VehicleStateSource", "VehicleState"] @@ -33,6 +36,7 @@ class VehicleStateSource(str, Enum): @define(slots=False) +@SCHEMAS.register(SchemaName.VEHICLE_STATE) class VehicleState(SchemaBase): """`vehicle_state.json` dataclass. diff --git a/t4_devkit/schema/v1/visibility.py b/t4_devkit/schema/v1/visibility.py index 7c95012c..13b52263 100644 --- a/t4_devkit/schema/v1/visibility.py +++ b/t4_devkit/schema/v1/visibility.py @@ -6,6 +6,9 @@ from t4_devkit.schema.tables.base import SchemaBase +from .name import SchemaName +from .registry import SCHEMAS + __all__ = ["VisibilityLevel", "Visibility"] @@ -27,6 +30,7 @@ class VisibilityLevel(str, Enum): @define(slots=False) +@SCHEMAS.register(SchemaName.VISIBILITY) class Visibility(SchemaBase): """A dataclass to represent schema table of `visibility.json` in T4Dataset 1.0. diff --git a/tests/schema/v1/test_registry.py b/tests/schema/v1/test_registry.py new file mode 100644 index 00000000..17304381 --- /dev/null +++ b/tests/schema/v1/test_registry.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import pytest + +from t4_devkit.schema import SCHEMAS as SCHEMAS_V0 +from t4_devkit.schema import SchemaName as SchemaNameV0 +from t4_devkit.schema.v1 import SCHEMAS, Attribute, SchemaName + + +def test_every_table_is_registered() -> None: + assert set(SCHEMAS.keys()) == {name.value for name in SchemaName} + assert SchemaName.SAMPLE.filename == "sample.json" + + +def test_required_tables() -> None: + required = {name.value for name in SchemaName if not name.is_optional()} + assert required == {"sample", "sample_data", "ego_pose", "sensor", "scene", "log", "vehicle"} + + +def test_registries_are_separate() -> None: + """Each registry only accepts its own table names.""" + with pytest.raises(TypeError): + SCHEMAS.register(SchemaNameV0.ATTRIBUTE) + with pytest.raises(TypeError): + SCHEMAS_V0.register(SchemaName.ATTRIBUTE) + + +def test_override_table_class() -> None: + """A user can replace a table class, as documented for 0.x.""" + + @SCHEMAS.register(SchemaName.ATTRIBUTE, force=True) + class CustomAttribute(Attribute): + pass + + try: + assert SCHEMAS.get(SchemaName.ATTRIBUTE) is CustomAttribute + finally: + SCHEMAS.register(SchemaName.ATTRIBUTE, force=True)(Attribute)