diff --git a/.github/workflows/galata.yml b/.github/workflows/galata.yml index fcf28f674dc3..662cab947922 100644 --- a/.github/workflows/galata.yml +++ b/.github/workflows/galata.yml @@ -23,6 +23,8 @@ jobs: - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + with: + python_version: "3.11" - name: Set up browser cache uses: actions/cache@v4 diff --git a/.github/workflows/linuxjs-tests.yml b/.github/workflows/linuxjs-tests.yml index 8fe4712b219f..bc649ec51f6a 100644 --- a/.github/workflows/linuxjs-tests.yml +++ b/.github/workflows/linuxjs-tests.yml @@ -13,10 +13,7 @@ jobs: name: JS strategy: matrix: - # Fix for https://github.com/jupyterlab/jupyterlab/issues/13903 - include: - - group: js-debugger - python-version: '3.11' + python-version: ["3.13"] group: [ js-application, diff --git a/.github/workflows/linuxtests.yml b/.github/workflows/linuxtests.yml index 3d4c346552c0..cb5feb71577a 100644 --- a/.github/workflows/linuxtests.yml +++ b/.github/workflows/linuxtests.yml @@ -15,7 +15,7 @@ jobs: matrix: group: [integrity, integrity2, integrity3, release_test, docs, usage, usage2, splice_source, python, examples, interop, nonode, lint] # This will be used by the base setup action - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.13"] include: - group: examples upload-output: true @@ -23,27 +23,27 @@ jobs: upload-output: true exclude: - group: integrity - python-version: "3.8" + python-version: "3.9" - group: integrity2 - python-version: "3.8" + python-version: "3.9" - group: integrity3 - python-version: "3.8" + python-version: "3.9" - group: release_test - python-version: "3.8" + python-version: "3.9" - group: docs - python-version: "3.8" + python-version: "3.9" - group: usage - python-version: "3.8" + python-version: "3.9" - group: usage2 - python-version: "3.8" + python-version: "3.9" - group: nonode - python-version: "3.8" + python-version: "3.9" - group: lint - python-version: "3.8" + python-version: "3.9" - group: examples - python-version: "3.8" + python-version: "3.9" - group: splice_source - python-version: "3.8" + python-version: "3.9" fail-fast: false timeout-minutes: 45 runs-on: ubuntu-22.04 @@ -88,7 +88,7 @@ jobs: - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 with: - python_version: "3.8" + python_version: "3.9" dependency_type: minimum - name: Install dependencies run: | diff --git a/.github/workflows/macostests.yml b/.github/workflows/macostests.yml index 9e7a38365339..ed5ebdc41178 100644 --- a/.github/workflows/macostests.yml +++ b/.github/workflows/macostests.yml @@ -14,10 +14,10 @@ jobs: strategy: matrix: group: [integrity, python, usage, usage2] - python-version: [3.11] + python-version: [3.12] include: - group: python - python-version: 3.12 + python-version: 3.13 fail-fast: false timeout-minutes: 45 runs-on: macos-latest diff --git a/.github/workflows/windowstests.yml b/.github/workflows/windowstests.yml index e9142e7fa527..d1eb9af9c519 100644 --- a/.github/workflows/windowstests.yml +++ b/.github/workflows/windowstests.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: group: [integrity, python] + python-version: ["3.13"] fail-fast: false runs-on: windows-latest timeout-minutes: 40 diff --git a/docs/source/extension/extension_points.rst b/docs/source/extension/extension_points.rst index eabb9cef579f..fe683ba5cc61 100644 --- a/docs/source/extension/extension_points.rst +++ b/docs/source/extension/extension_points.rst @@ -74,6 +74,7 @@ Here is a sample block of code that adds a command to the application (given by execute: () => { console.log(`Executed ${commandID}`); toggled = !toggled; + } }); This example adds a new command, which, when triggered, calls the ``execute`` function. diff --git a/jupyterlab/extensions/pypi.py b/jupyterlab/extensions/pypi.py index 413023222639..e97f0a6f55ae 100644 --- a/jupyterlab/extensions/pypi.py +++ b/jupyterlab/extensions/pypi.py @@ -65,8 +65,8 @@ def make_connection(self, host): proxy_host, _, proxy_port = http_proxy.netloc.partition(":") proxies = { - "http://": http_proxy_url, - "https://": https_proxy_url, + "http://": httpx.HTTPTransport(proxy=http_proxy_url), + "https://": httpx.HTTPTransport(proxy=https_proxy_url), } xmlrpc_transport_override = ProxiedTransport() @@ -131,7 +131,7 @@ def __init__( parent: Optional[config.Configurable] = None, ) -> None: super().__init__(app_options, ext_options, parent) - self._httpx_client = httpx.AsyncClient(proxies=proxies) + self._httpx_client = httpx.AsyncClient(mounts=proxies) # Set configurable cache size to fetch function self._fetch_package_metadata = partial(_fetch_package_metadata, self._httpx_client) self._observe_package_metadata_cache_size({"new": self.package_metadata_cache_size}) diff --git a/packages/filebrowser/src/model.ts b/packages/filebrowser/src/model.ts index 3d7832fbd7e0..6b3636b6327f 100644 --- a/packages/filebrowser/src/model.ts +++ b/packages/filebrowser/src/model.ts @@ -609,10 +609,15 @@ export class FileBrowserModel implements IDisposable { const path = this._model.path; const { sessions } = this.manager.services; const { oldValue, newValue } = change; + const prefix = this.driveName.length > 0 ? this.driveName + ':' : ''; const value = - oldValue && oldValue.path && PathExt.dirname(oldValue.path) === path + oldValue && + oldValue.path && + prefix + PathExt.dirname(oldValue.path) === path ? oldValue - : newValue && newValue.path && PathExt.dirname(newValue.path) === path + : newValue && + newValue.path && + prefix + PathExt.dirname(newValue.path) === path ? newValue : undefined; diff --git a/packages/filebrowser/test/model.spec.ts b/packages/filebrowser/test/model.spec.ts index 60f8fdd79227..dc3acbb4c5b2 100644 --- a/packages/filebrowser/test/model.spec.ts +++ b/packages/filebrowser/test/model.spec.ts @@ -152,6 +152,28 @@ describe('filebrowser/model', () => { expect(called).toBe(true); }); + it('should be emitted when a file is created in a drive with a name', async () => { + await state.clear(); + const driveName = 'RTC'; + const modelWithName = new FileBrowserModel({ + manager, + state, + driveName + }); + + let called = false; + modelWithName.fileChanged.connect((sender, args) => { + expect(sender).toBe(modelWithName); + expect(args.type).toBe('new'); + expect(args.oldValue).toBeNull(); + expect(args.newValue!.type).toBe('file'); + called = true; + }); + await manager.newUntitled({ type: 'file' }); + expect(called).toBe(true); + modelWithName.dispose(); + }); + it('should be emitted when a file is renamed', async () => { let called = false; model.fileChanged.connect((sender, args) => { diff --git a/packages/notebook/style/base.css b/packages/notebook/style/base.css index 17be4dc7b1bb..c7b237541996 100644 --- a/packages/notebook/style/base.css +++ b/packages/notebook/style/base.css @@ -204,6 +204,7 @@ flex: 0 0 auto; min-width: 36px; color: var(--jp-cell-inprompt-font-color); + opacity: 0.5; padding: var(--jp-code-padding); padding-left: 12px; font-family: var(--jp-cell-prompt-font-family); @@ -221,7 +222,13 @@ top: 8px; left: 8px; background: var(--jp-layout-color2); - border: var(--jp-border-width) solid var(--jp-input-border-color); + border-width: var(--jp-border-width); + border-style: solid; + border-color: color-mix( + in srgb, + var(--jp-input-border-color) 20%, + transparent + ); box-shadow: 2px 2px 4px 0 rgba(0, 0, 0, 0.12); } diff --git a/packages/outputarea/src/model.ts b/packages/outputarea/src/model.ts index a56ff05a68a7..72e1c66f363f 100644 --- a/packages/outputarea/src/model.ts +++ b/packages/outputarea/src/model.ts @@ -358,7 +358,7 @@ export class OutputAreaModel implements IOutputAreaModel { const curText = prev.streamText!; const newText = typeof value.text === 'string' ? value.text : value.text.join(''); - Private.addText(curText, newText); + this._streamIndex = Private.addText(this._streamIndex, curText, newText); return this.length; } @@ -366,7 +366,12 @@ export class OutputAreaModel implements IOutputAreaModel { if (typeof value.text !== 'string') { value.text = value.text.join(''); } - value.text = Private.processText(value.text); + const { text, index } = Private.processText( + this._streamIndex, + value.text + ); + this._streamIndex = index; + value.text = text; } // Create the new item. @@ -480,6 +485,7 @@ export class OutputAreaModel implements IOutputAreaModel { private _changed = new Signal( this ); + private _streamIndex = 0; } /** @@ -530,14 +536,20 @@ namespace Private { /* * Handle backspaces in `newText` and concatenates to `text`, if any. */ - export function processText(newText: string, text?: string): string { + export function processText( + index: number, + newText: string, + text?: string + ): { text: string; index: number } { if (text === undefined) { text = ''; } if (!(newText.includes('\b') || newText.includes('\r'))) { - return text + newText; + text = + text.slice(0, index) + newText + text.slice(index + newText.length); + return { text, index: index + newText.length }; } - let idx0 = text.length; + let idx0 = index; let idx1: number = -1; let lastEnd: number = 0; const regex = /[\n\b\r]/; @@ -587,14 +599,18 @@ namespace Private { throw Error(`This should not happen`); } } - return text; + return { text, index: idx0 }; } /* * Concatenate a string to an observable string, handling backspaces. */ - export function addText(curText: IObservableString, newText: string): void { - const text = processText(newText, curText.text); + export function addText( + prevIndex: number, + curText: IObservableString, + newText: string + ): number { + const { text, index } = processText(prevIndex, newText, curText.text); // Compute the difference between current text and new text. let done = false; let idx = 0; @@ -619,5 +635,6 @@ namespace Private { idx++; } } + return index; } } diff --git a/packages/rendermime/src/renderers.ts b/packages/rendermime/src/renderers.ts index 70bd3489f6d0..65c2a31331b5 100644 --- a/packages/rendermime/src/renderers.ts +++ b/packages/rendermime/src/renderers.ts @@ -787,6 +787,24 @@ function* alignedNodes( * @returns A promise which resolves when rendering is complete. */ export function renderText(options: renderText.IRenderOptions): Promise { + renderTextual(options, { + checkWeb: true, + checkPaths: false + }); + + // Return the rendered promise. + return Promise.resolve(undefined); +} + +/** + * Render the textual representation into a host node. + * + * Implements the shared logic for `renderText` and `renderError`. + */ +function renderTextual( + options: renderText.IRenderOptions, + autoLinkOptions: IAutoLinkOptions +): void { // Unpack the options. const { host, sanitizer, source } = options; @@ -801,16 +819,60 @@ export function renderText(options: renderText.IRenderOptions): Promise { const preTextContent = pre.textContent; + const cacheStoreOptions = []; + if (autoLinkOptions.checkWeb) { + cacheStoreOptions.push('web'); + } + if (autoLinkOptions.checkPaths) { + cacheStoreOptions.push('paths'); + } + const cacheStoreKey = cacheStoreOptions.join('-'); + let cacheStore = Private.autoLinkCache.get(cacheStoreKey); + if (!cacheStore) { + cacheStore = new WeakMap(); + Private.autoLinkCache.set(cacheStoreKey, cacheStore); + } + let ret: HTMLPreElement; if (preTextContent) { // Note: only text nodes and span elements should be present after sanitization in the `
` element.
-    const linkedNodes =
-      sanitizer.getAutolink?.() ?? true
-        ? autolink(preTextContent, {
-            checkWeb: true,
-            checkPaths: false
-          })
-        : [document.createTextNode(content)];
+    let linkedNodes: (HTMLAnchorElement | Text)[];
+    if (sanitizer.getAutolink?.() ?? true) {
+      const cache = getApplicableLinkCache(
+        cacheStore.get(host),
+        preTextContent
+      );
+      if (cache) {
+        const { cachedNodes: fromCache, addedText } = cache;
+        const newAdditions = autolink(addedText, autoLinkOptions);
+        const lastInCache = fromCache[fromCache.length - 1];
+        const firstNewNode = newAdditions[0];
+
+        if (lastInCache instanceof Text && firstNewNode instanceof Text) {
+          const joiningNode = lastInCache;
+          joiningNode.data += firstNewNode.data;
+          linkedNodes = [
+            ...fromCache.slice(0, -1),
+            joiningNode,
+            ...newAdditions.slice(1)
+          ];
+        } else {
+          linkedNodes = [...fromCache, ...newAdditions];
+        }
+      } else {
+        linkedNodes = autolink(preTextContent, autoLinkOptions);
+      }
+      cacheStore.set(host, {
+        preTextContent,
+        // Clone the nodes before storing them in the cache in case if another component
+        // attempts to modify (e.g. dispose of) them - which is the case for search highlights!
+        linkedNodes: linkedNodes.map(
+          node => node.cloneNode(true) as HTMLAnchorElement | Text
+        )
+      });
+    } else {
+      linkedNodes = [document.createTextNode(content)];
+    }
 
     const preNodes = Array.from(pre.childNodes) as (Text | HTMLSpanElement)[];
     ret = mergeNodes(preNodes, linkedNodes);
@@ -819,9 +881,6 @@ export function renderText(options: renderText.IRenderOptions): Promise {
   }
 
   host.appendChild(ret);
-
-  // Return the rendered promise.
-  return Promise.resolve(undefined);
 }
 
 /**
@@ -854,6 +913,68 @@ export namespace renderText {
   }
 }
 
+interface IAutoLinkCacheEntry {
+  preTextContent: string;
+  linkedNodes: (HTMLAnchorElement | Text)[];
+}
+
+/**
+ * Return the information from the cache that can be used given the cache entry and current text.
+ * If the cache is invalid given the current text (or cannot be used) `null` is returned.
+ */
+function getApplicableLinkCache(
+  cachedResult: IAutoLinkCacheEntry | undefined,
+  preTextContent: string
+): {
+  cachedNodes: IAutoLinkCacheEntry['linkedNodes'];
+  addedText: string;
+} | null {
+  if (!cachedResult) {
+    return null;
+  }
+  if (preTextContent.length < cachedResult.preTextContent.length) {
+    // If the new content is shorter than the cached content
+    // we cannot use the cache as we only support appending.
+    return null;
+  }
+  let addedText = preTextContent.substring(cachedResult.preTextContent.length);
+  let cachedNodes = cachedResult.linkedNodes;
+  const lastCachedNode =
+    cachedResult.linkedNodes[cachedResult.linkedNodes.length - 1];
+
+  // Only use cached nodes if:
+  // - the last cached node is a text node
+  // - the new content starts with a new line
+  // - the old content ends with a new line
+  if (
+    cachedResult.preTextContent.endsWith('\n') ||
+    addedText.startsWith('\n')
+  ) {
+    // Second or third condition is met, we can use the cached nodes
+    // (this is a no-op, we just continue execution).
+  } else if (lastCachedNode instanceof Text) {
+    // The first condition is met, we can use the cached nodes,
+    // but first we remove the Text node to re-analyse its text.
+    // This is required when we cached `aaa www.one.com bbb www.`
+    // and the incoming addition is `two.com`. We can still
+    // use text node `aaa ` and anchor node `www.one.com`, but
+    // we need to pass `bbb www.` + `two.com` through linkify again.
+    cachedNodes = cachedNodes.slice(0, -1);
+    addedText = lastCachedNode.textContent + addedText;
+  } else {
+    return null;
+  }
+
+  // Finally check if text has not changed.
+  if (!preTextContent.startsWith(cachedResult.preTextContent)) {
+    return null;
+  }
+  return {
+    cachedNodes,
+    addedText
+  };
+}
+
 /**
  * Render error into a host node.
  *
@@ -865,37 +986,13 @@ export function renderError(
   options: renderError.IRenderOptions
 ): Promise {
   // Unpack the options.
-  const { host, linkHandler, sanitizer, resolver, source } = options;
+  const { host, linkHandler, resolver } = options;
 
-  // Create the HTML content.
-  const content = sanitizer.sanitize(Private.ansiSpan(source), {
-    allowedTags: ['span']
+  renderTextual(options, {
+    checkWeb: true,
+    checkPaths: true
   });
 
-  // Set the sanitized content for the host node.
-  const pre = document.createElement('pre');
-  pre.innerHTML = content;
-
-  const preTextContent = pre.textContent;
-
-  let ret: HTMLPreElement;
-  if (preTextContent) {
-    // Note: only text nodes and span elements should be present after sanitization in the `
` element.
-    const linkedNodes =
-      sanitizer.getAutolink?.() ?? true
-        ? autolink(preTextContent, {
-            checkWeb: true,
-            checkPaths: true
-          })
-        : [document.createTextNode(content)];
-
-    const preNodes = Array.from(pre.childNodes) as (Text | HTMLSpanElement)[];
-    ret = mergeNodes(preNodes, linkedNodes);
-  } else {
-    ret = document.createElement('pre');
-  }
-  host.appendChild(ret);
-
   // Patch the paths if a resolver is available.
   let promise: Promise;
   if (resolver) {
@@ -1012,6 +1109,14 @@ export namespace renderError {
  * The namespace for module implementation details.
  */
 namespace Private {
+  /**
+   * Cache for auto-linking results to provide better performance when streaming outputs.
+   */
+  export const autoLinkCache = new Map<
+    string,
+    WeakMap
+  >();
+
   /**
    * Eval the script tags contained in a host populated by `innerHTML`.
    *
diff --git a/packages/rendermime/test/factories.spec.ts b/packages/rendermime/test/factories.spec.ts
index ffe1e59dd108..d9d3db2463e2 100644
--- a/packages/rendermime/test/factories.spec.ts
+++ b/packages/rendermime/test/factories.spec.ts
@@ -538,6 +538,25 @@ describe('rendermime/factories', () => {
     };
 
     describe('#createRenderer()', () => {
+      // Mock creation of DOM nodes to distinguish cached
+      /// (cloned) nodes from nodes created from scratch.
+      beforeEach(() => {
+        const originalCloneNode = Node.prototype.cloneNode;
+
+        Node.prototype.cloneNode = function (...args: any) {
+          const clonedNode = originalCloneNode.apply(this, args);
+
+          // Annotate as a node created by cloning.
+          clonedNode.wasCloned = true;
+
+          return clonedNode;
+        };
+      });
+
+      afterEach(() => {
+        jest.restoreAllMocks();
+      });
+
       it('should output the correct HTML', async () => {
         const f = errorRendererFactory;
         const mimeType = 'application/vnd.jupyter.stderr';
@@ -570,6 +589,173 @@ describe('rendermime/factories', () => {
         );
       });
 
+      it.each([
+        // Note: timeouts are set to 3.5 times more the local performance to allow for slower runs on CI
+        //
+        // Local benchmarks:
+        // - without linkify cache: 12.5s
+        // - with cache: 1.1s
+        [
+          'when new content arrives line by line',
+          '\n' + 'X'.repeat(5000),
+          1100 * 3.5
+        ],
+        // Local benchmarks:
+        // - without cache: 3.8s
+        // - with cache: 0.8s
+        [
+          'when new content is added to the same line',
+          'test.com ' + 'X'.repeat(2500) + ' www.',
+          800 * 3.5
+        ]
+      ])('should be fast %s', async (_, newContent, timeout) => {
+        let source = '';
+        const mimeType = 'application/vnd.jupyter.stderr';
+
+        const model = createModel(mimeType, source);
+        const w = errorRendererFactory.createRenderer({ mimeType, ...options });
+
+        const start = performance.now();
+        for (let i = 0; i < 25; i++) {
+          source += newContent;
+          model.setData({
+            data: {
+              [mimeType]: source
+            }
+          });
+          await w.renderModel(model);
+        }
+        const end = performance.now();
+
+        expect(end - start).toBeLessThan(timeout);
+      });
+
+      it.each([
+        ['arrives in a new line', 'www.example.com', '\n a new line of text'],
+        ['arrives after a new line', 'www.example.com\n', 'a new line of text'],
+        ['arrives after a text node', 'www.example.com next line', ' of text'],
+        ['arrives after a text node', 'www.example.com\nnext line', ' of text']
+      ])(
+        'should use cached links if new content %s',
+        async (_, oldSource, addition) => {
+          const mimeType = 'application/vnd.jupyter.stderr';
+          let source = oldSource;
+          const model = createModel(mimeType, source);
+          const w = errorRendererFactory.createRenderer({
+            mimeType,
+            ...defaultOptions
+          });
+          // Perform an initial render to populate the cache.
+          await w.renderModel(model);
+          const before = w.node.innerHTML;
+          const cachedLink = w.node.querySelector('a');
+          expect(cachedLink).toBe(w.node.childNodes[0].childNodes[0]);
+
+          // Update the source.
+          source += addition;
+          model.setData({
+            data: {
+              [mimeType]: source
+            }
+          });
+
+          // Perform a second render which should use the cache.
+          await w.renderModel(model);
+          const after = w.node.innerHTML;
+          const linkAfter = w.node.querySelector('a');
+
+          // The contents of the node should be updated with the new line.
+          expect(before).not.toEqual(after);
+          expect(after).toContain('line of text');
+
+          expect(cachedLink).not.toBe(null);
+          expect(cachedLink).not.toHaveProperty('wasCloned');
+
+          // If cached links were reused those would be cloned
+          expect(linkAfter).not.toBe(null);
+          expect(linkAfter).toEqual(cachedLink);
+          expect(linkAfter).toHaveProperty('wasCloned', true);
+        }
+      );
+
+      it('should not use cached links if the new content appends to the link', async () => {
+        const mimeType = 'application/vnd.jupyter.stderr';
+        let source = 'www.example.co';
+        const model = createModel(mimeType, source);
+        const w = errorRendererFactory.createRenderer({
+          mimeType,
+          ...defaultOptions
+        });
+        // Perform an initial render to populate the cache.
+        await w.renderModel(model);
+        const before = w.node.innerHTML;
+        const cachedLink = w.node.querySelector('a');
+
+        // Update the source.
+        source += 'm';
+        model.setData({
+          data: {
+            [mimeType]: source
+          }
+        });
+
+        // Perform a second render.
+        await w.renderModel(model);
+        const after = w.node.innerHTML;
+        const linkAfter = w.node.querySelector('a');
+
+        // The contents of the node should be updated with the new line.
+        expect(before).not.toEqual(after);
+
+        expect(cachedLink).not.toBe(null);
+        expect(cachedLink!.textContent).toEqual('www.example.co');
+        expect(cachedLink).not.toHaveProperty('wasCloned');
+
+        // If cached links were reused those would be cloned
+        expect(linkAfter).not.toBe(null);
+        expect(linkAfter!.textContent).toEqual('www.example.com');
+        expect(linkAfter).not.toHaveProperty('wasCloned');
+      });
+
+      it('should use partial cache if a link is created by addition of a new fragment', async () => {
+        const mimeType = 'application/vnd.jupyter.stderr';
+        let source = 'aaa www.one.com bbb www.';
+        const model = createModel(mimeType, source);
+        const w = errorRendererFactory.createRenderer({
+          mimeType,
+          ...defaultOptions
+        });
+        // Perform an initial render to populate the cache.
+        await w.renderModel(model);
+        const cachedTextNode = w.node.childNodes[0].childNodes[0];
+        const linksBefore = w.node.querySelectorAll('a');
+        expect(linksBefore).toHaveLength(1);
+
+        // Update the source.
+        source += 'two.com';
+        model.setData({
+          data: {
+            [mimeType]: source
+          }
+        });
+
+        // Perform a second render.
+        await w.renderModel(model);
+        const textNodeAfter = w.node.childNodes[0].childNodes[0];
+        const linksAfter = w.node.querySelectorAll('a');
+
+        // It should not use the second text node (`bbb www.`) from cache and instead
+        // it should fragment properly linkify the second link
+        expect(linksAfter).toHaveLength(2);
+
+        expect(cachedTextNode).toBeInstanceOf(Text);
+        expect(cachedTextNode).not.toHaveProperty('wasCloned');
+
+        // If cached nodes were reused those would be cloned
+        expect(textNodeAfter).toEqual(cachedTextNode);
+        expect(textNodeAfter).toHaveProperty('wasCloned', true);
+      });
+
       it('should autolink a single known file path', async () => {
         const f = errorRendererFactory;
         const urls = [
@@ -642,10 +828,10 @@ describe('rendermime/factories', () => {
         const source = 'www.example.com';
         const expected =
           '
www.example.com
'; - const f = textRendererFactory; + const f = errorRendererFactory; const mimeType = 'application/vnd.jupyter.stderr'; const model = createModel(mimeType, source); - const w = f.createRenderer({ mimeType, ...options }); + const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toBe(expected); }); diff --git a/packages/ui-components/src/components/windowedlist.ts b/packages/ui-components/src/components/windowedlist.ts index fd059bb8992e..782cb6ba3f86 100644 --- a/packages/ui-components/src/components/windowedlist.ts +++ b/packages/ui-components/src/components/windowedlist.ts @@ -1315,6 +1315,7 @@ export class WindowedList< private _applyNoWindowingStyles() { this._viewport.style.position = 'relative'; this._viewport.style.top = '0px'; + this._viewport.style.minHeight = ''; this._innerElement.style.height = ''; } /** diff --git a/pyproject.toml b/pyproject.toml index 4f96a1f6bb75..33c89595858f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ classifiers = [ ] dependencies = [ "async_lru>=1.0.0", - "httpx>=0.25.0", + "httpx~=0.28.0", "importlib-metadata>=4.8.3;python_version<\"3.10\"", "importlib-resources>=1.4;python_version<\"3.9\"", "ipykernel>=6.5.0", @@ -47,7 +47,7 @@ dependencies = [ "jupyterlab_server>=2.27.1,<3", "notebook_shim>=0.2", "packaging", - "setuptools>=40.1.0", + "setuptools>=41.1.0", "tomli>=1.2.2;python_version<\"3.11\"", "tornado>=6.2.0", "traitlets",