From 079f79b4e045c351edad2a35fa0e777cfd3de5a9 Mon Sep 17 00:00:00 2001 From: CNFeffery Date: Fri, 15 Nov 2024 13:34:15 +0800 Subject: [PATCH 1/6] feat: add new parameter assets_path_ignore for dash.Dash() --- dash/dash.py | 56 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 3ad375c823..8066fe4a66 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 @@ -1348,9 +1355,7 @@ def dispatch(self): outputs_grouping = map_grouping( lambda ind: flat_outputs[ind], outputs_indices ) - g.outputs_grouping = ( - outputs_grouping # pylint: disable=assigning-non-slot - ) + g.outputs_grouping = outputs_grouping # pylint: disable=assigning-non-slot g.using_outputs_grouping = ( # pylint: disable=assigning-non-slot not isinstance(outputs_indices, int) and outputs_indices != list(range(grouping_len(outputs_indices))) @@ -1467,11 +1472,16 @@ 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 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 +1490,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): @@ -2254,9 +2274,7 @@ def update(pathname_, search_, **states): ] + [ # pylint: disable=not-callable - self.layout() - if callable(self.layout) - else self.layout + self.layout() if callable(self.layout) else self.layout ] ) if _ID_CONTENT not in self.validation_layout: From 080131692d1d6fa68cd580642e7925989c3583a9 Mon Sep 17 00:00:00 2001 From: CNFeffery Date: Fri, 15 Nov 2024 15:05:51 +0800 Subject: [PATCH 2/6] update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3aed45a4..878c4d256f 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 + +- [#2994](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 From a731de35a968d5e4d4941956638d64f6237260f9 Mon Sep 17 00:00:00 2001 From: CNFeffery Date: Fri, 15 Nov 2024 15:06:41 +0800 Subject: [PATCH 3/6] alter CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 878c4d256f..64a2164477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Added -- [#2994](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) +- [#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 From 66e0f627f7db985487431df1db51f5a780794c44 Mon Sep 17 00:00:00 2001 From: CNFeffery Date: Fri, 15 Nov 2024 15:52:51 +0800 Subject: [PATCH 4/6] handle ignore_path_list default None --- dash/dash.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 8066fe4a66..59cf178c04 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1475,7 +1475,9 @@ def _walk_assets_directory(self): 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 if ignore_path + re.compile(ignore_path) + for ignore_path in (ignore_path_list or []) + if ignore_path ] for current, _, files in sorted(os.walk(walk_dir)): From d9630a9f248fb9ba15bd3856ea241a3dd81029e5 Mon Sep 17 00:00:00 2001 From: CNFeffery Date: Fri, 15 Nov 2024 15:57:25 +0800 Subject: [PATCH 5/6] add test for #3077 --- .../dash_assets/test_assets_path_ignore.py | 51 +++++++++++++++++++ .../normal_files/normal.css | 3 ++ .../normal_files/normal.js | 2 + .../should_be_ignored/ignored.css | 3 ++ .../should_be_ignored/ignored.js | 2 + 5 files changed, 61 insertions(+) create mode 100644 tests/integration/dash_assets/test_assets_path_ignore.py create mode 100644 tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.css create mode 100644 tests/integration/dash_assets/test_assets_path_ignore_assets/normal_files/normal.js create mode 100644 tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.css create mode 100644 tests/integration/dash_assets/test_assets_path_ignore_assets/should_be_ignored/ignored.js 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%} +
+ {%config%} + {%scripts%} + {%renderer%} +
+ + """ + + 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 From c025f1b6cff3c671e31b448e31db082c8e7e5e18 Mon Sep 17 00:00:00 2001 From: CNFeffery Date: Sun, 17 Nov 2024 22:10:54 +0800 Subject: [PATCH 6/6] fix lint --- dash/dash.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 59cf178c04..809ca98538 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1355,7 +1355,9 @@ def dispatch(self): outputs_grouping = map_grouping( lambda ind: flat_outputs[ind], outputs_indices ) - g.outputs_grouping = outputs_grouping # pylint: disable=assigning-non-slot + g.outputs_grouping = ( + outputs_grouping # pylint: disable=assigning-non-slot + ) g.using_outputs_grouping = ( # pylint: disable=assigning-non-slot not isinstance(outputs_indices, int) and outputs_indices != list(range(grouping_len(outputs_indices))) @@ -2276,7 +2278,9 @@ def update(pathname_, search_, **states): ] + [ # pylint: disable=not-callable - self.layout() if callable(self.layout) else self.layout + self.layout() + if callable(self.layout) + else self.layout ] ) if _ID_CONTENT not in self.validation_layout: