diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3aed45a4..64a2164477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [UNRELEASED] + +## Added + +- [#3077](https://github.com/plotly/dash/pull/3077) Add new parameter `assets_path_ignore` to `dash.Dash()`. Closes [#3076](https://github.com/plotly/dash/issues/3076) + ## [2.18.2] - 2024-11-04 ## Fixed diff --git a/dash/dash.py b/dash/dash.py index 3ad375c823..809ca98538 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -246,6 +246,12 @@ class Dash: to sensitive files. :type assets_ignore: string + :param assets_path_ignore: A list of regex, each regex as a string to pass to ``re.compile``, for + assets path to omit from immediate loading. The files in these ignored paths will still be + served if specifically requested. You cannot use this to prevent access + to sensitive files. + :type assets_path_ignore: list of strings + :param assets_external_path: an absolute URL from which to load assets. Use with ``serve_locally=False``. assets_external_path is joined with assets_url_path to determine the absolute url to the @@ -391,6 +397,7 @@ def __init__( # pylint: disable=too-many-statements use_pages: Optional[bool] = None, assets_url_path: str = "assets", assets_ignore: str = "", + assets_path_ignore: List[str] = None, assets_external_path: Optional[str] = None, eager_loading: bool = False, include_assets_files: bool = True, @@ -451,6 +458,7 @@ def __init__( # pylint: disable=too-many-statements ), # type: ignore assets_url_path=assets_url_path, assets_ignore=assets_ignore, + assets_path_ignore=assets_path_ignore, assets_external_path=get_combined_config( "assets_external_path", assets_external_path, "" ), @@ -730,7 +738,6 @@ def layout(self, value): and not self.validation_layout and not self.config.suppress_callback_exceptions ): - layout_value = self._layout_value() _validate.validate_layout(value, layout_value) self.validation_layout = layout_value @@ -1467,11 +1474,18 @@ def _walk_assets_directory(self): walk_dir = self.config.assets_folder slash_splitter = re.compile(r"[\\/]+") ignore_str = self.config.assets_ignore + ignore_path_list = self.config.assets_path_ignore ignore_filter = re.compile(ignore_str) if ignore_str else None + ignore_path_filters = [ + re.compile(ignore_path) + for ignore_path in (ignore_path_list or []) + if ignore_path + ] for current, _, files in sorted(os.walk(walk_dir)): if current == walk_dir: base = "" + s = "" else: s = current.replace(walk_dir, "").lstrip("\\").lstrip("/") splitted = slash_splitter.split(s) @@ -1480,22 +1494,32 @@ def _walk_assets_directory(self): else: base = splitted[0] - if ignore_filter: - files_gen = (x for x in files if not ignore_filter.search(x)) + # Check if any level of current path matches ignore path + if s and any( + ignore_path_filter.search(x) + for ignore_path_filter in ignore_path_filters + for x in s.split(os.path.sep) + ): + pass else: - files_gen = files + if ignore_filter: + files_gen = (x for x in files if not ignore_filter.search(x)) + else: + files_gen = files - for f in sorted(files_gen): - path = "/".join([base, f]) if base else f + for f in sorted(files_gen): + path = "/".join([base, f]) if base else f - full = os.path.join(current, f) + full = os.path.join(current, f) - if f.endswith("js"): - self.scripts.append_script(self._add_assets_resource(path, full)) - elif f.endswith("css"): - self.css.append_css(self._add_assets_resource(path, full)) - elif f == "favicon.ico": - self._favicon = path + if f.endswith("js"): + self.scripts.append_script( + self._add_assets_resource(path, full) + ) + elif f.endswith("css"): + self.css.append_css(self._add_assets_resource(path, full)) + elif f == "favicon.ico": + self._favicon = path @staticmethod def _invalid_resources_handler(err): diff --git a/tests/integration/dash_assets/test_assets_path_ignore.py b/tests/integration/dash_assets/test_assets_path_ignore.py new file mode 100644 index 0000000000..fac29bd7b6 --- /dev/null +++ b/tests/integration/dash_assets/test_assets_path_ignore.py @@ -0,0 +1,51 @@ +from dash import Dash, html + + +def test_api001_assets_path_ignore(dash_duo): + app = Dash( + __name__, + assets_folder="test_assets_path_ignore_assets", + assets_path_ignore=["should_be_ignored"], + ) + app.index_string = """ + + + {%metas%} + {%title%} + {%css%} + + +
+
+ {%app_entry%} + + + """ + + app.layout = html.Div() + + dash_duo.start_server(app) + + assert ( + dash_duo.find_element("#normal-test-target").value_of_css_property( + "background-color" + ) + == "rgba(255, 0, 0, 1)" + ) + + assert ( + dash_duo.find_element("#ignored-test-target").value_of_css_property( + "background-color" + ) + != "rgba(255, 0, 0, 1)" + ) + + normal_target_content = dash_duo.find_element("#normal-test-target").text + ignored_target_content = dash_duo.find_element("#ignored-test-target").text + + assert normal_target_content == "loaded" + assert ignored_target_content != "loaded" diff --git a/tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.css b/tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.css new file mode 100644 index 0000000000..4e31efc8a8 --- /dev/null +++ b/tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.css @@ -0,0 +1,3 @@ +#normal-test-target { + background-color: rgba(255, 0, 0, 1); +} \ No newline at end of file diff --git a/tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.js b/tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.js new file mode 100644 index 0000000000..ffc037f036 --- /dev/null +++ b/tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.js @@ -0,0 +1,2 @@ +const normalTarget = document.getElementById('normal-test-target'); +normalTarget.innerHTML = 'loaded'; \ No newline at end of file diff --git a/tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.css b/tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.css new file mode 100644 index 0000000000..412aaa9bef --- /dev/null +++ b/tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.css @@ -0,0 +1,3 @@ +#ignored-test-target { + background-color: rgba(255, 0, 0, 1); +} \ No newline at end of file diff --git a/tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.js b/tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.js new file mode 100644 index 0000000000..006c5dce46 --- /dev/null +++ b/tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.js @@ -0,0 +1,2 @@ +const ignoredTarget = document.getElementById('ignored-test-target'); +ignoredTarget.innerHTML = 'loaded'; \ No newline at end of file