Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure valid key validation is enabled for all components #493

Merged
merged 3 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions lumen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...]]] = []
Expand Down Expand Up @@ -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]):
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion lumen/filters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions lumen/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -492,8 +493,9 @@ class Layout(Component, Viewer):

_header_format: ClassVar[str] = '<div style="font-size: 1.5em; font-weight: bold;">{header}</div>'

# 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
Expand Down
1 change: 1 addition & 0 deletions lumen/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion lumen/sources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion lumen/tests/sample_dashboard/dashboard.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sources:
test:
type: 'file'
files: ['../sources/test.csv']
tables: ['../sources/test.csv']
pipelines:
test:
source: test
Expand Down
2 changes: 1 addition & 1 deletion lumen/tests/sample_dashboard/dashboard_legacy.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sources:
test:
type: 'file'
files: ['../sources/test.csv']
tables: ['../sources/test.csv']
targets:
- title: "Test"
source: test
Expand Down
2 changes: 1 addition & 1 deletion lumen/tests/sample_dashboard/sync_query.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ config:
sources:
test:
type: 'file'
files: ['../sources/test.csv']
tables: ['../sources/test.csv']
targets:
- title: "Test 1"
source: test
Expand Down
2 changes: 1 addition & 1 deletion lumen/tests/sample_dashboard/sync_query_filters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ config:
sources:
test:
type: 'file'
files: ['../sources/test.csv']
tables: ['../sources/test.csv']
targets:
- title: "Test"
source: test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ config:
sources:
test:
type: 'file'
files: ['../sources/test.csv']
tables: ['../sources/test.csv']
targets:
- title: "Test"
source: test
Expand Down
2 changes: 1 addition & 1 deletion lumen/tests/sample_dashboard/transform_variable.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ variables:
sources:
test:
type: 'file'
files: ['../sources/test.csv']
tables: ['../sources/test.csv']
kwargs:
parse_dates: ['D']
targets:
Expand Down
2 changes: 1 addition & 1 deletion lumen/tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 6 additions & 1 deletion lumen/tests/validation/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
(
Expand All @@ -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'})
5 changes: 5 additions & 0 deletions lumen/tests/validation/test_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'})
7 changes: 6 additions & 1 deletion lumen/tests/validation/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
Expand Down Expand Up @@ -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': []})
5 changes: 5 additions & 0 deletions lumen/tests/validation/test_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {}})
4 changes: 4 additions & 0 deletions lumen/tests/validation/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
9 changes: 7 additions & 2 deletions lumen/tests/validation/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{"format": "csv", "type": "default"},
),
(
{"formats": "csv"},
{},
"The Download component requires 'format' parameter to be defined",
),
(
Expand Down Expand Up @@ -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",
),
(
Expand All @@ -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'})
4 changes: 3 additions & 1 deletion lumen/transforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,6 +43,8 @@ class Transform(MultiTypeComponent):

_field_params: ClassVar[List[str]] = []

_valid_keys: ClassVar[List[str] | Literal['params'] | None] = 'params'

__abstract = True

@classmethod
Expand Down
13 changes: 11 additions & 2 deletions lumen/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down