diff --git a/lumen/base.py b/lumen/base.py index 1e9bb5add..163a18f5a 100644 --- a/lumen/base.py +++ b/lumen/base.py @@ -49,6 +49,9 @@ class Component(param.Parameterized): # component specification _internal_params: ClassVar[List[str]] = ['name'] + # Deprecated parameters that are still allowed as spec keys + _legacy_params: ClassVar[List[str]] = [] + # Keys that must be declared declared as a list of strings or # tuples of strings if one of multiple must be defined. _required_keys: ClassVar[List[str | Tuple[str, ...]]] = [] @@ -134,9 +137,10 @@ def _sync_refs(self, trigger: bool = True): @classproperty def _valid_keys_(cls) -> List[str] | None: if cls._valid_keys == 'params': - return [p for p in cls.param if p not in cls._internal_params] + valid = [p for p in cls.param if p not in cls._internal_params] else: - return cls._valid_keys + valid = cls._valid_keys + return valid if valid is None else valid + cls._legacy_params @classmethod def _validate_keys_(cls, spec: Dict[str, Any]): @@ -451,7 +455,7 @@ def _valid_keys_(cls) -> List[str | Tuple[str, ...]] | None: if valid is not None and 'type' not in valid: valid.append('type') - return valid + return valid if valid is None else valid + cls._legacy_params @classproperty def _base_type(cls): 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/layout.py b/lumen/layout.py index c8b060a2c..49a916bb3 100644 --- a/lumen/layout.py +++ b/lumen/layout.py @@ -264,8 +264,9 @@ class Facet(Component): sort = param.ListSelector(default=[], objects=[], doc=""" List of fields to sort by.""") - _valid_keys: ClassVar[Literal['params']] = 'params' + # Validation attributes _required_keys: ClassVar[List[str | Tuple[str, ...]]] = ['by'] + _valid_keys: ClassVar[Literal['params']] = 'params' def __init__(self, **params): super().__init__(**params) @@ -492,8 +493,9 @@ class Layout(Component, Viewer): _header_format: ClassVar[str] = '
{header}
' + # Validation attributes + _legacy_params: ClassVar[List[str]] = ['table'] _required_keys: ClassVar[List[str | Tuple[str, ...]]] = ['title', 'views'] - _valid_keys: ClassVar[List[str]] = [ 'config', 'facet_layout', 'sort', # Deprecated 'layout', 'refresh_rate', 'reloadable', 'show_title', 'title', 'tsformat', 'description', # Simple 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/sample_dashboard/dashboard.yaml b/lumen/tests/sample_dashboard/dashboard.yaml index fadeb370e..a3fdf4599 100644 --- a/lumen/tests/sample_dashboard/dashboard.yaml +++ b/lumen/tests/sample_dashboard/dashboard.yaml @@ -1,7 +1,7 @@ sources: test: type: 'file' - files: ['../sources/test.csv'] + tables: ['../sources/test.csv'] pipelines: test: source: test diff --git a/lumen/tests/sample_dashboard/dashboard_legacy.yaml b/lumen/tests/sample_dashboard/dashboard_legacy.yaml index 065d31d61..b0718900f 100644 --- a/lumen/tests/sample_dashboard/dashboard_legacy.yaml +++ b/lumen/tests/sample_dashboard/dashboard_legacy.yaml @@ -1,7 +1,7 @@ sources: test: type: 'file' - files: ['../sources/test.csv'] + tables: ['../sources/test.csv'] targets: - title: "Test" source: test diff --git a/lumen/tests/sample_dashboard/sync_query.yaml b/lumen/tests/sample_dashboard/sync_query.yaml index 2e7dab85e..378cc580d 100644 --- a/lumen/tests/sample_dashboard/sync_query.yaml +++ b/lumen/tests/sample_dashboard/sync_query.yaml @@ -4,7 +4,7 @@ config: sources: test: type: 'file' - files: ['../sources/test.csv'] + tables: ['../sources/test.csv'] targets: - title: "Test 1" source: test diff --git a/lumen/tests/sample_dashboard/sync_query_filters.yaml b/lumen/tests/sample_dashboard/sync_query_filters.yaml index 6a7b6d270..b1eb3fa0c 100644 --- a/lumen/tests/sample_dashboard/sync_query_filters.yaml +++ b/lumen/tests/sample_dashboard/sync_query_filters.yaml @@ -3,7 +3,7 @@ config: sources: test: type: 'file' - files: ['../sources/test.csv'] + tables: ['../sources/test.csv'] targets: - title: "Test" source: test diff --git a/lumen/tests/sample_dashboard/sync_query_filters_default.yaml b/lumen/tests/sample_dashboard/sync_query_filters_default.yaml index 8ea7a15ff..5920e898d 100644 --- a/lumen/tests/sample_dashboard/sync_query_filters_default.yaml +++ b/lumen/tests/sample_dashboard/sync_query_filters_default.yaml @@ -3,7 +3,7 @@ config: sources: test: type: 'file' - files: ['../sources/test.csv'] + tables: ['../sources/test.csv'] targets: - title: "Test" source: test diff --git a/lumen/tests/sample_dashboard/transform_variable.yaml b/lumen/tests/sample_dashboard/transform_variable.yaml index 897da41a0..dbe5d0846 100644 --- a/lumen/tests/sample_dashboard/transform_variable.yaml +++ b/lumen/tests/sample_dashboard/transform_variable.yaml @@ -5,7 +5,7 @@ variables: sources: test: type: 'file' - files: ['../sources/test.csv'] + tables: ['../sources/test.csv'] kwargs: parse_dates: ['D'] targets: diff --git a/lumen/tests/test_dashboard.py b/lumen/tests/test_dashboard.py index 29bec53ef..d6e5717ff 100644 --- a/lumen/tests/test_dashboard.py +++ b/lumen/tests/test_dashboard.py @@ -33,7 +33,7 @@ def test_dashboard_with_local_view_legacy(set_root): def test_dashboard_from_spec(): spec = { 'sources': { - 'test': {'type': 'file', 'files': ['./sources/test.csv']} + 'test': {'type': 'file', 'tables': ['./sources/test.csv']} }, 'layouts': [{ 'title': 'Test', 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..648aa4700 100644 --- a/lumen/tests/validation/test_pipeline.py +++ b/lumen/tests/validation/test_pipeline.py @@ -51,7 +51,7 @@ ), ( {"type": "file", "tables": {"penguins": "url.csv"}}, - [{"type": "widget", "fields": "species"}], + [{"type": "widget"}], [{"type": "aggregate", "method": "mean", "by": ["species", "sex", "year"]}], "The WidgetFilter component requires 'field' parameter to be defined", ), @@ -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..b32e00d78 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 @@ -96,13 +96,22 @@ class View(MultiTypeComponent, Viewer): # Parameters which reference fields in the table _field_params: ClassVar[List[str]] = ['field'] + # Optionally declares the Panel types used to render this View + _panel_type: ClassVar[Type[Viewable] | None] = None + + # Whether this View can be rendered without a data source _requires_source: ClassVar[bool] = True + # Internal cache of link_selections objects _selections: ClassVar[WeakKeyDictionary[Document, link_selections]] = WeakKeyDictionary() + # Whether this source supports linked selections _supports_selections: ClassVar[bool] = False - _panel_type: ClassVar[Type[Viewable] | None] = None + # Validation attributes + _internal_params: ClassVar[List[str]] = ['name', 'rerender'] + _legacy_params: ClassVar[List[str]] = ['sql_transforms', 'source', 'table', 'transforms'] + _valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params' __abstract = True