diff --git a/shiny/_app.py b/shiny/_app.py index 0de886a69..835e93a10 100644 --- a/shiny/_app.py +++ b/shiny/_app.py @@ -2,6 +2,8 @@ import copy import os +import shutil +import tempfile import secrets from contextlib import AsyncExitStack, asynccontextmanager from inspect import signature @@ -214,6 +216,25 @@ def __init__( cast("Tag | TagList", ui), lib_prefix=self.lib_prefix ) + def __del__(self): + current_temp_dir = os.path.realpath(tempfile.gettempdir()) + + user_dependencies = [ + v.source["subdir"] + for k, v in self._registered_dependencies.items() + if k.startswith("include-") + ] + + for item in user_dependencies: + # Only remove the item if it exists and it is in the current temp directory + if ( + os.path.exists(item) + and os.path.commonprefix([os.path.realpath(item), current_temp_dir]) + == current_temp_dir + ): + print("Removing dependency: ", item) + shutil.rmtree(item) + def init_starlette_app(self) -> starlette.applications.Starlette: routes: list[starlette.routing.BaseRoute] = [ starlette.routing.WebSocketRoute("/websocket/", self._on_connect_cb), diff --git a/shiny/ui/_include_helpers.py b/shiny/ui/_include_helpers.py index 3c0c9409a..3c7fa923e 100644 --- a/shiny/ui/_include_helpers.py +++ b/shiny/ui/_include_helpers.py @@ -214,25 +214,42 @@ def create_include_dependency( def maybe_copy_files(path: Path | str, include_files: bool) -> tuple[str, str]: hash = get_hash(path, include_files) + print("path: ", path, "Hash: ", hash) - # To avoid unnecessary work when the same file is included multiple times, - # use a directory scoped by a hash of the file. - tmpdir = os.path.join(tempfile.gettempdir(), "shiny_include_files", hash) + tmpdir = os.path.join(tempfile.gettempdir(), f"shiny_include_{hash}") path_dest = os.path.join(tmpdir, os.path.basename(path)) + print("tmpdir: ", tmpdir, "path_dest: ", path_dest) + # To avoid unnecessary work when the same file is included multiple times, + # use a directory scoped by a hash of the file. # Since the hash/tmpdir should represent all the files in the path's directory, - # we can simply return here + # we can check if it exists to determine if we have a cache hit if os.path.exists(path_dest): + print("Path already exists:", path_dest) return path_dest, hash # Otherwise, make sure we have a clean slate if os.path.exists(tmpdir): + print("Folder already exists, but not files, removing.") shutil.rmtree(tmpdir) if include_files: + print( + "Copying all included files from: ", + path, + " with perms: ", + oct(os.stat(path).st_mode), + ) shutil.copytree(os.path.dirname(path), tmpdir) + else: os.makedirs(tmpdir, exist_ok=True) + print( + "Copying files from: ", + path, + " with perms: ", + oct(os.stat(path).st_mode), + ) shutil.copy(path, path_dest) return path_dest, hash @@ -240,11 +257,12 @@ def maybe_copy_files(path: Path | str, include_files: bool) -> tuple[str, str]: def get_hash(path: Path | str, include_files: bool) -> str: if include_files: - key = get_file_key(path) - else: dir = os.path.dirname(path) files = glob.iglob(os.path.join(dir, "**"), recursive=True) key = "\n".join([get_file_key(x) for x in files]) + else: + key = get_file_key(path) + return hash_deterministic(key) diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/app.py b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/app.py new file mode 100644 index 000000000..c8e979769 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/app.py @@ -0,0 +1,27 @@ +from pathlib import Path + +from shiny import App, Inputs, Outputs, Session, ui + +js_file = Path(__file__).parent / "js" / "customjs.js" +css_file = Path(__file__).parent / "css" / "style.css" + +# Define the UI +app_ui = ui.page_fluid( + ui.include_css(css_file, method="link_files"), + ui.include_js(js_file, method="link_files"), + ui.h1("Simple Shiny App with External CSS"), + ui.div( + ui.p("This is a simple Shiny app that demonstrates ui.include_css()"), + ui.p("The styling comes from an external CSS file!"), + class_="content", + ), +) + + +# Define the server +def server(input: Inputs, output: Outputs, session: Session): + pass + + +# Create and run the app +app = App(app_ui, server) diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/css/evenmorecss/more.css b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/css/evenmorecss/more.css new file mode 100644 index 000000000..3eda1891b --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/css/evenmorecss/more.css @@ -0,0 +1,3 @@ +body { + background-color: #c8e1f7; +} \ No newline at end of file diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/css/style.css b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/css/style.css new file mode 100644 index 000000000..99c3ee888 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/css/style.css @@ -0,0 +1,21 @@ +@import url("./evenmorecss/more.css"); + +body { + font-family: Arial, sans-serif; +} + +h1 { + color: black; + border-bottom: 2px solid #4682b4; + padding-bottom: 10px; +} + +.content { + margin: 20px; + padding: 15px; + background-color: white; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + + diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/js/customjs.js b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/js/customjs.js new file mode 100644 index 000000000..4dc8fcc69 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/js/customjs.js @@ -0,0 +1,4 @@ +const newParagraph = document.createElement('p'); +newParagraph.textContent = 'Heyo!'; +document.body.appendChild(newParagraph); + diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/test_css-js-inclusion.py b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/test_css-js-inclusion.py new file mode 100644 index 000000000..b144f78f6 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/include_files_case/test_css-js-inclusion.py @@ -0,0 +1,9 @@ +from playwright.sync_api import Page, expect + +from shiny.run import ShinyAppProc + + +def test_inclusion(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + expect(page.locator("body > p")).to_have_text("Heyo!") diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/app.py b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/app.py new file mode 100644 index 000000000..4072ca609 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/app.py @@ -0,0 +1,27 @@ +from pathlib import Path + +from shiny import App, Inputs, Outputs, Session, ui + +js_file = Path(__file__).parent / "customjs.js" +css_file = Path(__file__).parent / "style.css" + +# Define the UI +app_ui = ui.page_fluid( + ui.include_css(css_file, method="link"), + ui.include_js(js_file, method="link"), + ui.h1("Simple Shiny App with External CSS"), + ui.div( + ui.p("This is a simple Shiny app that demonstrates ui.include_css()"), + ui.p("The styling comes from an external CSS file!"), + class_="content", + ), +) + + +# Define the server +def server(input: Inputs, output: Outputs, session: Session): + pass + + +# Create and run the app +app = App(app_ui, server) diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/customjs.js b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/customjs.js new file mode 100644 index 000000000..c7e8ae251 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/customjs.js @@ -0,0 +1,3 @@ +const newParagraph = document.createElement('p'); +newParagraph.textContent = 'Heyo!'; +document.body.appendChild(newParagraph); diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/style.css b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/style.css new file mode 100644 index 000000000..078238b67 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/style.css @@ -0,0 +1,18 @@ +body { + background-color: #70bfef; + font-family: Arial, sans-serif; +} + +h1 { + color: black; + border-bottom: 2px solid #4682b4; + padding-bottom: 10px; +} + +.content { + margin: 20px; + padding: 15px; + background-color: white; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} diff --git a/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/test_css-js-inclusion.py b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/test_css-js-inclusion.py new file mode 100644 index 000000000..b144f78f6 --- /dev/null +++ b/tests/playwright/shiny/bugs/2061-css-js-inclusion/not_include_files_case/test_css-js-inclusion.py @@ -0,0 +1,9 @@ +from playwright.sync_api import Page, expect + +from shiny.run import ShinyAppProc + + +def test_inclusion(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + expect(page.locator("body > p")).to_have_text("Heyo!")