diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 422c4de62..825139a05 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,21 +16,11 @@ concurrency: jobs: pre_commit: - name: Run pre-commit hooks + name: Run pre-commit runs-on: 'ubuntu-latest' steps: - - uses: actions/checkout@v3 - with: - fetch-depth: "1" - - name: set PY - run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - - uses: actions/cache@v3 - with: - path: ~/.cache/pre-commit - key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: pre-commit - uses: pre-commit/action@v3.0.0 - test_suite: + - uses: holoviz-dev/holoviz_tasks/pre-commit@v0.1a18 + unit_test_suite: name: Pytest on ${{ matrix.python-version }}, ${{ matrix.os }} needs: [pre_commit] runs-on: ${{ matrix.os }} @@ -47,9 +37,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: holoviz-dev/holoviz_tasks/install@v0.1a15 + - uses: holoviz-dev/holoviz_tasks/install@v0.1a18 with: - name: test_suite + name: unit_test_suite python-version: ${{ matrix.python-version }} channel-priority: flexible channels: pyviz/label/dev,bokeh,conda-forge,nodefaults diff --git a/lumen/filters/base.py b/lumen/filters/base.py index 1082980f5..535739f10 100644 --- a/lumen/filters/base.py +++ b/lumen/filters/base.py @@ -63,9 +63,13 @@ class Filter(MultiTypeComponent): def __init__(self, **params): super().__init__(**params) - if state.config and state.config.sync_with_url and self.sync_with_url and pn.state.location: + if self._sync_with_url: pn.state.location.sync(self, {'value': self.field}, on_error=self._url_sync_error) + @property + def _sync_with_url(self) -> bool: + return bool(state.config and state.config.sync_with_url and self.sync_with_url and pn.state.location) + @classproperty def _required_keys(cls) -> List[str | Tuple[str, ...]]: # type: ignore return ['field'] if cls._requires_field else [] @@ -313,8 +317,11 @@ def __init__(self, **params): self.widget.name = self.label self.widget.visible = self.visible self.widget.disabled = self.disabled + val = self.value self.widget.link(self, bidirectional=True, value='value', visible='visible', disabled='disabled') - if self.default is not None: + if val is not None: + self.widget.value = val + elif self.default is not None: self.widget.value = self.default @classmethod diff --git a/lumen/tests/sample_dashboard/sync_query_filters_default.yaml b/lumen/tests/sample_dashboard/sync_query_filters_default.yaml new file mode 100644 index 000000000..8ea7a15ff --- /dev/null +++ b/lumen/tests/sample_dashboard/sync_query_filters_default.yaml @@ -0,0 +1,18 @@ +config: + sync_with_url: true +sources: + test: + type: 'file' + files: ['../sources/test.csv'] +targets: + - title: "Test" + source: test + filters: + - type: widget + field: A + - type: widget + field: C + default: ['foo1'] + views: + - table: test + type: test diff --git a/lumen/tests/sources/test_duckdb.py b/lumen/tests/sources/test_duckdb.py index 3492fa1b2..460f6725c 100644 --- a/lumen/tests/sources/test_duckdb.py +++ b/lumen/tests/sources/test_duckdb.py @@ -45,7 +45,7 @@ def duckdb_source(): f"SET home_directory='{root}';" ], root=root, - sql_expr='SELECT A, B, C, D::TIMESTAMP AS D FROM {table}', + sql_expr='SELECT A, B, C, D::TIMESTAMP_NS AS D FROM {table}', tables={ 'test_sql': f"sqlite_scan('{root + '/test.db'}', 'mixed')", 'test_sql_with_none': f"sqlite_scan('{root + '/test.db'}', 'mixed_none')", @@ -81,15 +81,18 @@ def test_duckdb_get_schema(duckdb_source): 'type': 'string' } } - assert duckdb_source.get_schema('test_sql') == expected_sql + source = duckdb_source.get_schema('test_sql') + source["C"]["enum"].sort() + assert source == expected_sql assert list(duckdb_source._schema_cache.keys()) == ['test_sql'] def test_duckdb_get_schema_with_none(duckdb_source): + enum = ['foo1', None, 'foo3', 'foo5'] expected_sql = { 'A': {'inclusiveMaximum': 4.0, 'inclusiveMinimum': 0.0, 'type': 'number'}, 'B': {'inclusiveMaximum': 1.0, 'inclusiveMinimum': 0.0, 'type': 'number'}, - 'C': {'enum': ['foo1', np.nan, 'foo3', 'foo5'], 'type': 'string'}, + 'C': {'enum': enum, 'type': 'string'}, 'D': { 'format': 'datetime', 'inclusiveMaximum': '2009-01-07 00:00:00', @@ -97,7 +100,9 @@ def test_duckdb_get_schema_with_none(duckdb_source): 'type': 'string' } } - assert duckdb_source.get_schema('test_sql_with_none') == expected_sql + source = duckdb_source.get_schema('test_sql_with_none') + source["C"]["enum"].sort(key=enum.index) + assert source == expected_sql assert list(duckdb_source._schema_cache.keys()) == ['test_sql_with_none'] diff --git a/lumen/tests/test_dashboard.py b/lumen/tests/test_dashboard.py index 262b721c5..29bec53ef 100644 --- a/lumen/tests/test_dashboard.py +++ b/lumen/tests/test_dashboard.py @@ -90,6 +90,37 @@ def test_dashboard_with_url_sync_filters(set_root, document): pn.state.location.search = '?A=%5B0.3%2C+0.8%5D&C=%5B%22foo1%22%2C+%22foo2%22%2C+%22foo3%22%5D' assert f2.value == ['foo1', 'foo2', 'foo3'] +def test_dashboard_with_url_sync_filters_with_default(set_root, document): + root = pathlib.Path(__file__).parent / 'sample_dashboard' + set_root(str(root)) + dashboard = Dashboard(str(root / 'sync_query_filters_default.yaml')) + dashboard._render_dashboard() + layout = dashboard.layouts[0] + f1, f2 = list(layout._pipelines.values())[0].filters + f1.value = (0.1, 0.7) + assert pn.state.location.search == '?C=%5B%27foo1%27%5D&A=%5B0.1%2C+0.7%5D' + pn.state.location.search = '?A=%5B0.3%2C+0.8%5D' + assert f1.value == (0.3, 0.8) + assert f2.value == ['foo1'] + assert f1.widget.value == (0.3, 0.8) + assert f2.widget.value == ['foo1'] + f2.value = ['foo1', 'foo2'] + assert pn.state.location.search == '?A=%5B0.3%2C+0.8%5D&C=%5B%22foo1%22%2C+%22foo2%22%5D' + pn.state.location.search = '?A=%5B0.3%2C+0.8%5D&C=%5B%22foo1%22%2C+%22foo2%22%2C+%22foo3%22%5D' + assert f2.value == ['foo1', 'foo2', 'foo3'] + assert f2.widget.value == ['foo1', 'foo2', 'foo3'] + +def test_dashboard_with_url_sync_filters_with_overwritten_default(set_root, document): + root = pathlib.Path(__file__).parent / 'sample_dashboard' + set_root(str(root)) + dashboard = Dashboard(str(root / 'sync_query_filters_default.yaml')) + dashboard._render_dashboard() + layout = dashboard.layouts[0] + f1, f2 = list(layout._pipelines.values())[0].filters + f1.value = (0.1, 0.7) + f2.value = [] # overwriting default with empty list + assert pn.state.location.search == '?C=%5B%5D&A=%5B0.1%2C+0.7%5D' + assert f2.widget.value == [] @sql_available def test_dashboard_with_sql_source_and_transforms(set_root, document, mixed_df_object_type): diff --git a/lumen/util.py b/lumen/util.py index 106a0d7c3..2aa399fe5 100644 --- a/lumen/util.py +++ b/lumen/util.py @@ -15,6 +15,7 @@ import bokeh import pandas as pd import panel as pn +import param from jinja2 import DebugUndefined, Environment, Undefined from packaging.version import Version @@ -25,6 +26,9 @@ log = getLogger(__name__) bokeh3 = Version(bokeh.__version__) > Version("3.0") +param2 = Version(param.__version__) > Version("2.0rc1") + +disallow_refs = {'allow_refs': False} if param2 else {} VARIABLE_RE = re.compile(r'\$variables\.([a-zA-Z_]\w*)') diff --git a/lumen/variables/base.py b/lumen/variables/base.py index 98bf1f731..d43bd596d 100644 --- a/lumen/variables/base.py +++ b/lumen/variables/base.py @@ -13,7 +13,7 @@ from ..base import MultiTypeComponent from ..state import state -from ..util import is_ref, resolve_module_reference +from ..util import disallow_refs, is_ref, resolve_module_reference if TYPE_CHECKING: from panel.viewable import Viewable @@ -378,7 +378,8 @@ class Parameter(Variable): `Parameter` variables reflect the current value of a parameter. """ - parameter = param.ClassSelector(class_=param.Parameter, constant=True, doc=""" + parameter = param.ClassSelector(class_=param.Parameter, constant=True, + **disallow_refs, doc=""" A parameter instance whose current value will be reflected on this variable.""") diff --git a/lumen/views/base.py b/lumen/views/base.py index 6a4877028..fff8d51b1 100644 --- a/lumen/views/base.py +++ b/lumen/views/base.py @@ -143,7 +143,7 @@ def __panel__(self) -> Viewable: return pn.panel(pn.bind(lambda e: self.panel, self.param.rerender)) def _update_loading(self, event): - if self._panel is not None: + if hasattr(self._panel, 'loading'): with immediate_dispatch(): self._panel.loading = event.new diff --git a/setup.py b/setup.py index d6c1c5c31..981dac808 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ def get_setup_version(reponame): 'codecov', 'pre-commit', 'matplotlib >=3.4', # Ubuntu + Python 3.9 installs old version matplotlib (3.3.2) + 'pytest-github-actions-annotate-failures', ], 'doc': [ 'nbsite >=0.8.2',