From 928486cafe64d424f3f84b361556cd071b2ead8c Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 2 Mar 2022 12:16:13 -0600 Subject: [PATCH 1/4] Attach metadata about HTMLDependency()'s in the rendered document --- htmltools/_core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/htmltools/_core.py b/htmltools/_core.py index e334f6e..9f7ca00 100644 --- a/htmltools/_core.py +++ b/htmltools/_core.py @@ -794,6 +794,13 @@ def _hoist_head_content( # end. This matters only if the tag starts out with some children. head.insert(0, Tag("meta", charset="utf-8")) deps = x.get_dependencies() + # Add some metadata about the dependencies so that shiny.js' renderDependency + # logic knows not to re-render them. + head.append(Tag( + "script", + ";".join([d.name + "[" + str(d.version) + "]" for d in deps]), + type="application/html-dependencies", + )) head.extend( [ d.as_html_tags(lib_prefix=lib_prefix, include_version=include_version) From 7e8288438dbe8338c99430ed3bae91681e84fc8c Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 2 Mar 2022 13:08:57 -0600 Subject: [PATCH 2/4] Close #23: make hashing behind head_content() consistent across python sessions --- htmltools/_core.py | 28 ++++++++++++++++++++-------- htmltools/_util.py | 12 +++++++++++- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/htmltools/_core.py b/htmltools/_core.py index 9f7ca00..c65da25 100644 --- a/htmltools/_core.py +++ b/htmltools/_core.py @@ -22,7 +22,13 @@ if sys.version_info >= (3, 8): from typing import TypedDict, SupportsIndex, Protocol, runtime_checkable, Literal else: - from typing_extensions import TypedDict, SupportsIndex, Protocol, runtime_checkable, Literal + from typing_extensions import ( + TypedDict, + SupportsIndex, + Protocol, + runtime_checkable, + Literal, + ) from packaging.version import Version @@ -32,6 +38,7 @@ _package_dir, # type: ignore _html_escape, # type: ignore _flatten, # type: ignore + hash_deterministic, ) __all__ = ( @@ -793,14 +800,19 @@ def _hoist_head_content( # Put at beginning of head, and other hoisted tags at the # end. This matters only if the tag starts out with some children. head.insert(0, Tag("meta", charset="utf-8")) - deps = x.get_dependencies() + # Add some metadata about the dependencies so that shiny.js' renderDependency # logic knows not to re-render them. - head.append(Tag( - "script", - ";".join([d.name + "[" + str(d.version) + "]" for d in deps]), - type="application/html-dependencies", - )) + deps = x.get_dependencies() + if len(deps) > 0: + head.append( + Tag( + "script", + ";".join([d.name + "[" + str(d.version) + "]" for d in deps]), + type="application/html-dependencies", + ) + ) + head.extend( [ d.as_html_tags(lib_prefix=lib_prefix, include_version=include_version) @@ -1139,7 +1151,7 @@ def head_content(*args: TagChildArg) -> HTMLDependency: head = TagList(*args) head_str = head.get_html_string() # Create unique ID to use as name - name = "headcontent_{:x}".format(abs(hash(head_str))) + name = "headcontent_" + hash_deterministic(head_str) return HTMLDependency(name=name, version="0.0", head=head) diff --git a/htmltools/_util.py b/htmltools/_util.py index 1564644..314580b 100644 --- a/htmltools/_util.py +++ b/htmltools/_util.py @@ -1,6 +1,7 @@ +from hashlib import sha256 +import importlib import os import re -import importlib import tempfile from typing import ( Any, @@ -127,6 +128,15 @@ def _package_dir(package: str) -> str: return os.path.dirname(pkg_file) +def hash_deterministic(s: str) -> str: + """ + Returns a deterministic hash of the given string. + """ + h = sha256() + h.update(s.encode("utf-8")) + return h.hexdigest() + + class _HttpServerInfo(NamedTuple): port: int thread: Thread From 07e8c367426d6f797dd5167b44274bffe34ca076 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 2 Mar 2022 13:09:09 -0600 Subject: [PATCH 3/4] Update test snapshots --- tests/snapshots/snap_test_deps.py | 5 +++++ tests/test_deps.py | 7 ++++--- tests/test_html_document.py | 4 ++++ tests/test_jsx_tags.py | 6 ++++-- tests/test_tags.py | 2 ++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/snapshots/snap_test_deps.py b/tests/snapshots/snap_test_deps.py index 6989a20..0ac3ae7 100644 --- a/tests/snapshots/snap_test_deps.py +++ b/tests/snapshots/snap_test_deps.py @@ -11,6 +11,7 @@ + @@ -23,6 +24,7 @@ + @@ -35,6 +37,7 @@ + @@ -49,6 +52,7 @@ + @@ -61,6 +65,7 @@ + diff --git a/tests/test_deps.py b/tests/test_deps.py index a00caf8..79d4c76 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -1,6 +1,3 @@ -import os -from tempfile import TemporaryDirectory -from typing import Union, Optional import textwrap from htmltools import * @@ -32,6 +29,7 @@ def test_dep_resolution(): + @@ -46,6 +44,7 @@ def test_dep_resolution(): + @@ -93,6 +92,7 @@ def test_append_deps(): + @@ -138,6 +138,7 @@ def fake_dep(**kwargs): + diff --git a/tests/test_html_document.py b/tests/test_html_document.py index 249bec1..f6ee34f 100644 --- a/tests/test_html_document.py +++ b/tests/test_html_document.py @@ -30,6 +30,7 @@ def test_html_document_html_input(): Title + abcd @@ -49,6 +50,7 @@ def test_html_document_html_input(): + abcd @@ -68,6 +70,7 @@ def test_html_document_html_input(): + abcd @@ -96,6 +99,7 @@ def test_html_document_head_hoisting(): + diff --git a/tests/test_jsx_tags.py b/tests/test_jsx_tags.py index a148654..bb88ff8 100644 --- a/tests/test_jsx_tags.py +++ b/tests/test_jsx_tags.py @@ -15,6 +15,7 @@ def test_jsx_tags(): + @@ -33,7 +34,7 @@ def test_jsx_tags(): """ - % (react_ver, react_dom_ver) + % (react_ver, react_dom_ver, react_ver, react_dom_ver) ) # Only the "top-level" tag gets wrapped in @@ -64,7 +66,7 @@ def test_jsx_tags(): """ - % (react_ver, react_dom_ver) + % (react_ver, react_dom_ver, react_ver, react_dom_ver) ) x = Foo( diff --git a/tests/test_tags.py b/tests/test_tags.py index 535774d..ed258e0 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -235,6 +235,7 @@ def test_html_save(): + @@ -253,6 +254,7 @@ def test_html_save(): + From f9b8cbbf3ef506beb012c9d58dfa9d2e2db81908 Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 3 Mar 2022 14:20:36 -0600 Subject: [PATCH 4/4] Use SHA1 instead of SHA256; add a note about head_content()'s behavior --- htmltools/_core.py | 8 ++++++++ htmltools/_util.py | 6 ++---- tests/test_html_document.py | 8 ++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/htmltools/_core.py b/htmltools/_core.py index c65da25..ed4adbc 100644 --- a/htmltools/_core.py +++ b/htmltools/_core.py @@ -1132,6 +1132,14 @@ def head_content(*args: TagChildArg) -> HTMLDependency: *args The content to place in the ````. + Note + ---- + If the same content, ``x``, is included in a document multiple times via + ``head_content(x)``, ``x`` will only appear once in the final HTML document's + ````. More often than not, this is desirable behavior, but if you need the + same content included multiple times, you can add some irrelevant/empty tags (e.g., + ``TagList(x, Tag("meta"))``) to make sure ``x`` is included multiple times. + Example ------- >>> from htmltools import * diff --git a/htmltools/_util.py b/htmltools/_util.py index 314580b..1569bd9 100644 --- a/htmltools/_util.py +++ b/htmltools/_util.py @@ -1,4 +1,4 @@ -from hashlib import sha256 +import hashlib import importlib import os import re @@ -132,9 +132,7 @@ def hash_deterministic(s: str) -> str: """ Returns a deterministic hash of the given string. """ - h = sha256() - h.update(s.encode("utf-8")) - return h.hexdigest() + return hashlib.sha1(s.encode('utf-8')).hexdigest() class _HttpServerInfo(NamedTuple): diff --git a/tests/test_html_document.py b/tests/test_html_document.py index f6ee34f..fc58047 100644 --- a/tests/test_html_document.py +++ b/tests/test_html_document.py @@ -30,7 +30,7 @@ def test_html_document_html_input(): Title - + abcd @@ -50,7 +50,7 @@ def test_html_document_html_input(): - + abcd @@ -70,7 +70,7 @@ def test_html_document_html_input(): - + abcd @@ -99,7 +99,7 @@ def test_html_document_head_hoisting(): - +