diff --git a/lumen/filters/base.py b/lumen/filters/base.py index 535739f10..af8b6d2c5 100644 --- a/lumen/filters/base.py +++ b/lumen/filters/base.py @@ -7,7 +7,7 @@ import types from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Tuple, Type, + TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Literal, Tuple, Type, ) import bokeh # type: ignore @@ -60,6 +60,7 @@ class Filter(MultiTypeComponent): # Specification configuration _internal_params: ClassVar[List[str]] = ['name', 'schema'] _requires_field: ClassVar[bool] = True + _valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params' def __init__(self, **params): super().__init__(**params) diff --git a/lumen/pipeline.py b/lumen/pipeline.py index 7ec09de16..e1c6cbd4a 100644 --- a/lumen/pipeline.py +++ b/lumen/pipeline.py @@ -145,6 +145,7 @@ class Pipeline(Viewer, Component): _internal_params: ClassVar[List[str]] = ['data', 'name', 'schema', '_stale'] _required_fields: ClassVar[List[str | Tuple[str, str]]] = [('source', 'pipeline')] + _valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params' def __init__(self, *, source, table, **params): if 'schema' not in params: diff --git a/lumen/sources/base.py b/lumen/sources/base.py index a31cf81a4..00b8403f0 100644 --- a/lumen/sources/base.py +++ b/lumen/sources/base.py @@ -14,7 +14,7 @@ from os.path import basename from pathlib import Path from typing import ( - TYPE_CHECKING, Any, ClassVar, Dict, List, Tuple, Type, Union, + TYPE_CHECKING, Any, ClassVar, Dict, List, Literal, Tuple, Type, Union, ) from urllib.parse import quote, urlparse @@ -170,6 +170,9 @@ class Source(MultiTypeComponent): # Declare whether source supports SQL transforms _supports_sql: ClassVar[bool] = False + # Valid keys incude all parameters (except _internal_params) + _valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params' + @property def _reload_params(self) -> List[str]: "List of parameters that trigger a data reload." diff --git a/lumen/tests/validation/test_filters.py b/lumen/tests/validation/test_filters.py index cc6cfe0b9..691baa0d0 100644 --- a/lumen/tests/validation/test_filters.py +++ b/lumen/tests/validation/test_filters.py @@ -20,7 +20,7 @@ "The ConstantFilter component requires 'field' parameter to be defined", ), ( - {"type": "widget", "fields": "island"}, + {"type": "widget"}, "The WidgetFilter component requires 'field' parameter to be defined", ), ( @@ -41,3 +41,8 @@ def test_filter_Filter(spec, msg): else: with pytest.raises(ValidationError, match=msg): Filter.validate(spec) + + +def test_filter_key_validation(): + with pytest.raises(ValidationError, match="ConstantFilter component specification contained unknown key 'feld'"): + Filter.validate({'type': 'constant', 'feld': 'foo'}) diff --git a/lumen/tests/validation/test_layout.py b/lumen/tests/validation/test_layout.py index dc3f20a00..b3f266dd4 100644 --- a/lumen/tests/validation/test_layout.py +++ b/lumen/tests/validation/test_layout.py @@ -104,3 +104,8 @@ def test_layout_Layout(spec, msg): else: with pytest.raises(ValidationError, match=msg): Layout.validate(spec, context) + + +def test_layout_key_validation(): + with pytest.raises(ValidationError, match="Layout component specification contained unknown key 'src'"): + Layout.validate({'title': 'Table', 'src': 'penguins'}) diff --git a/lumen/tests/validation/test_pipeline.py b/lumen/tests/validation/test_pipeline.py index c765e7a81..2f0963d05 100644 --- a/lumen/tests/validation/test_pipeline.py +++ b/lumen/tests/validation/test_pipeline.py @@ -100,3 +100,8 @@ def test_pipeline_Pipeline(source, filters, transforms, msg): else: with pytest.raises(ValidationError, match=msg): Pipeline.validate(spec) + + +def test_pipeline_key_validation(): + with pytest.raises(ValidationError, match="Pipeline component specification contained unknown key 'transfomers'"): + Pipeline.validate({'source': None, 'transfomers': []}) diff --git a/lumen/tests/validation/test_sources.py b/lumen/tests/validation/test_sources.py index d727be710..208546640 100644 --- a/lumen/tests/validation/test_sources.py +++ b/lumen/tests/validation/test_sources.py @@ -57,3 +57,8 @@ def test_source_Source(spec, context, msg): else: with pytest.raises(ValidationError, match=msg): Source.validate(spec, context) + + +def test_source_key_validation(): + with pytest.raises(ValidationError, match="FileSource component specification contained unknown key 'tablas'"): + Source.validate({'type': 'file', 'tablas': {}}) diff --git a/lumen/tests/validation/test_transform.py b/lumen/tests/validation/test_transform.py index 105129f4f..e4d080e90 100644 --- a/lumen/tests/validation/test_transform.py +++ b/lumen/tests/validation/test_transform.py @@ -29,3 +29,7 @@ def test_transforms_Transform(spec, msg): else: with pytest.raises(ValidationError, match=msg): Transform.validate(spec) + +def test_transform_key_validation(): + with pytest.raises(ValidationError, match="Iloc component specification contained unknown key 'strt'"): + Transform.validate({'type': 'iloc', 'strt': 3}) diff --git a/lumen/tests/validation/test_views.py b/lumen/tests/validation/test_views.py index 31ef41836..164cb84fd 100644 --- a/lumen/tests/validation/test_views.py +++ b/lumen/tests/validation/test_views.py @@ -16,7 +16,7 @@ {"format": "csv", "type": "default"}, ), ( - {"formats": "csv"}, + {}, "The Download component requires 'format' parameter to be defined", ), ( @@ -46,7 +46,7 @@ def test_target_Download(spec, output): None, ), ( - {"type": "download", "formats": "csv"}, + {"type": "download"}, "The DownloadView component requires 'format' parameter to be defined", ), ( @@ -71,3 +71,8 @@ def test_target_View(spec, msg): else: with pytest.raises(ValidationError, match=msg): View.validate(spec) + + +def test_filter_key_validation(): + with pytest.raises(ValidationError, match="Table component specification contained unknown key 'feld'"): + View.validate({'type': 'table', 'feld': 'foo'}) diff --git a/lumen/transforms/base.py b/lumen/transforms/base.py index 04e3cf919..39768877d 100644 --- a/lumen/transforms/base.py +++ b/lumen/transforms/base.py @@ -7,7 +7,7 @@ import hashlib from typing import ( - TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Tuple, Union, + TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Literal, Tuple, Union, ) import numpy as np @@ -43,6 +43,8 @@ class Transform(MultiTypeComponent): _field_params: ClassVar[List[str]] = [] + _valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params' + __abstract = True @classmethod diff --git a/lumen/views/base.py b/lumen/views/base.py index c39ecc697..65300c2db 100644 --- a/lumen/views/base.py +++ b/lumen/views/base.py @@ -8,7 +8,7 @@ from io import BytesIO, StringIO from typing import ( - IO, TYPE_CHECKING, Any, ClassVar, Dict, List, Type, + IO, TYPE_CHECKING, Any, ClassVar, Dict, List, Literal, Type, ) from weakref import WeakKeyDictionary @@ -93,6 +93,8 @@ class View(MultiTypeComponent, Viewer): # Panel extension to load to render this View _extension: ClassVar[str | None] = None + _internal_params: ClassVar[List[str]] = ['rerender'] + # Parameters which reference fields in the table _field_params: ClassVar[List[str]] = ['field'] @@ -104,6 +106,8 @@ class View(MultiTypeComponent, Viewer): _panel_type: ClassVar[Type[Viewable] | None] = None + _valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params' + __abstract = True def __init__(self, **params):