PageSourceSearch

https://wasimlorgat.com/posts/jupytercon-2023.html

html wasimlorgat.com collected 2026-09-28 06:52:45 UTC 29,702 bytes, 666 lines download raw bytes

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 &amp; 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 &amp; 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 &amp; 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.