diff --git a/api/src/opentrons/protocols/labware.py b/api/src/opentrons/protocols/labware.py index d3159ee5d9f..ec05e3cbb72 100644 --- a/api/src/opentrons/protocols/labware.py +++ b/api/src/opentrons/protocols/labware.py @@ -4,7 +4,7 @@ import json import os from pathlib import Path -from typing import Any, AnyStr, Dict, Optional, Union, List +from typing import Any, AnyStr, Dict, Optional, Union, List, Sequence, Literal import jsonschema # type: ignore @@ -17,10 +17,30 @@ USER_DEFS_PATH, ) from opentrons_shared_data.labware.types import LabwareDefinition +from opentrons_shared_data.errors.exceptions import InvalidProtocolData MODULE_LOG = logging.getLogger(__name__) +LabwareProblem = Literal[ + "no-schema-id", "bad-schema-id", "schema-mismatch", "invalid-json" +] + + +class NotALabwareError(InvalidProtocolData): + def __init__( + self, problem: LabwareProblem, wrapping: Sequence[BaseException] + ) -> None: + messages: dict[LabwareProblem, str] = { + "no-schema-id": "No schema ID present in file", + "bad-schema-id": "Bad schema ID in file", + "invalid-json": "File does not contain valid JSON", + "schema-mismatch": "File does not match labware schema", + } + super().__init__( + message=messages[problem], detail={"kind": problem}, wrapping=wrapping + ) + def get_labware_definition( load_name: str, @@ -126,7 +146,7 @@ def save_definition( json.dump(labware_def, f) -def verify_definition( +def verify_definition( # noqa: C901 contents: Union[AnyStr, LabwareDefinition, Dict[str, Any]] ) -> LabwareDefinition: """Verify that an input string is a labware definition and return it. @@ -146,15 +166,24 @@ def verify_definition( if isinstance(contents, dict): to_return = contents else: - to_return = json.loads(contents) + try: + to_return = json.loads(contents) + except json.JSONDecodeError as e: + raise NotALabwareError("invalid-json", [e]) from e try: schema_version = to_return["schemaVersion"] + except KeyError as e: + raise NotALabwareError("no-schema-id", [e]) from e + + try: schema = schemata_by_version[schema_version] - except KeyError: - raise RuntimeError( - f'Invalid or unknown labware schema version {to_return.get("schemaVersion", None)}' - ) - jsonschema.validate(to_return, schema) + except KeyError as e: + raise NotALabwareError("bad-schema-id", [e]) from e + + try: + jsonschema.validate(to_return, schema) + except jsonschema.ValidationError as e: + raise NotALabwareError("schema-mismatch", [e]) from e # we can type ignore this because if it passes the jsonschema it has # the correct structure diff --git a/api/src/opentrons/util/entrypoint_util.py b/api/src/opentrons/util/entrypoint_util.py index 2da4cac874c..508f6769bc5 100644 --- a/api/src/opentrons/util/entrypoint_util.py +++ b/api/src/opentrons/util/entrypoint_util.py @@ -6,7 +6,6 @@ from dataclasses import dataclass import json import logging -from json import JSONDecodeError import pathlib import subprocess import sys @@ -21,8 +20,6 @@ TYPE_CHECKING, ) -from jsonschema import ValidationError # type: ignore - from opentrons.calibration_storage.deck_configuration import ( deserialize_deck_configuration, ) @@ -32,7 +29,7 @@ JUPYTER_NOTEBOOK_LABWARE_DIR, SystemArchitecture, ) -from opentrons.protocol_api import labware +from opentrons.protocols import labware from opentrons.calibration_storage import helpers from opentrons.protocol_engine.errors.error_occurrence import ( ErrorOccurrence as ProtocolEngineErrorOccurrence, @@ -79,7 +76,7 @@ def labware_from_paths( if child.is_file() and child.suffix.endswith("json"): try: defn = labware.verify_definition(child.read_bytes()) - except (ValidationError, JSONDecodeError): + except labware.NotALabwareError: log.info(f"{child}: invalid labware, ignoring") log.debug( f"{child}: labware invalid because of this exception.", diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index b27b3c9c3d3..bdc952d5760 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -1,4 +1,5 @@ """Exception hierarchy for error codes.""" + from typing import Dict, Any, Optional, List, Iterator, Union, Sequence, overload from logging import getLogger from traceback import format_exception_only, format_tb @@ -1099,7 +1100,7 @@ def __init__( self, message: Optional[str] = None, detail: Optional[Dict[str, str]] = None, - wrapping: Optional[Sequence[EnumeratedError]] = None, + wrapping: Optional[Sequence[Union[EnumeratedError, BaseException]]] = None, ) -> None: """Build an InvalidProtocolData.""" super().__init__(ErrorCodes.INVALID_PROTOCOL_DATA, message, detail, wrapping)