1<!DOCTYPE html> 2<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head> 3 4<meta charset="utf-8"> 5<meta name="generator" content="quarto-1.10.6"> 6 7<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> 8 9<meta name="author" content="Wasim Lorgat"> 10<meta name="dcterms.date" content="2023-05-22"> 11 12<title>Write, document, test and distribute Python packages with nbdev & Quarto â Wasim Lorgat</title> 13<style> 14/* Default styles provided by pandoc. 15** See https://pandoc.org/MANUAL.html#variables-for-html for config info. 16*/ 17code{white-space: pre-wrap;} 18span.smallcaps{font-variant: small-caps;} 19div.columns{display: flex; gap: min(4vw, 1.5em);} 20div.column{flex: auto; overflow-x: auto;} 21div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} 22ul.task-list{list-style: none;} 23ul.task-list li input[type="checkbox"] { 24 width: 0.8em; 25 margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */ 26 vertical-align: middle; 27} 28</style> 29 30
31<script src="../site_libs/quarto-nav/quarto-nav.js"></script>
vendor: 1 bytes, line 31
31
32<script src="../site_libs/quarto-nav/headroom.min.js"></script>
vendor: 1 bytes, line 32
32
33<script src="../site_libs/clipboard/clipboard.min.js"></script>
vendor: 1 bytes, line 33
33
34<script src="../site_libs/quarto-search/autocomplete.umd.js"></script>
vendor: 1 bytes, line 34
34
35<script src="../site_libs/quarto-search/fuse.min.js"></script>
vendor: 1 bytes, line 35
35
36<script src="../site_libs/quarto-search/quarto-search.js"></script>
36 37<meta name="quarto:offset" content="../"> 38<link href="../favicon.ico" rel="icon">
39<script src="../site_libs/quarto-html/quarto.js" type="module"></script>
vendor: 1 bytes, line 39
39
40<script src="../site_libs/quarto-html/tabsets/tabsets.js" type="module"></script>
vendor: 1 bytes, line 40
40
41<script src="../site_libs/quarto-html/popper.min.js"></script>
vendor: 1 bytes, line 41
41
42<script src="../site_libs/quarto-html/tippy.umd.min.js"></script>
vendor: 1 bytes, line 42
42
43<script src="../site_libs/quarto-html/anchor.min.js"></script>
43 44<link href="../site_libs/quarto-html/tippy.css" rel="stylesheet"> 45<link href="../site_libs/quarto-html/quarto-syntax-highlighting-f75b6103069db49ee0d40b58c3a28e95.css" rel="stylesheet" id="quarto-text-highlighting-styles">
46<script src="../site_libs/bootstrap/bootstrap.min.js"></script>
46 47<link href="../site_libs/bootstrap/bootstrap-icons.css" rel="stylesheet"> 48<link href="../site_libs/bootstrap/bootstrap-e2e1ca73c4bf218057e45371bf87a690.min.css" rel="stylesheet" append-hash="true" id="quarto-bootstrap" data-mode="light">
49<script id="quarto-search-options" type="application/json">{ 50 "location": "navbar", 51 "copy-button": false, 52 "collapse-after": 3, 53 "panel-placement": "end", 54 "type": "overlay", 55 "limit": 50, 56 "keyboard-shortcut": [ 57 "f", 58 "/", 59 "s" 60 ], 61 "show-item-context": false, 62 "language": { 63 "search-no-results-text": "No results", 64 "search-matching-documents-text": "matching documents", 65 "search-copy-link-title": "Copy link to search", 66 "search-hide-matches-text": "Hide additional matches", 67 "search-more-match-text": "more match in this document", 68 "search-more-matches-text": "more matches in this document", 69 "search-clear-button-title": "Clear", 70 "search-text-placeholder": "", 71 "search-detached-cancel-button-title": "Cancel", 72 "search-submit-button-title": "Submit", 73 "search-label": "Search" 74 } 75}</script>
vendor: 1 bytes, line 75
75
76<script defer="" data-api="https://meepo.shop/api/event" data-domain="wasimlorgat.com" src="https://meepo.shop/js/script.outbound-links.js"></script>
76 77 78 79<meta property="og:title" content="Write, document, test and distribute Python packages with nbdev & Quarto"> 80<meta property="og:description" content=""> 81<meta property="og:image" content="https://wasimlorgat.com/posts/images/jupytercon-2023-tutorial.png"> 82<meta property="og:site_name" content="Wasim Lorgat"> 83<meta property="og:image:height" content="900"> 84<meta property="og:image:width" content="1600"> 85</head> 86 87<body class="nav-fixed quarto-light"> 88 89<div id="quarto-search-results"></div> 90 <header id="quarto-header" class="headroom fixed-top"> 91 <nav class="navbar navbar-expand-lg " data-bs-theme="dark"> 92 <div class="navbar-container container-fluid"> 93 <div class="navbar-brand-container mx-auto"> 94 <a class="navbar-brand" href="../index.html"> 95 <span class="navbar-title">Wasim Lorgat</span> 96 </a> 97 </div> 98 <div id="quarto-search" class="" title="Search"></div> 99 <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" role="menu" aria-expanded="false" aria-label="Toggle navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }"> 100 <span class="navbar-toggler-icon"></span> 101</button> 102 <div class="collapse navbar-collapse" id="navbarCollapse"> 103 <ul class="navbar-nav navbar-nav-scroll me-auto"> 104 <li class="nav-item"> 105 <a class="nav-link" href="../index.html"> 106<span class="menu-text">Posts</span></a> 107 </li> 108 <li class="nav-item"> 109 <a class="nav-link" href="../projects.html"> 110<span class="menu-text">Projects</span></a> 111 </li> 112 <li class="nav-item"> 113 <a class="nav-link" href="../tils/index.html"> 114<span class="menu-text">Notes</span></a> 115 </li> 116</ul> 117 <ul class="navbar-nav navbar-nav-scroll ms-auto"> 118 <li class="nav-item compact"> 119 <a class="nav-link" href="https://github.com/seem"> <i class="bi bi-github" role="img"> 120</i> 121<span class="menu-text"></span></a> 122 </li> 123</ul> 124 </div> <!-- /navcollapse --> 125 <div class="quarto-navbar-tools"> 126</div> 127 </div> <!-- /container-fluid --> 128 </nav> 129</header> 130<!-- content --> 131<div id="quarto-content" class="quarto-container page-columns page-rows-contents page-layout-article page-navbar"> 132<!-- sidebar --> 133<!-- margin-sidebar --> 134 <div id="quarto-margin-sidebar" class="sidebar margin-sidebar zindex-bottom"> 135 136 </div> 137<!-- main --> 138<main class="content" id="quarto-document-content"> 139 140<header id="title-block-header" class="quarto-title-block default"> 141<div class="quarto-title"> 142<h1 class="title">Write, document, test and distribute Python packages with nbdev & Quarto</h1> 143 </div> 144 145 146 147<div class="quarto-title-meta"> 148 149 <div> 150 <div class="quarto-title-meta-heading">Author</div> 151 <div class="quarto-title-meta-contents"> 152 <p>Wasim Lorgat </p> 153 </div> 154 </div> 155 156 <div> 157 <div class="quarto-title-meta-heading">Published</div> 158 <div class="quarto-title-meta-contents"> 159 <p class="date">May 22, 2023</p> 160 </div> 161 </div> 162 163 164 </div> 165 166 167 168</header> 169 170 171<p>I attended <a href="https://www.jupytercon.com/">JupyterCon 2023</a> in Paris two weeks ago. On Thursday, <a href="https://hamel.dev/">Hamel Husain</a>, myself, and <a href="https://www.linkedin.com/in/jjallaire">J. J. Allaire</a> (as a teaching assistant) presented a two and a half hour tutorial on Writing, Documenting, Testing, and Distributing Python Packages with <a href="https://nbdev.fast.ai/">nbdev</a> and <a href="https://quarto.org/">Quarto</a>.</p> 172<p>I think it went well! The room was full and attendees engaged with great questions.</p> 173<div class="quarto-figure quarto-figure-center"> 174<figure class="figure"> 175<p><img src="images/jupytercon-2023-tutorial.png" class="shadow-sm border preview-image img-fluid figure-img"></p> 176<figcaption>Screenshot of the <a href="https://fastai.github.io/jupytercon-2023/">tutorial website</a> <a href="../tils/using-shot-scraper.html">taken with <code>shot-scraper</code></a>.</figcaption> 177</figure> 178</div> 179<p>Hamel opened by describing the roots of nbdevâs programming paradigm: a combination of <em>literate programming</em> (<a href="https://en.wikipedia.org/wiki/Literate_programming">introduced by Donald Knuth</a>) and <em>exploratory programming</em> (<a href="https://www.youtube.com/watch?v=PUv66718DII">beautifully demonstrated by Bret Victor</a>) made possible by <a href="https://jupyter.org/">Jupyter</a> notebooks. He then took us through both basic and advanced usage of Quarto â the publishing platform that powers nbdevâs documentation generation.</p> 180<p>I followed with a demonstration of my approach to notebook-driven development by live coding the now classic nbdev example: a library for interacting with playing cards based on <a href="https://greenteapress.com/thinkpython2/html/thinkpython2019.html">Chapter 18 of Allen B. Downeyâs Think Python</a>. I tried to focus on the thinking process and how it differs from more conventional programming paradigms, rather than focusing on the mechanics of how nbdev works since that is so readily available online.</p> 181<p>My demonstration was largely based on the <a href="https://nbdev.fast.ai/tutorials/tutorial.html">
181official written nbdev tutorial</a> and <a href="https://nbdev.fast.ai/tutorials/best_practices.html">best practices post</a>. I also used the tutorial as an opportunity to refine these docs. The biggest refinement was that I now recommend JupyterLab instead of the classic notebook. There are two main reasons for this:</p> 182<ul> 183<li><p>Lab has a growing extension ecosystem:</p> 184<ul> 185<li><p>I canât live without the wonderful <a href="https://github.com/quarto-dev/quarto/tree/main/apps/jupyterlab"><code>jupyterlab-quarto</code></a>. It renders Quarto markdown including frontmatter, callouts, and fenced divs which greatly improves the authoring experience.</p></li> 186<li><p>There is interesting work being done on LLM-related extensions like <a href="https://github.com/jupyterlab/jupyter-ai"><code>jupyter-ai</code></a> which adds an LLM chat interface to the sidebar.</p></li> 187</ul></li> 188<li><p><a href="https://github.com/jupyterlab/jupyterlab/releases/tag/v4.0.0">Lab 4.0.0 was released!</a> It comes with significantly better performance among a ton of other improvements. Most notably for me is a built-in implementation of classicâs <a href="https://jupyter-contrib-nbextensions.readthedocs.io/en/latest/nbextensions/collapsible_headings/readme.html">collapsible headings</a> extension (including the keyboard shortcuts). However, it is worth noting that extensions will need to migrate to version 4 and Iâm keen to see what this process will look like!</p></li> 189</ul> 190<p>You can also find an outline of the tutorial as well as the slides for Hamelâs opening section at the tutorial website: <a href="https://fastai.github.io/jupytercon-2023" class="uri">https://fastai.github.io/jupytercon-2023</a>.</p> 191<p>By the way, we wrote the tutorial site using Quarto too! Here is the underlying repo: <a href="https://github.com/fastai/jupytercon-2023" class="uri">https://github.com/fastai/jupytercon-2023</a>.</p> 192<p>The conference venue was beautiful! On the last day, <a href="https://twitter.com/davidbrochart">David Brochart</a>, <a href="https://hamel.dev/">Hamel Husain</a>, and I took a walk around the <a href="https://www.cite-sciences.fr/en/home">Cité des sciences et de lâindustrie</a> and surrounding area, and David shared some of the fascinating history behind the city.</p> 193<div class="quarto-figure quarto-figure-center"> 194<figure class="figure"> 195<p><img src="images/david-hamel-wasim-jupytercon-2023.jpeg" class="img-fluid figure-img"></p> 196<figcaption><a href="https://twitter.com/davidbrochart">David Brochart</a>, <a href="https://hamel.dev/">Hamel Husain</a>, and myself pictured in front of the <a href="https://philharmoniedeparis.fr/en">Philharmonie de Paris</a> a short walk away from the JupyterCon 2023 conference hall.</figcaption> 197</figure> 198</div> 199 200 201 202</main> <!-- /main -->
203<script> 204 let links = document.querySelectorAll("a[data-analytics]"); 205 for (var i = 0; i < links.length; i++) { 206 links[i].addEventListener('click', handleLinkEvent); 207 links[i].addEventListener('auxclick', handleLinkEvent); 208 } 209 210 function handleLinkEvent(event) { 211 var link = event.target; 212 var middle = event.type == "auxclick" && event.which == 2; 213 var click = event.type == "click"; 214 while (link && (typeof link.tagName == 'undefined' || link.tagName.toLowerCase() != 'a' || !link.href)) { 215 link = link.parentNode; 216 } 217 if (middle || click) { 218 let attributes = link.getAttribute('data-analytics').split(/,(.+)/); 219 let events = [JSON.parse(attributes[0]), JSON.parse(attributes[1] || '{}')]; 220 plausible(...events); 221 } 222 if (!link.target) { 223 if (!(event.ctrlKey || event.metaKey || event.shiftKey) && click) { 224 setTimeout(function () { 225 location.href = link.href; 226 }, 150); 227 event.preventDefault(); 228 } 229 } 230 } 231</script>
vendor: 1 bytes, line 231
231
232<script id="quarto-html-after-body" type="application/javascript"> 233 window.document.addEventListener("DOMContentLoaded", function (event) { 234 const icon = "î§"; 235 const anchorJS = new window.AnchorJS(); 236 anchorJS.options = { 237 placement: 'right', 238 icon: icon 239 }; 240 anchorJS.add('.anchored'); 241 const isCodeAnnotation = (el) => { 242 for (const clz of el.classList) { 243 if (clz.startsWith('code-annotation-')) { 244 return true; 245 } 246 } 247 return false; 248 } 249 const onCopySuccess = function(e) { 250 // button target 251 const button = e.trigger; 252 // don't keep focus 253 button.blur(); 254 // flash "checked" 255 button.classList.add('code-copy-button-checked'); 256 var currentTitle = button.getAttribute("title"); 257 button.setAttribute("title", "Copied!"); 258 let tooltip; 259 if (window.bootstrap) { 260 button.setAttribute("data-bs-toggle", "tooltip"); 261 button.setAttribute("data-bs-placement", "left"); 262 button.setAttribute("data-bs-title", "Copied!"); 263 tooltip = new bootstrap.Tooltip(button, 264 { trigger: "manual", 265 customClass: "code-copy-button-tooltip", 266 offset: [0, -8]}); 267 tooltip.show(); 268 } 269 setTimeout(function() { 270 if (tooltip) { 271 tooltip.hide(); 272 button.removeAttribute("data-bs-title"); 273 button.removeAttribute("data-bs-toggle"); 274 button.removeAttribute("data-bs-placement"); 275 } 276 button.setAttribute("title", currentTitle); 277 button.classList.remove('code-copy-button-checked'); 278 }, 1000); 279 // clear code selection 280 e.clearSelection(); 281 } 282 const getTextToCopy = function(trigger) { 283 const outerScaffold = trigger.parentElement.cloneNode(true); 284 const codeEl = outerScaffold.querySelector('code'); 285 for (const childEl of codeEl.children) { 286 if (isCodeAnnotation(childEl)) { 287 childEl.remove(); 288 } 289 } 290 return codeEl.innerText; 291 } 292 const clipboard = new window.ClipboardJS('.code-copy-button:not([data-in-quarto-modal])', { 293 text: getTextToCopy 294 }); 295 clipboard.on('success', onCopySuccess); 296 if (window.document.getElementById('quarto-embedded-source-code-modal')) { 297 const clipboardModal = new window.ClipboardJS('.code-copy-button[data-in-quarto-modal]', { 298 text: getTextToCopy, 299 container: window.document.getElementById('quarto-embedded-source-code-modal') 300 }); 301 clipboardModal.on('success', onCopySuccess); 302 } 303 var localhostRegex = new RegExp(/^(?:http|https):\/\/localhost\:?[0-9]*\//); 304 var mailtoRegex = new RegExp(/^mailto:/); 305 var filterRegex = new RegExp("https:\/\/wasimlorgat\.com"); 306 var isInternal = (href) => { 307 return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href); 308 } 309 // Inspect non-navigation links and adorn them if external 310 var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)'); 311 for (var i=0; i<links.length; i++) { 312 const link = links[i]; 313 if (!isInternal(link.href)) { 314 // undo the damage that might have been done by quarto-nav.js in the case of 315 // links that we want to consider external 316 if (link.dataset.originalHref !== undefined) { 317 link.href = link.dataset.originalHref; 318 } 319 } 320 } 321 function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { 322 const config = { 323 allowHTML: true, 324 maxWidth: 500, 325 delay: 100, 326 arrow: false, 327 appendTo: function(el) { 328 return el.parentElement; 329 }, 330 interactive: true, 331 interactiveBorder: 10, 332 theme: 'quarto', 333 placement: 'bottom-start', 334 }; 335 if (contentFn) { 336 config.content = contentFn; 337 } 338 if (onTriggerFn) { 339 config.onTrigger = onTriggerFn; 340 } 341 if (onUntriggerFn) { 342 config.onUntrigger = onUntriggerFn; 343 } 344 window.tippy(el, config); 345 } 346 const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); 347 for (var i=0; i<noterefs.length; i++) { 348 const ref = noterefs[i]; 349 tippyHover(ref, function() { 350 // use id or data attribute instead here 351 let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href'); 352 try { href = new URL(href).hash; } catch {} 353 const id = href.replace(/^#\/?/, ""); 354 const note = window.document.getElementById(id); 355 if (note) { 356 return note.innerHTML; 357 } else { 358 return ""; 359 } 360 }); 361 } 362 const xrefs = window.document.querySelectorAll('a.quarto-xref'); 363 const processXRef = (id, note) => { 364 // Strip column container classes 365 const stripColumnClz = (el) => { 366 el.classList.remove("page-full", "page-columns"); 367 if (el.children) { 368 for (const child of el.children) {
369 stripColumnClz(child); 370 } 371 } 372 } 373 stripColumnClz(note) 374 if (id === null || id.startsWith('sec-')) { 375 // Special case sections, only their first couple elements 376 const container = document.createElement("div"); 377 if (note.children && note.children.length > 2) { 378 container.appendChild(note.children[0].cloneNode(true)); 379 for (let i = 1; i < note.children.length; i++) { 380 const child = note.children[i]; 381 if (child.tagName === "P" && child.innerText === "") { 382 continue; 383 } else { 384 container.appendChild(child.cloneNode(true)); 385 break; 386 } 387 } 388 if (window.Quarto?.typesetMath) { 389 window.Quarto.typesetMath(container); 390 } 391 return container.innerHTML 392 } else { 393 if (window.Quarto?.typesetMath) { 394 window.Quarto.typesetMath(note); 395 } 396 return note.innerHTML; 397 } 398 } else { 399 // Remove any anchor links if they are present 400 const anchorLink = note.querySelector('a.anchorjs-link'); 401 if (anchorLink) { 402 anchorLink.remove(); 403 } 404 if (window.Quarto?.typesetMath) { 405 window.Quarto.typesetMath(note); 406 } 407 if (note.classList.contains("callout")) { 408 return note.outerHTML; 409 } else { 410 return note.innerHTML; 411 } 412 } 413 } 414 for (var i=0; i<xrefs.length; i++) { 415 const xref = xrefs[i]; 416 tippyHover(xref, undefined, function(instance) { 417 instance.disable(); 418 let url = xref.getAttribute('href'); 419 let hash = undefined; 420 if (url.startsWith('#')) { 421 hash = url; 422 } else { 423 try { hash = new URL(url).hash; } catch {} 424 } 425 if (hash) { 426 const id = hash.replace(/^#\/?/, ""); 427 const note = window.document.getElementById(id); 428 if (note !== null) { 429 try { 430 const html = processXRef(id, note.cloneNode(true)); 431 instance.setContent(html); 432 } finally { 433 instance.enable(); 434 instance.show(); 435 } 436 } else { 437 // See if we can fetch this 438 fetch(url.split('#')[0]) 439 .then(res => res.text()) 440 .then(html => { 441 const parser = new DOMParser(); 442 const htmlDoc = parser.parseFromString(html, "text/html"); 443 const note = htmlDoc.getElementById(id); 444 if (note !== null) { 445 const html = processXRef(id, note); 446 instance.setContent(html); 447 } 448 }).finally(() => { 449 instance.enable(); 450 instance.show(); 451 }); 452 } 453 } else { 454 // See if we can fetch a full url (with no hash to target) 455 // This is a special case and we should probably do some content thinning / targeting 456 fetch(url) 457 .then(res => res.text()) 458 .then(html => { 459 const parser = new DOMParser(); 460 const htmlDoc = parser.parseFromString(html, "text/html"); 461 const note = htmlDoc.querySelector('main.content'); 462 if (note !== null) { 463 // This should only happen for chapter cross references 464 // (since there is no id in the URL) 465 // remove the first header 466 if (note.children.length > 0 && note.children[0].tagName === "HEADER") { 467 note.children[0].remove(); 468 } 469 const html = processXRef(null, note); 470 instance.setContent(html); 471 } 472 }).finally(() => { 473 instance.enable(); 474 instance.show(); 475 }); 476 } 477 }, function(instance) { 478 }); 479 } 480 let selectedAnnoteEl; 481 const selectorForAnnotation = ( cell, annotation) => { 482 let cellAttr = 'data-code-cell="' + cell + '"'; 483 let lineAttr = 'data-code-annotation="' + annotation + '"'; 484 const selector = 'span[' + cellAttr + '][' + lineAttr + ']'; 485 return selector; 486 } 487 const selectCodeLines = (annoteEl) => { 488 const doc = window.document; 489 const targetCell = annoteEl.getAttribute("data-target-cell"); 490 const targetAnnotation = annoteEl.getAttribute("data-target-annotation"); 491 const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation)); 492 const lines = annoteSpan.getAttribute("data-code-lines").split(","); 493 const lineIds = lines.map((line) => { 494 return targetCell + "-" + line; 495 }) 496 let top = null; 497 let height = null; 498 let parent = null; 499 if (lineIds.length > 0) { 500 //compute the position of the single el (top and bottom and make a div) 501 const el = window.document.getElementById(lineIds[0]); 502 top = el.offsetTop; 503 height = el.offsetHeight; 504 parent = el.parentElement.parentElement; 505 if (lineIds.length > 1) { 506 const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]); 507 const bottom = lastEl.offsetTop + lastEl.offsetHeight; 508 height = bottom - top; 509 } 510 if (top !== null && height !== null && parent !== null) { 511 // cook up a div (if necessary) and position it 512 let div = window.document.getElementById("code-annotation-line-highlight"); 513 if (div === null) { 514 div = window.document.createElement("div"); 515 div.setAttribute("id", "code-annotation-line-highlight"); 516 div.style.position = 'absolute'; 517 parent.appendChild(div); 518 } 519 div.style.top = top - 2 + "px"; 520 div.style.height = height + 4 + "px"; 521 div.style.left = 0; 522 let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); 523 if (gutterDiv === null) { 524 gutterDiv = window.document.createElement("div"); 525 gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter"); 526 gutterDiv.style.position = 'absolute'; 527 const codeCell = window.document.getElementById(targetCell); 528 const gutter = codeCell.querySelector('.code-annotation-gutter'); 529 gutter.appendChild(gutterDiv); 530 } 531 gutterDiv.style.top = top - 2 + "px"; 532 gutterDiv.style.height = height + 4 + "px"; 533 } 534 selectedAnnoteEl = annoteEl; 535 } 536 }; 537 const unselectCodeLines = () => { 538 const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"]; 539 elementsIds.forEach((elId) => { 540 const div = window.document.getElementById(elId); 541 if (div) { 542 div.remove(); 543 } 544 }); 545 selectedAnnoteEl = undefined; 546 }; 547 // Handle positioning of the toggle 548 window.addEventListener( 549 "resize", 550 throttle(() => { 551 elRect = undefined; 552 if (selectedAnnoteEl) { 553 selectCodeLines(selectedAnnoteEl); 554 } 555 }, 10) 556 ); 557 function throttle(fn, ms) { 558 let throttle = false;
559 let timer; 560 return (...args) => { 561 if(!throttle) { // first call gets through 562 fn.apply(this, args); 563 throttle = true; 564 } else { // all the others get throttled 565 if(timer) clearTimeout(timer); // cancel #2 566 timer = setTimeout(() => { 567 fn.apply(this, args); 568 timer = throttle = false; 569 }, ms); 570 } 571 }; 572 } 573 // Attach click handler to the DT 574 const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); 575 for (const annoteDlNode of annoteDls) { 576 annoteDlNode.addEventListener('click', (event) => { 577 const clickedEl = event.target; 578 if (clickedEl !== selectedAnnoteEl) { 579 unselectCodeLines(); 580 const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active'); 581 if (activeEl) { 582 activeEl.classList.remove('code-annotation-active'); 583 } 584 selectCodeLines(clickedEl); 585 clickedEl.classList.add('code-annotation-active'); 586 } else { 587 // Unselect the line 588 unselectCodeLines(); 589 clickedEl.classList.remove('code-annotation-active'); 590 } 591 }); 592 } 593 const findCites = (el) => { 594 const parentEl = el.parentElement; 595 if (parentEl) { 596 const cites = parentEl.dataset.cites; 597 if (cites) { 598 return { 599 el, 600 cites: cites.split(' ') 601 }; 602 } else { 603 return findCites(el.parentElement) 604 } 605 } else { 606 return undefined; 607 } 608 }; 609 var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]'); 610 for (var i=0; i<bibliorefs.length; i++) { 611 const ref = bibliorefs[i]; 612 const citeInfo = findCites(ref); 613 if (citeInfo) { 614 tippyHover(citeInfo.el, function() { 615 var popup = window.document.createElement('div'); 616 citeInfo.cites.forEach(function(cite) { 617 var citeDiv = window.document.createElement('div'); 618 citeDiv.classList.add('hanging-indent'); 619 citeDiv.classList.add('csl-entry'); 620 var biblioDiv = window.document.getElementById('ref-' + cite); 621 if (biblioDiv) { 622 citeDiv.innerHTML = biblioDiv.innerHTML; 623 } 624 popup.appendChild(citeDiv); 625 }); 626 return popup.innerHTML; 627 }); 628 } 629 } 630 }); 631 </script>
631 632<input type="hidden" id="giscus-base-theme" value="light"> 633<input type="hidden" id="giscus-alt-theme" value="dark">
634<script> 635 function loadGiscus() { 636 // Function to get the theme based on body class 637 const getTheme = () => { 638 let baseTheme = document.getElementById('giscus-base-theme').value; 639 let altTheme = document.getElementById('giscus-alt-theme').value; 640 return document.body.classList.contains('quarto-dark') ? altTheme : baseTheme; 641 }; 642 const script = document.createElement("script"); 643 script.src = "https://giscus.app/client.js"; 644 script.async = true; 645 script.dataset.repo = "seem/blog"; 646 script.dataset.repoId = "R_kgDOIBWk3A"; 647 script.dataset.category = "Announcements"; 648 script.dataset.categoryId = "DIC_kwDOIBWk3M4CSSa6"; 649 script.dataset.mapping = "pathname"; 650 script.dataset.reactionsEnabled = "1"; 651 script.dataset.emitMetadata = "0"; 652 script.dataset.inputPosition = "top"; 653 script.dataset.theme = getTheme(); 654 script.dataset.lang = "en"; 655 script.crossOrigin = "anonymous"; 656 // Append the script to the desired div instead of at the end of the body 657 document.getElementById("quarto-content").appendChild(script); 658 } 659 loadGiscus(); 660</script>
660 661</div> <!-- /content --> 662 663 664 665 666<footer class="footer"><div class="nav-footer"><div class="nav-footer-center"><div class="toc-actions"><ul><li><a href="https://github.com/seem/blog/edit/main/posts/jupytercon-2023.qmd" class="toc-action"><i class="bi bi-github"></i>Edit this page</a></li><li><a href="https://github.com/seem/blog/blob/main/posts/jupytercon-2023.qmd" class="toc-action"><i class="bi empty"></i>View source</a></li><li><a href="https://github.com/seem/blog/issues/new" class="toc-action"><i class="bi empty"></i>Report an issue</a></li></ul></div></div></div></footer></body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.