From 060685f43dbe71ec9b3fac776acc5650abe4d0ca Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Mon, 10 Nov 2025 17:34:41 -0500 Subject: [PATCH 01/15] bugfix: only track mtime as int --- src/html_compose/live/watcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html_compose/live/watcher.py b/src/html_compose/live/watcher.py index 861eb28..0c74b0a 100644 --- a/src/html_compose/live/watcher.py +++ b/src/html_compose/live/watcher.py @@ -415,7 +415,7 @@ def stat_watcher(self): if not stat.S_ISREG(st.st_mode): # Not a regular file, skip it continue - mtime = st.st_mtime + mtime = int(st.st_mtime) except OSError: # Might be deleted, we'll catch it later continue From 6648e643ec3edb2797c903cb9b425c4d817361da Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 08:28:15 -0500 Subject: [PATCH 02/15] fix: path.join with leading slash discards left side --- src/html_compose/resource/util_funcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/html_compose/resource/util_funcs.py b/src/html_compose/resource/util_funcs.py index 6090d88..d3874d0 100644 --- a/src/html_compose/resource/util_funcs.py +++ b/src/html_compose/resource/util_funcs.py @@ -35,6 +35,7 @@ def _cachebust_resource_uri(source: str): "does not exist" ) from exc + source = source.lstrip("/") resource_path = path.join(base_dir, source) now = time() ts = misc_stat_cache.get(path.join(base_dir, source), None) From 5656f13c9b27fbc3036ad8bcdfb4e944ce943574 Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 08:33:02 -0500 Subject: [PATCH 03/15] Invert - correct - ignore glob output --- src/html_compose/live/watcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html_compose/live/watcher.py b/src/html_compose/live/watcher.py index 0c74b0a..b12b129 100644 --- a/src/html_compose/live/watcher.py +++ b/src/html_compose/live/watcher.py @@ -263,7 +263,7 @@ def try_path_hit(self, path: str) -> bool: for pattern in self.ignore_glob: if glob_matcher(pattern, path): - return True + return False for pattern in self.path_glob: if glob_matcher(pattern, path): From dfad8ec5035ef7189afac2cf3591223788a5418a Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:03:38 -0500 Subject: [PATCH 04/15] Make node calllables its own basic type --- src/html_compose/base_types.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/html_compose/base_types.py b/src/html_compose/base_types.py index 2060867..bd91cf7 100644 --- a/src/html_compose/base_types.py +++ b/src/html_compose/base_types.py @@ -65,6 +65,13 @@ def resolve(self, parent=None) -> Iterable[str]: def __html__(self) -> str: return self.render() +# A node resolver is a callable that returns a Node, +# possibly taking the calling element and parent element as arguments. +NodeResolver = ( + Callable[[], "Node"] + | Callable[[ElementBase], "Node"] + | Callable[[ElementBase, ElementBase], "Node"] +) # The Node type is a union of all possible types that can be rendered Node = ( @@ -76,9 +83,7 @@ def __html__(self) -> str: | ElementBase # Base class for all HTML elements | _HasHtml # Returns HTML that does not need escaping | Iterable["Node"] - | Callable[[], "Node"] - | Callable[[ElementBase], "Node"] - | Callable[[ElementBase, ElementBase], "Node"] + | NodeResolver ) # These types are used for attribute values From 673175acd5a2f4486386f8599808ed795085766d Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:06:01 -0500 Subject: [PATCH 05/15] Minor cleanup for cachebust --- src/html_compose/resource/util_funcs.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/html_compose/resource/util_funcs.py b/src/html_compose/resource/util_funcs.py index d3874d0..2086efa 100644 --- a/src/html_compose/resource/util_funcs.py +++ b/src/html_compose/resource/util_funcs.py @@ -20,11 +20,8 @@ def _cachebust_resource_uri(source: str): misc_stat_cache = _State.misc_stat_cache stat_cache = _State.stat_cache - cache_cap = settings.cache_cap - stat_poll_interval = settings.stat_poll_interval base_dir = settings.base_dir - query_string = settings.query_string - base_dir = base_dir + if misc_stat_cache.get(base_dir) is None: try: misc_stat_cache[base_dir] = int(stat(base_dir).st_mtime) @@ -39,7 +36,7 @@ def _cachebust_resource_uri(source: str): resource_path = path.join(base_dir, source) now = time() ts = misc_stat_cache.get(path.join(base_dir, source), None) - update_ts = ts is None or (now - ts) > stat_poll_interval + update_ts = ts is None or (now - ts) > settings.stat_poll_interval if update_ts: try: ts = int(stat(resource_path).st_mtime) @@ -50,7 +47,7 @@ def _cachebust_resource_uri(source: str): "does not exist" ) from exc - if len(stat_cache) >= cache_cap: + if len(stat_cache) >= settings.cache_cap: # Clear if it's too big stat_cache.clear() stat_cache[resource_path] = ts @@ -65,7 +62,7 @@ def _cachebust_resource_uri(source: str): errors="surrogateescape", ) # add our cache buster - pairs.append((query_string, str(int(ts)))) + pairs.append((settings.query_string, str(int(ts)))) # re assemble the query string, try our best to preservees exactly new_qs = urllib.parse.urlencode( pairs, From c00f757b3db4a40547b97435e01bcef1d78d9263 Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:28:19 -0500 Subject: [PATCH 06/15] fix: live server waits for daemon to terminate before restarting This should prevent the browser update from firing mid reload --- src/html_compose/live/live_server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/html_compose/live/live_server.py b/src/html_compose/live/live_server.py index b825ee9..b1105d1 100644 --- a/src/html_compose/live/live_server.py +++ b/src/html_compose/live/live_server.py @@ -123,6 +123,7 @@ def live_server( ) daemon_task = ProcessTask(daemon, delay=0, sync=False) + daemon_stop_task = Task(action=lambda: daemon_task.cancel(), sync=True) # Run livereload server server = run_server(host, port) tr = TaskRunner() @@ -186,6 +187,10 @@ def reload(): if reload_tripped: daemon_task.delay = delay + daemon_delay + + # this should make them fire on the same tick, in order + daemon_stop_task.delay = daemon_task.delay + # This constant should mean the server port is up browser_update_task.delay = ( daemon_task.delay + livereload_delay @@ -194,6 +199,7 @@ def reload(): f"Reloading daemon after {daemon_task.delay} seconds..." ) if any([c.server_reload for c in conds_hit]): + tr.add_task(daemon_stop_task) tr.add_task(daemon_task) tr.add_task(browser_update_task) sleep(loop_delay) From 47e086b146260ee3d47bb5256778fc6d181e020d Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:39:44 -0500 Subject: [PATCH 07/15] This reads maybe less confusing --- src/html_compose/base_element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html_compose/base_element.py b/src/html_compose/base_element.py index c01c2ad..ba40e5a 100644 --- a/src/html_compose/base_element.py +++ b/src/html_compose/base_element.py @@ -13,7 +13,7 @@ class ElementMeta(ABCMeta): """ - The metaclass for all HTML elements to hack the base class interface + The metaclass for all HTML elements """ # We aggressively hack the type checker here From 356c3a57ad561af54dcfc6bee6b9624b9ce420ec Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:39:57 -0500 Subject: [PATCH 08/15] add return type --- src/html_compose/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html_compose/document.py b/src/html_compose/document.py index 6b811a9..cc3c8b1 100644 --- a/src/html_compose/document.py +++ b/src/html_compose/document.py @@ -127,7 +127,7 @@ def document_generator( lang: str | None = None, head: el.head | list | None = None, body: Iterable[Node] | el.body | None = None, -): +) -> str: """ Return a full HTML5 document as a string. From 0c99141260cd259e4f1da5d9f91fff6d5a186ac0 Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:40:11 -0500 Subject: [PATCH 09/15] add types --- src/html_compose/util_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html_compose/util_funcs.py b/src/html_compose/util_funcs.py index 6d9d56e..e69481b 100644 --- a/src/html_compose/util_funcs.py +++ b/src/html_compose/util_funcs.py @@ -86,7 +86,7 @@ def get_livereload_env() -> dict | None: def generate_livereload_env( - host, port, proxy_host: str | None, proxy_uri: str | None = None + host: str, port: int, proxy_host: str | None, proxy_uri: str | None = None ) -> dict: flags = { "port": port, From 7895fecf3c6462a8e714221982aa77986a1fae45 Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:41:19 -0500 Subject: [PATCH 10/15] add return type --- src/html_compose/resource/js_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html_compose/resource/js_import.py b/src/html_compose/resource/js_import.py index 178f012..192e703 100644 --- a/src/html_compose/resource/js_import.py +++ b/src/html_compose/resource/js_import.py @@ -203,7 +203,7 @@ def __init__( "If hash is set, crossorigin must be set to ''/'anonymous'" ) - def uri(self): + def uri(self) -> str: """ Returns the source URI - with cache busting if enabled which is implemented by getting the mtime of the local file From 289a5202e6d4577fe3fa5a36c90e862ddfb181bb Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:41:28 -0500 Subject: [PATCH 11/15] reorder import --- src/html_compose/elements/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/html_compose/elements/__init__.py b/src/html_compose/elements/__init__.py index 7445393..b659107 100644 --- a/src/html_compose/elements/__init__.py +++ b/src/html_compose/elements/__init__.py @@ -102,6 +102,8 @@ def get(value: str) -> BaseAttribute: """ +import os + from .a_element import a as a from .abbr_element import abbr as abbr from .address_element import address as address @@ -216,8 +218,6 @@ def get(value: str) -> BaseAttribute: from .video_element import video as video from .wbr_element import wbr as wbr -import os - # hack: force PDOC to treat elements as submodules if not os.environ.get("PDOC_GENERATING", False): __all__ = [ From 26a19721e349e73da9043e99e23bf948409fb3f0 Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:41:57 -0500 Subject: [PATCH 12/15] Add a test case --- tests/test_util_funcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_util_funcs.py b/tests/test_util_funcs.py index d8ba89c..0691725 100644 --- a/tests/test_util_funcs.py +++ b/tests/test_util_funcs.py @@ -114,6 +114,7 @@ def test_glob_func(): "path/nextsection/dir_cursive/nextsection/end.txt", True, ), + ("front/css/", "front/css/utilities/debug-outline.css", True), ] for pattern, target, expected in test_cases: assert glob_matcher(pattern, target) == expected, ( From 91e8e2a01d940c2f31207dc379bab54c8711a16c Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:43:55 -0500 Subject: [PATCH 13/15] fmt --- src/html_compose/base_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/html_compose/base_types.py b/src/html_compose/base_types.py index bd91cf7..b6a138a 100644 --- a/src/html_compose/base_types.py +++ b/src/html_compose/base_types.py @@ -65,6 +65,7 @@ def resolve(self, parent=None) -> Iterable[str]: def __html__(self) -> str: return self.render() + # A node resolver is a callable that returns a Node, # possibly taking the calling element and parent element as arguments. NodeResolver = ( From 970ebbf3eb0641758748249c1ba993f95464fba0 Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:49:42 -0500 Subject: [PATCH 14/15] add mention of html-convert tool --- src/html_compose/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/html_compose/__init__.py b/src/html_compose/__init__.py index 0fc6d3e..aeab72f 100644 --- a/src/html_compose/__init__.py +++ b/src/html_compose/__init__.py @@ -192,6 +192,7 @@ ```sh html-compose convert {filename or empty for stdin} html-compose convert --noimport el # produces el.div() style references +html-convert # an alias for html-compose convert ``` `html-convert` provides access to this tool as shorthand. From 57b74a0494c09509c026abc98b07904ff0e6694a Mon Sep 17 00:00:00 2001 From: jealouscloud Date: Tue, 11 Nov 2025 20:51:42 -0500 Subject: [PATCH 15/15] Update version/changelog --- changelog.txt | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index fa6fc40..8aba157 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +# 0.11.1 +* livereload fix: live server waits for daemon to terminate before restarting +* livereload fix: Fix bug for ignore_glob not firing +* livereload fix: always track stat mtime as one type (int) + # 0.11.0 * Add html_compose.resource module which includes js_import, css_import, and font_import helpers diff --git a/pyproject.toml b/pyproject.toml index e15db70..d6baee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "html-compose" -version = "0.11.0" +version = "0.11.1" description = "Composable HTML generation in python" authors = [ { name = "jealouscloud", email = "github@noaha.org" }