diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c2d0bd2f..0c6e32c87b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Added - [#3294](https://github.com/plotly/dash/pull/3294) Added the ability to pass `allow_optional` to Input and State to allow callbacks to work even if these components are not in the dash layout. +- [#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) ## [3.0.4] - 2025-04-24 diff --git a/dash/dash.py b/dash/dash.py index 2b0f9889ad..17a6c93380 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -18,7 +18,7 @@ import base64 import traceback from urllib.parse import urlparse -from typing import Any, Callable, Dict, Optional, Union, Sequence, Literal +from typing import Any, Callable, Dict, Optional, Union, Sequence, Literal, List import flask @@ -262,6 +262,12 @@ class Dash(ObsoleteChecker): 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 @@ -408,6 +414,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, @@ -464,6 +471,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, "" ), @@ -764,7 +772,6 @@ def layout(self, value: Any): 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 @@ -1505,11 +1512,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) @@ -1518,22 +1532,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)) # type: ignore[reportArgumentType] - 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)) # type: ignore[reportArgumentType] + 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%} +