fix cassette hashing

This commit is contained in:
Ben Burns
2025-02-13 22:56:16 -08:00
parent 2b72fbd5de
commit 2dbdb36743
12 changed files with 269 additions and 29 deletions
+5 -3
View File
@@ -24,14 +24,16 @@ function decompressData(compressedString: string): NockCassetteData {
return msgpack.decode(decompressed) as NockCassetteData;
}
// deno-lint-ignore no-unused-vars
class HashedCassette {
hash: string;
private recording = true;
constructor(
private readonly cassettePath: string,
private readonly hash: string
) {}
hash: string
) {
this.hash = hash;
}
async enter() {
try {
+2 -2
View File
@@ -63,7 +63,7 @@ class HashedCassette:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception as e:
except Exception:
os.remove(self.cassette_path)
else:
existing_hash = cassette_data.get("cassette_hash")
@@ -96,7 +96,7 @@ class HashedCassette:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception as e:
except Exception:
return result
# Update the cassette data with the expected hash.
if cassette_data.get("cassette_hash") != self.hash_value:
+1
View File
@@ -1,3 +1,4 @@
hook_state = {
"document_filename": "__UNKNOWN__",
"document_content": "__UNKNOWN__",
}
+12 -12
View File
@@ -16,7 +16,7 @@ from markdown_exec.hooks import SessionHistoryEntry
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.notebook_convert import convert_notebook
from _scripts.setup_vcr import load_postamble, load_preamble, _hash_string
from _scripts.setup_vcr import get_hash_for_session, load_postamble, load_preamble, _hash_string
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -157,7 +157,12 @@ def handle_vcr_setup(
try:
if hook_state['document_filename'] == '__UNKNOWN__':
raise SuperFencesException(
f"error while processing {language} block: document filename is unknown"
f"error while processing {language} block: document filename hasn't been set yet"
)
if hook_state['document_content'] == '__UNKNOWN__':
raise SuperFencesException(
f"error while processing {language} block: document content hasn't been set yet"
)
if session is None or session == "" and id is None or id == "":
@@ -181,8 +186,9 @@ def handle_vcr_setup(
# Add context manager at start with explicit __enter__ and __exit__ calls
hash_ = get_hash_for_session(language, session, hook_state['document_content'])
wrapped_lines = [
load_preamble(language, code, cassette_name),
load_preamble(language, hash_, cassette_name),
code,
]
@@ -220,24 +226,17 @@ def handle_vcr_teardown(
session: str,
history: list[SessionHistoryEntry],
):
last_inputs = dict(history[-1].inputs)
code = load_postamble(language)
md = last_inputs["md"]
html = False
update_toc = False
path = last_inputs.get("extra", {}).get("path", None)
if path is None:
logger.warning(f"no document filename found while tearing down {session}!")
else:
logger.info(f"tearing down {language} {session} on {path}")
logger.info(f"tearing down {language} {session} on {hook_state['document_filename']}")
kwargs = dict(
code=code,
session=session,
id=f"{id}_vcr_end",
md=md,
md=None, # md is unused by the formatter, but it's a required argument
html=html,
update_toc=update_toc,
extra={},
@@ -344,4 +343,5 @@ def on_post_build(config):
def on_pre_page(page: Page, **kwargs: Dict[str, Any]):
logger.info(f"on_pre_page: {page.file.src_path}")
hook_state['document_filename'] = page.file.src_path
hook_state['document_content'] = page.file.content_string
return page
+67 -4
View File
@@ -1,7 +1,12 @@
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
import hashlib
import json
import os
from textwrap import dedent
import re
from textwrap import dedent, indent
from mistune import BlockParser, BlockState, Markdown, create_markdown
from mistune.renderers.markdown import MarkdownRenderer
preambles = {
"python": "vcr_setup_preamble.py",
@@ -50,21 +55,19 @@ preamble_cleanups = {
}
def load_preamble(language: str, code: str, cassette_name: str) -> str:
def load_preamble(language: str, hash_: str, cassette_name: str) -> str:
"""Load the source code for the preamble for a given language."""
_assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
preamble_path = os.path.join(_assets_dir, preambles[language])
with open(preamble_path, "r") as f:
lines = f.readlines()
hash_ = _hash_string(code)
lines.append(preamble_inits[language](cassette_name, hash_))
return "\n".join(lines).strip()
def load_postamble(language: str) -> str:
"""Load the source code for the postamble for a given language."""
return preamble_cleanups[language]()
@@ -75,3 +78,63 @@ def _hash_string(input_string: str) -> str:
sha256_hash = hashlib.sha256(encoded_string)
# Get the hexadecimal digest of the hash
return sha256_hash.hexdigest()
def extract_code_blocks_for_session(language: str, session: str, content: str) -> str:
code_blocks_for_session = []
TAB_REGEX = r"^===!? \"(?P<title>[^\"]+)\"\n(?P<content>(?:(?P<indent> )+[^\n]*\n)+)"
def parse_tabs(block: BlockParser, m: re.Match, state: BlockState) -> str:
state.append_token(
{
"raw": m.group(0),
"type": "block_tab",
"attrs": {
"title": m.group("title"),
"level": len(m.group("indent")) // 4,
"content": dedent(m.group("content")).strip(),
},
}
)
return m.end()
def render_tabs(self, token: dict, state: BlockState):
recursive_transformer = create_markdown(renderer=DocumentRenderer())
recursive_transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
recursive_transformer.renderer.register("block_tab", render_tabs)
return (
f'=== "{token["attrs"]["title"]}"\n'
f'{indent(recursive_transformer(token["attrs"]["content"]), " " * token["attrs"]["level"])}\n'
)
class DocumentRenderer(MarkdownRenderer):
def block_code(self, token: dict, state: BlockState):
if token["style"] == "fenced":
if token["attrs"]["info"]:
attributes = {}
block_language = token["attrs"]["info"].split()[0]
for match in re.finditer(r'(?P<key>\w+)=(?:(?P<value>(?:[\w]+))|"(?P<value_quoted>(?:[^"\s]+))")', token["attrs"]["info"]):
attributes[match.group("key")] = match.group("value") or match.group("value_quoted")
if block_language == language and "session" in attributes and attributes["session"] == session:
code_blocks_for_session.append(token["raw"].rstrip())
return super().block_code(token, state)
transformer: Markdown = create_markdown(renderer=DocumentRenderer())
transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
transformer.renderer.register("block_tab", render_tabs)
# Parses the page content, which causes the code blocks to be added to the code_blocks_for_session list.
# There's probably some way to do this by using the renderer as a filter, but I would've had to NO-OP
# all of the default behavior, and this was easier.
transformer(content)
return code_blocks_for_session
def get_hash_for_session(language: str, session: str, content: str) -> str:
# include the preamble in the hash so we invalidate if it changes
preamble_hash = _hash_string(load_preamble(language, session, "test"))
code_blocks_for_session = [preamble_hash, *extract_code_blocks_for_session(language, session, content)]
return _hash_string("\n".join(code_blocks_for_session))