Skip to content

Commit

Permalink
Issue #82 - Done more work making errors more type-specific to aid fu…
Browse files Browse the repository at this point in the history
…ture APIs providing tailored error messages to users.
  • Loading branch information
Rob Barry committed Aug 6, 2021
1 parent 6ad3650 commit e21b666
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 55 deletions.
1 change: 1 addition & 0 deletions csvqb/csvqb/models/cube/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from .cube import Cube
from .catalog import CatalogMetadataBase
from .csvqb import *
from .validationerrors import *
1 change: 1 addition & 0 deletions csvqb/csvqb/models/cube/csvqb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
from .components import *
from .catalog import CatalogMetadata
from ..cube import Cube
from .validationerrors import *

QbCube = Cube[CatalogMetadata]
45 changes: 18 additions & 27 deletions csvqb/csvqb/models/cube/csvqb/validationerrors.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
from abc import ABC
from dataclasses import dataclass, field
from typing import Type, Optional, Union
from dataclasses import dataclass
from typing import Optional, Type, Union

from csvqb.models.cube import (
QbDataStructureDefinition,
from csvqb.models.cube.csvqb.components import (
QbObservationValue,
QbMultiUnits,
QbDataStructureDefinition,
)
from csvqb.models.validationerror import ValidationError

from csvqb.models.validationerror import SpecificValidationError

ComponentTypeDescription = Union[str, Type[QbDataStructureDefinition]]


def _get_component_type_description(t: ComponentTypeDescription) -> str:
def get_description_for_component(t: ComponentTypeDescription) -> str:
if isinstance(t, str):
return t

return t.__name__


@dataclass
class SpecificValidationError(ValidationError, ABC):
"""Abstract base class to represent ValidationErrors which are more specific and so can be interpreted by code."""

message: str = field(init=False)


@dataclass
class OutputUriTemplateMissingError(SpecificValidationError):
"""
Expand All @@ -39,13 +30,13 @@ class OutputUriTemplateMissingError(SpecificValidationError):

def __post_init__(self):
self.message = (
f"'{self.csv_column_name}' - an {_get_component_type_description(self.component_type)} must have an "
f"'{self.csv_column_name}' - a {get_description_for_component(self.component_type)} must have an "
+ "output_uri_template defined."
)


@dataclass
class MaximumNumberOfComponentsError(SpecificValidationError):
class MaxNumComponentsExceededError(SpecificValidationError):
"""
Represents an error where the user can define a maximum number of components of a given type, but has defined
too many.
Expand All @@ -58,15 +49,15 @@ class MaximumNumberOfComponentsError(SpecificValidationError):

def __post_init__(self):
self.message = (
f"Found {self.actual_number} of {_get_component_type_description(self.component_type)}s. "
+ f"Expected maximum {self.maximum_number}."
f"Found {self.actual_number} of {get_description_for_component(self.component_type)}s. "
+ f"Expected a maximum of {self.maximum_number}."
)
if self.additional_explanation is not None:
self.message += " " + self.additional_explanation


@dataclass
class MinimumNumberOfComponentsError(SpecificValidationError):
class MinNumComponentsNotSatisfiedError(SpecificValidationError):
"""
Represents an error where the user must define a minimum number of components of a given type, but has not done so.
"""
Expand All @@ -78,7 +69,7 @@ class MinimumNumberOfComponentsError(SpecificValidationError):

def __post_init__(self):
self.message = (
f"At least {self.minimum_number} {_get_component_type_description(self.component_type)}s must be defined."
f"At least {self.minimum_number} {get_description_for_component(self.component_type)}s must be defined."
+ f" Found {self.actual_number}."
)
if self.additional_explanation is not None:
Expand All @@ -98,8 +89,8 @@ class WrongNumberComponentsError(SpecificValidationError):

def __post_init__(self):
self.message = (
f"Found {self.actual_number} {_get_component_type_description(self.component_type)}s."
+ f" Expected {self.expected_number}."
f"Found {self.actual_number} {get_description_for_component(self.component_type)}s."
+ f" Expected exactly {self.expected_number}."
)
if self.additional_explanation is not None:
self.message += " " + self.additional_explanation
Expand All @@ -117,8 +108,8 @@ class NeitherDefinedError(SpecificValidationError):

def __post_init__(self):
self.message = (
f"Found neither {_get_component_type_description(self.component_one)} "
+ f"nor {_get_component_type_description(self.component_two)} defined. "
f"Found neither {get_description_for_component(self.component_one)} "
+ f"nor {get_description_for_component(self.component_two)} defined. "
+ "One of these must be defined."
)

Expand Down Expand Up @@ -146,8 +137,8 @@ class IncompatibleComponentsError(SpecificValidationError):

def __post_init__(self):
self.message = (
f"Both {_get_component_type_description(self.component_one)} "
+ f"and {_get_component_type_description(self.component_two)} have been defined. "
f"Both {get_description_for_component(self.component_one)} "
+ f"and {get_description_for_component(self.component_two)} have been defined. "
+ f"These components cannot be used together."
)

Expand Down
24 changes: 9 additions & 15 deletions csvqb/csvqb/models/cube/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
import pandas as pd

from csvqb.models.validationerror import ValidationError
from .validationerrors import (
DuplicateColumnTitleError,
ColumnNotFoundInDataError,
MissingColumnDefinitionError,
)
from .columns import CsvColumn
from csvqb.models.cube.catalog import CatalogMetadataBase
from csvqb.inputs import pandas_input_to_columnar
from .catalog import CatalogMetadataBase
from ..pydanticmodel import PydanticModel

TMetadata = TypeVar("TMetadata", bound=CatalogMetadataBase, covariant=True)
Expand All @@ -35,9 +39,7 @@ def _validate_columns(self) -> List[ValidationError]:
existing_col_titles: Set[str] = set()
for col in self.columns:
if col.csv_column_title in existing_col_titles:
errors.append(
ValidationError(f"Duplicate column title '{col.csv_column_title}'")
)
errors.append(DuplicateColumnTitleError(col.csv_column_title))
else:
existing_col_titles.add(col.csv_column_title)

Expand All @@ -46,22 +48,14 @@ def _validate_columns(self) -> List[ValidationError]:
if col.csv_column_title in self.data.columns:
maybe_column_data = self.data[col.csv_column_title]
else:
errors.append(
ValidationError(
f"Column '{col.csv_column_title}' not found in data provided."
)
)
errors.append(ColumnNotFoundInDataError(col.csv_column_title))

errors += col.validate_data(maybe_column_data)

if self.data is not None:
defined_column_titles = [c.csv_column_title for c in self.columns]
for column in list(self.data.columns):
if column not in defined_column_titles:
errors.append(
ValidationError(
f"Column '{column}' does not have a mapping defined."
)
)
errors.append(MissingColumnDefinitionError(column))

return errors
41 changes: 41 additions & 0 deletions csvqb/csvqb/models/cube/validationerrors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from dataclasses import dataclass

from csvqb.models.validationerror import SpecificValidationError


@dataclass
class DuplicateColumnTitleError(SpecificValidationError):
"""
An error to inform the user that they have defined two instances of the same column.
"""

csv_column_title: str

def __post_init__(self):
self.message = f"Duplicate column title '{self.csv_column_title}'"


@dataclass
class ColumnNotFoundInDataError(SpecificValidationError):
"""
An error to inform the user that they have defined a column which cannot be found in the provided data.
"""

csv_column_title: str

def __post_init__(self):
self.message = f"Column '{self.csv_column_title}' not found in data provided."


@dataclass
class MissingColumnDefinitionError(SpecificValidationError):
"""
An error to inform the user that there is a column in their data that does not have a mapping specified.
"""

csv_column_title: str

def __post_init__(self):
self.message = (
f"Column '{self.csv_column_title}' does not have a mapping defined."
)
12 changes: 11 additions & 1 deletion csvqb/csvqb/models/validationerror.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@
ValidationError
---------------
"""
from dataclasses import dataclass
from dataclasses import dataclass, field
from abc import ABC


@dataclass
class ValidationError:
"""Class representing an error validating a model."""

message: str


@dataclass
class SpecificValidationError(ValidationError, ABC):
"""Abstract base class to represent ValidationErrors which are more specific and so can be interpreted by code."""

message: str = field(init=False)
12 changes: 6 additions & 6 deletions csvqb/csvqb/tests/unit/cube/qb/test_cubeqb_errorvalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from csvqb.models.cube import *
from csvqb.models.cube.csvqb.validationerrors import (
OutputUriTemplateMissingError,
MinimumNumberOfComponentsError,
MinNumComponentsNotSatisfiedError,
UnitsNotDefinedError,
BothUnitTypesDefinedError,
MaximumNumberOfComponentsError,
MaxNumComponentsExceededError,
WrongNumberComponentsError,
IncompatibleComponentsError,
)
Expand Down Expand Up @@ -148,7 +148,7 @@ def test_no_dimensions_validation_error():

assert_num_validation_errors(errors, 1)
error = errors[0]
assert isinstance(error, MinimumNumberOfComponentsError)
assert isinstance(error, MinNumComponentsNotSatisfiedError)
assert error.component_type == QbDimension
assert error.minimum_number == 1
assert error.actual_number == 0
Expand Down Expand Up @@ -258,7 +258,7 @@ def test_multiple_units_columns():

assert_num_validation_errors(errors, 1)
error = errors[0]
assert isinstance(error, MaximumNumberOfComponentsError)
assert isinstance(error, MaxNumComponentsExceededError)
assert error.component_type == QbMultiUnits
assert error.maximum_number == 1
assert error.actual_number == 2
Expand Down Expand Up @@ -393,7 +393,7 @@ def test_multi_measure_obs_val_with_multiple_measure_dimensions():

assert_num_validation_errors(errors, 1)
error = errors[0]
assert isinstance(error, MaximumNumberOfComponentsError)
assert isinstance(error, MaxNumComponentsExceededError)
assert error.component_type == QbMultiMeasureDimension
assert error.maximum_number == 1
assert error.actual_number == 2
Expand Down Expand Up @@ -442,7 +442,7 @@ def test_measure_dimension_with_single_measure_obs_val():
assert error.component_two == QbMultiMeasureDimension
assert (
error.additional_explanation
== "A single measure cube cannot have a measure dimension."
== "A single-measure cube cannot have a measure dimension."
)


Expand Down
6 changes: 6 additions & 0 deletions csvqb/csvqb/tests/unit/cube/test_cube_errorvalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def test_column_not_configured_error():

assert len(validation_errors) == 1
error = validation_errors[0]
assert isinstance(error, MissingColumnDefinitionError)
assert error.csv_column_title == "Some Dimension"
assert "Column 'Some Dimension'" in error.message


Expand All @@ -37,6 +39,8 @@ def test_column_title_wrong_error():

assert len(validation_errors) == 1
error = validation_errors[0]
assert isinstance(error, ColumnNotFoundInDataError)
assert error.csv_column_title == "Some Column Title"
assert "Column 'Some Column Title'" in error.message


Expand All @@ -59,6 +63,8 @@ def test_two_column_same_title():

assert len(validation_errors) == 1
error = validation_errors[0]
assert isinstance(error, DuplicateColumnTitleError)
assert error.csv_column_title == "Some Dimension"
assert "Duplicate column title 'Some Dimension'" == error.message


Expand Down
12 changes: 6 additions & 6 deletions csvqb/csvqb/utils/qb/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

from csvqb.models.cube.csvqb.validationerrors import (
OutputUriTemplateMissingError,
MinimumNumberOfComponentsError,
MaximumNumberOfComponentsError,
MinNumComponentsNotSatisfiedError,
MaxNumComponentsExceededError,
WrongNumberComponentsError,
UnitsNotDefinedError,
BothUnitTypesDefinedError,
Expand Down Expand Up @@ -74,7 +74,7 @@ def _validate_dimensions(cube: Cube) -> List[ValidationError]:
)

if len(dimension_columns) == 0:
errors.append(MinimumNumberOfComponentsError(QbDimension, 1, 0))
errors.append(MinNumComponentsNotSatisfiedError(QbDimension, 1, 0))
return errors


Expand Down Expand Up @@ -104,7 +104,7 @@ def _validate_observation_value_constraints(cube: Cube) -> List[ValidationError]

if len(multi_units_columns) > 1:
errors.append(
MaximumNumberOfComponentsError(QbMultiUnits, 1, len(multi_units_columns))
MaxNumComponentsExceededError(QbMultiUnits, 1, len(multi_units_columns))
)

if len(observed_value_columns) != 1:
Expand Down Expand Up @@ -149,7 +149,7 @@ def _validate_multi_measure_cube(
)
elif len(multi_measure_columns) > 1:
errors.append(
MaximumNumberOfComponentsError(
MaxNumComponentsExceededError(
QbMultiMeasureDimension, 1, len(multi_measure_columns)
)
)
Expand All @@ -168,7 +168,7 @@ def _validate_single_measure_cube(
IncompatibleComponentsError(
QbSingleMeasureObservationValue,
QbMultiMeasureDimension,
additional_explanation="A single measure cube cannot have a measure dimension.",
additional_explanation="A single-measure cube cannot have a measure dimension.",
)
)

Expand Down

0 comments on commit e21b666

Please sign in to comment.