# 2026.03.24 Weekly Meeting

**URL:** <https://discourse.slicer.org/t/2026-03-24-weekly-meeting/46543>\
**Category:** Weekly meetings\
**Created:** [March 24, 2026, 2:03am UTC](https://discourse.slicer.org/t/2026-03-24-weekly-meeting/46543 "2026-03-24T02:03:28Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![Sam\_Horvath](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.slicer.org/sam_horvath/32/3092_2.png) [@Sam\_Horvath](https://discourse.slicer.org/u/Sam_Horvath)\
**Post date:** [March 24, 2026, 2:03am UTC](https://discourse.slicer.org/t/2026-03-24-weekly-meeting/46543/1 "2026-03-24T02:03:28Z")

</div>

Tomorrow, we will be having our next weekly hangout at **10:00 AM ET until 11:00 AM ET.**

Anyone is welcome to join at this link: [https://bit.ly/slicer-googlemeet-hosted-by-kitware](https://bit.ly/slicer-googlemeet-hosted-by-kitware)

* * *

> **Weekly Meeting**
>
> **Starts:** March 24, 2026, 10:00am (America/New\_York)\
> **Link:** <https://bit.ly/slicer-googlemeet-hosted-by-kitware>

**Agenda:**

Please post to this thread to put a topic on the agenda! We will try to prioritize agenda items during the meeting.

* * *

Thanks  
Sam and Ebrahim

---

<div class="post-metadata">

**Author:** ![amyers](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.slicer.org/amyers/32/82504_2.png) [@amyers](https://discourse.slicer.org/u/amyers)\
**Post date:** [March 24, 2026, 12:33pm UTC](https://discourse.slicer.org/t/2026-03-24-weekly-meeting/46543/2 "2026-03-24T12:33:08Z")

</div>

Topics:

1. Back-end processing logic for segmentation
2. Running multiple 3D Slicer instances

---

<div class="post-metadata">

**Author:** ![ebrahim](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.slicer.org/ebrahim/32/13403_2.png) [@ebrahim](https://discourse.slicer.org/u/ebrahim)\
**Post date:** [March 24, 2026, 1:47pm UTC](https://discourse.slicer.org/t/2026-03-24-weekly-meeting/46543/3 "2026-03-24T13:47:20Z")

</div>

Topics:

1. python packaging improvements discussion at Slicer#9010
2. telemetry questions

---

<div class="post-metadata">

**Author:** ![ebrahim](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.slicer.org/ebrahim/32/13403_2.png) [@ebrahim](https://discourse.slicer.org/u/ebrahim)\
**Post date:** [March 24, 2026, 3:48pm UTC](https://discourse.slicer.org/t/2026-03-24-weekly-meeting/46543/4 "2026-03-24T15:48:32Z")

</div>

Notes from the meeting today:

## Running multiple 3D Slicer instances (Alex)

Starting 3d slicer as an external process, on thinks of QProcess… is running multiple 3D slicer instances simultaneously a problem?

- No it should be fine. They can co-exist. But communicating between them is a problem you’d need to solve if you need that. Recommend WebServer or OpenIGT.

## python packaging improvements (Ebrahim)

> <https://github.com/Slicer/Slicer/pull/9010>
>
> \# Python Dependency Handling Improvements
> 
> \## Summary
> 
> This PR introduces a …new \`slicer.pydeps\` module for managing Python dependencies in Slicer extensions. The goal is to provide a standardized, user-friendly way to check, prompt for, and install Python packages. The existing \`slicer.util.pip\_install\` and \`slicer.util.pip\_uninstall\` functions are retained as backward-compatible wrappers that delegate to \`slicer.pydeps\`.
> 
> \*\*New/updated functions:\*\*
> 
> | Function | Purpose |
> |----------|---------|
> | \`slicer.pydeps.load\_requirements(path)\` | Load a \`requirements.txt\` file into \`Requirement\` objects |
> | \`slicer.pydeps.load\_pyproject\_dependencies(path)\` | Load \`\[project.dependencies\]\` from a \`pyproject.toml\` file into \`Requirement\` objects |
> | \`slicer.pydeps.pip\_check(req)\` | Check if requirements are satisfied (pure Python, no subprocess) |
> | \`slicer.pydeps.pip\_install(...)\` | Canonical home; extended with modal progress dialog, non-blocking mode, status bar feedback, \`--no-deps\` support, and \`skip\_packages\` for selective dependency installation. Also available as \`slicer.util.pip\_install\` (backward-compatible wrapper) |
> | \`slicer.pydeps.pip\_ensure(reqs, requester="...")\` | High-level: checks, prompts, installs with progress, and offers restart if updated packages were already imported |
> 
> All install functions support optional \`constraints\` (constraints file), \`no\_deps\_requirements\` (packages to install with \`--no-deps\`), and \`skip\_packages\` (packages to exclude from the transitive dependency tree) parameters.
> 
> \*\*Typical usage in an extension:\*\*
> 
> \`\`\`python
> import slicer.pydeps
> 
> reqs = slicer.pydeps.load\_requirements(self.resourcePath("requirements.txt"))
> slicer.pydeps.pip\_ensure(reqs, requester="MyExtension")
> import my\_dependency # Now safe
> \`\`\`
> 
> Or, with \`pyproject.toml\`:
> 
> \`\`\`python
> import slicer.pydeps
> 
> reqs = slicer.pydeps.load\_pyproject\_dependencies(self.resourcePath("pyproject.toml"))
> slicer.pydeps.pip\_ensure(reqs, requester="MyExtension")
> import my\_dependency # Now safe
> \`\`\`
> 
> With \`skip\_packages\` (for extensions that need to exclude certain transitive dependencies):
> 
> \`\`\`python
> import slicer.pydeps
> 
> reqs = \[Requirement("nnunetv2\>=2.3")\]
> skipped = slicer.pydeps.pip\_ensure(
> reqs,
> skip\_packages=\["SimpleITK", "torch", "requests"\],
> requester="SlicerNNUNet",
> )
> \# skipped contains the requirement strings that were excluded
> \`\`\`
> 
> \*\*Behavior of \`pip\_install\`:\*\*
> 
> Four operating modes:
> 
> \- \`show\_progress=True, blocking=True\` : Modal progress dialog (new default behavior)
> \- \`show\_progress=True, blocking=False\`: Status bar messages
> \- \`show\_progress=False, blocking=True\`: Busy cursor only (the previous default behavior, except with a busy cursor now added)
> \- \`show\_progress=False, blocking=False\`: No visual indication that anything is happening (without looking at python console); specify callbacks to create more reasonable custom behaviors.
> 
> \## Changes
> 
> \- \*\*Base/Python/slicer/pydeps.py\*\* — New module: \`load\_requirements\`, \`load\_pyproject\_dependencies\`, \`pip\_check\`, \`pip\_ensure\`, \`pip\_install\`, \`pip\_uninstall\`, and non-blocking infrastructure
> \- \*\*Base/Python/slicer/util.py\*\* — Backward-compatible wrappers (\`pip\_install\`, \`pip\_uninstall\`) that delegate to \`slicer.pydeps\`
> \- \*\*Base/Python/slicer/tests/test\_slicer\_pydeps.py\*\* — Unit tests
> \- \*\*Docs/\*\* — Updated \`python\_faq.md\` and \`script\_repository/gui.md\`
> 
> \---
> 
> \<details\>
> \<summary\>\<strong\>Testing Snippets (click to expand)\</strong\>\</summary\>
> 
> Convenient snippets to quickly try things. First, run this once in the Python console:
> 
> \`\`\`python
> import slicer.pydeps
> \`\`\`
> 
> \### load\_requirements
> 
> \`\`\`python
> import tempfile, os
> with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
> f.write("numpy\>=1.20\\npandas\>=2.0\\nscipy\\n")
> path = f.name
> reqs = slicer.pydeps.load\_requirements(path)
> print(\[f"{r.name}: {r.specifier}" for r in reqs\])
> os.unlink(path)
> \`\`\`
> 
> \### load\_pyproject\_dependencies
> 
> \`\`\`python
> import tempfile, os
> with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
> f.write("\[project\]\\ndependencies = \[\\n")
> f.write(' "numpy\>=1.20",\\n "pandas\>=2.0",\\n "scipy",\\n\]\\n')
> path = f.name
> reqs = slicer.pydeps.load\_pyproject\_dependencies(path)
> print(\[f"{r.name}: {r.specifier}" for r in reqs\])
> os.unlink(path)
> \`\`\`
> 
> \### pip\_check
> 
> \`\`\`python
> from packaging.requirements import Requirement
> 
> \# Check bundled package
> slicer.pydeps.pip\_check(Requirement("numpy\>=1.0")) # True
> 
> \# Unsatisfiable version
> slicer.pydeps.pip\_check(Requirement("numpy\>=99999.0")) # False
> 
> \# Package not installed
> slicer.pydeps.pip\_check(Requirement("nonexistent-package-xyz")) # False
> 
> \# Marker that doesn't apply (always satisfied)
> slicer.pydeps.pip\_check(Requirement("foo; sys\_platform == 'nonexistent'")) # True
> \`\`\`
> 
> \### pip\_install with progress dialog (default)
> 
> \`\`\`python
> \# Default: shows modal progress dialog, blocks until complete
> \# (If already installed, completes quickly with "already satisfied")
> slicer.util.pip\_install("scikit-image", requester="ReviewTest")
> \`\`\`
> 
> To see the full installation flow, uninstall first:
> \`\`\`python
> slicer.util.pip\_uninstall("scikit-image")
> slicer.util.pip\_install("scikit-image", requester="ReviewTest")
> \`\`\`
> 
> If it's still too fast, try putting "torch" as the package :)
> 
> \### pip\_install without progress dialog
> 
> \`\`\`python
> \# Busy cursor only, no dialog
> slicer.util.pip\_install("scikit-image", show\_progress=False)
> \`\`\`
> 
> \### Non-blocking pip\_install with status bar messages
> 
> \`\`\`python
> slicer.util.pip\_install("scikit-image", blocking=False, requester="ReviewTest")
> \`\`\`
> 
> \### Non-blocking pip\_install with custom callbacks
> 
> \`\`\`python
> def onLog(line):
> print(f"\[pip\] {line}")
> 
> def onComplete(code):
> print(f"Done! Return code: {code}")
> 
> \# Using --help to see callbacks firing with lots of output
> slicer.util.pip\_install("--help", blocking=False, show\_progress=False, logCallback=onLog, completedCallback=onComplete)
> \`\`\`
> 
> \### Check if pip install is in progress
> 
> \`\`\`python
> slicer.util.pip\_install("scikit-image", blocking=False, requester="ReviewTest")
> print(slicer.pydeps.isPipInstallInProgress()) # probably True
> \`\`\`
> 
> versus just this:
> 
> \`\`\`python
> print(slicer.pydeps.isPipInstallInProgress()) # False
> \`\`\`
> 
> \### pip\_ensure
> 
> \`\`\`python
> from packaging.requirements import Requirement
> 
> \# With prompt dialog
> reqs = \[Requirement("charset-normalizer\>=3.0")\]
> slicer.pydeps.pip\_ensure(reqs, requester="ReviewTest") # Does nothing if already installed, try uninstalling first to see it do an install: slicer.util.pip\_uninstall("charset-normalizer")
> 
> 
> \# Without install prompt (doesn't ask for confirmation whether you want to install)
> slicer.pydeps.pip\_ensure(reqs, prompt\_install=False, requester="ReviewTest") # Does nothing if already installed, try uninstalling first to see it do an install: slicer.util.pip\_uninstall("charset-normalizer")
> \`\`\`
> 
> \### pip\_ensure restart prompt
> 
> After installation, \`pip\_ensure\` checks if any updated packages were already imported in the current session. If so, it shows a "Restart Recommended" dialog with details (old → new versions). The user can restart immediately or continue.
> 
> \`\`\`python
> from packaging.requirements import Requirement
> import numpy # Ensure numpy is in sys.modules
> 
> \# Reinstall numpy (already imported) — should trigger restart prompt
> slicer.util.pip\_uninstall("numpy")
> reqs = \[Requirement("numpy\>=1.0")\]
> slicer.pydeps.pip\_ensure(reqs, requester="ReviewTest")
> \# A "Restart Recommended" dialog should appear because numpy was already imported
> \`\`\`
> 
> To disable the restart prompt: \`slicer.pydeps.pip\_ensure(reqs, prompt\_restart=False)\`
> 
> \### With constraints file
> 
> \`\`\`python
> import tempfile, os
> 
> \# Create a constraints file that pins charset-normalizer
> with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
> f.write("charset-normalizer==3.3.2\\n")
> constraints\_path = f.name
> 
> \# Install requests (which depends on charset-normalizer) with the constraint
> slicer.util.pip\_install("requests", constraints=constraints\_path)
> os.unlink(constraints\_path)
> \`\`\`
> 
> \### With skip\_packages (selective dependency installation)
> 
> \`\`\`python
> \# Install scikit-image but skip imageio (one of its dependencies)
> skipped = slicer.util.pip\_install(
> "scikit-image",
> skip\_packages=\["imageio"\],
> requester="ReviewTest",
> )
> print(f"Skipped: {skipped}")
> 
> \# Or via pip\_ensure:
> from packaging.requirements import Requirement
> reqs = \[Requirement("scikit-image\>=0.20")\]
> skipped = slicer.pydeps.pip\_ensure(
> reqs,
> skip\_packages=\["imageio"\],
> requester="ReviewTest",
> )
> \`\`\`
> 
> \### PythonSlicer (command-line) usage
> 
> Run this from a terminal using Slicer's PythonSlicer executable (not the Slicer Python console):
> 
> \`\`\`bash
> /path/to/PythonSlicer -c "import slicer.pydeps; slicer.pydeps.pip\_install('charset-normalizer')"
> \`\`\`
> 
> This verifies that \`pip\_install\` works in the PythonSlicer environment where \`slicer.app\` and Qt are not available. The function automatically falls back to simple blocking mode.
> 
> \</details\>
> 
> \---
> 
> \<details\>
> \<summary\>\<strong\>Design Rationale (click to expand)\</strong\>\</summary\>
> 
> \### Why requirements.txt? And why also pyproject.toml?
> 
> \`requirements.txt\` is the primary recommended format. Slicer extensions aren't Python packages — they just need "install these things into this environment," which is exactly what \`requirements.txt\` is for. It's pip's native input format, every Python developer knows it, and it requires no boilerplate beyond the dependency list itself.
> 
> That said, \`pyproject.toml\` is the modern standard in the Python ecosystem (PEP 621). It offers structured parsing via \`tomllib\` (stdlib) with no ad-hoc text handling, and extensions that already have a \`pyproject.toml\` for other tooling (ruff, pytest, etc.) can keep dependencies in one file. So we provide \`load\_pyproject\_dependencies\` as an alternative for extensions that prefer it.
> 
> Both formats boil down to PEP 508 dependency strings. Both loader functions return the same \`list\[Requirement\]\` type, so the downstream API (\`pip\_check\`, \`pip\_ensure\`, \`pip\_install\`) works identically regardless of which one you use.
> 
> \`load\_pyproject\_dependencies\` reads only the \`\[project.dependencies\]\` list. Other fields in the \`\[project\]\` table (\`name\`, \`version\`, etc.) are not read or validated — extensions aren't Python packages and shouldn't need to provide them.
> 
> \### Why pure-Python pip\_check instead of pip --dry-run?
> 
> Installing is not done frequently, but \_checking\_ may be called frequently. A pure-Python implementation using \`importlib.metadata\` avoids the overhead of a subprocess. If we used \`pip --dry-run\` it would have to be a subprocess.
> 
> \### Why explicit pip\_ensure instead of lazy import magic?
> 
> We considered \[LazyImportGroup\](https://github.com/Slicer/Slicer/issues/7707) which intercepts first use of imports to trigger installation. It is elegant, but it reduces transparency and makes debugging harder for extension developers. For now we get this pattern which has more boilerplate but is more transparent and simple to debug:
> 
> \`\`\`python
> slicer.pydeps.pip\_ensure(reqs, requester="MyExtension")
> import my\_dependency # Explicit, debuggable
> \`\`\`
> 
> IDE support works via \`TYPE\_CHECKING\`:
> \`\`\`python
> from typing import TYPE\_CHECKING
> if TYPE\_CHECKING:
> import my\_dependency # For type hints only
> \`\`\`
> 
> We can still consider the lazy import ideas in the future -- the lower level tools here would still be useful.
> 
> \### Why \`pip\_install\` now defaults to showing a progress dialog
> 
> Before this PR, \`pip\_install\` was always blocking with no progress feedback. The new defaults (\`blocking=True\`, \`show\_progress=True\`) mean existing code that calls \`pip\_install\` will now get a modal progress dialog "for free" without any code changes. This improves UX for many extensions immediately, while still allowing opt-out via \`show\_progress=False\`.
> 
> Hopefully it doesnt' break too many extensions...
> 
> \### Prevention of multiple non-blocking pip installs
> 
> When \`pip\_install\` runs with \`blocking=False\`, it returns immediately while pip runs in the background. If another non-blocking \`pip\_install\` is started before the first completes then we get chaos.
> 
> The solution adopted here is a module-level \`\_pip\_install\_in\_progress\` flag that raises \`RuntimeError\` if a second non-blocking install is attempted. Developers can check \`isPipInstallInProgress()\` first if they want to guard against this themselves.
> 
> \### Why \`no\_deps\_requirements\` parameter
> 
> Some Python packages declare overly strict dependency requirements that conflict with other packages in Slicer's environment. The standard workaround requires two separate pip calls:
> 
> \`\`\`python
> pip\_install("--no-deps problematic-package==1.0") # Ignore its deps
> pip\_install("numpy scipy") # Install known-good deps manually
> \`\`\`
> 
> The \`no\_deps\_requirements\` parameter handles this two-step process internally, making the intent self-documenting:
> 
> \`\`\`python
> pip\_install(requirements="numpy scipy", no\_deps\_requirements="problematic-pkg==1.0")
> \`\`\`
> 
> This also correctly handles non-blocking mode by chaining the two pip calls internally.
> 
> \### Why \`skip\_packages\` parameter
> 
> Multiple Slicer extensions (SlicerTotalSegmentator, SlicerNNUNet) independently implement ~70-90 lines of recursive selective-install code to install packages while excluding certain transitive dependencies. Common examples: SimpleITK (Slicer bundles a custom version), torch (must be installed via SlicerPyTorch for the correct CUDA/CPU build), and requests (already bundled, replacing it forces an unnecessary restart).
> 
> The \`skip\_packages\` parameter centralizes this logic. When provided, each package is installed with \`--no-deps\`, its dependency tree is walked recursively, and any package matching the skip list is excluded. Package METADATA is updated after installation so that \`pip check\` doesn't flag skipped packages as missing.
> 
> \### \`skip\_packages\` vs \`no\_deps\_requirements\`
> 
> Both are available; they serve different purposes:
> 
> \- \*\*\`no\_deps\_requirements\`\*\*: "Install these packages without any of their deps." You provide the correct deps yourself. Fast (2 pip calls), doesn't modify METADATA. Use when a specific package has broken dependency declarations.
> \- \*\*\`skip\_packages\`\*\*: "Install everything except these specific packages, anywhere in the dependency tree." Automatic recursive walk with METADATA scrubbing. Slower (one pip call per package). Use when you want most of a package's deps but need to exclude specific ones that Slicer provides differently.
> 
> They are mutually exclusive — providing both raises \`ValueError\`.
> 
> \### Why \`pip\_install\` doesn't accept \`list\[Requirement\]\`
> 
> It would feel natural to write \`pip\_install(load\_requirements("requirements.txt"))\`. The fact that you can't is a little unfortunate — it looks like it \*should\* work. But \`pip\_install\` is extremely widely used in the wild (via \`slicer.util.pip\_install\`), and we don't want to make breaking changes to its signature. It's also a low-level function that mirrors the pip CLI, taking the same kind of string arguments you'd pass on the command line, so it makes sense to leave it working the way it does.
> 
> The new structured \`list\[Requirement\]\` input type goes into the new \`slicer.pydeps\` functions instead: \`pip\_check\` and \`pip\_ensure\`. In practice, \`slicer.pydeps.pip\_ensure(load\_requirements("requirements.txt"))\` is the call you want anyway -- it checks what's already installed, prompts the user, installs only what's missing, and detects whether a restart is needed. So accepting \`Requirement\` objects at the \`pip\_ensure\` level rather than the \`pip\_install\` level steers developers toward the safer, idempotent workflow.
> 
> \### Why \`pip\_\` function naming
> 
> The new functions follow the existing naming convention established by \`slicer.util.pip\_install\` and \`slicer.util.pip\_uninstall\`. All pip-related functions now live together in \`slicer.pydeps\` with the \`pip\_\` prefix, providing a consistent, discoverable API.
> 
> \### Why \`slicer.pydeps\` (module naming)
> 
> The new module needed a name that (1) clearly conveys its purpose, (2) doesn't collide with existing packages in the Python ecosystem, and (3) works within Slicer's Python environment.
> 
> \*\*\`slicer.packaging\` was the initial choice\*\* — it's descriptive and mirrors the well-known third-party \`packaging\` library. However, Slicer's Python environment adds the \`slicer/\` directory itself to \`sys.path\` (i.e., \`\<build\>/bin/Python/slicer/\` is a path entry). This means a file named \`slicer/packaging.py\` shadows the third-party \`packaging\` library for any top-level \`from packaging import ...\` statement, breaking imports of \`packaging.requirements\`, \`packaging.markers\`, etc.
> 
> \*\*\`slicer.dependencies\`\*\* was considered, but in a codebase where half the stack is C++ with its own dependency tree (VTK, ITK, Qt, CTK), the unqualified "dependencies" is ambiguous.
> 
> \*\*\`slicer.pydeps\`\*\* was chosen because:
> \- The \`py\` prefix immediately scopes it to \*Python\* dependencies, which is what this module manages
> \- It's short (6 characters, comparable to \`util\`) and easy to type
> \- No collision with any stdlib module or common PyPI package
> \- Reads naturally: \`slicer.pydeps.pip\_ensure(...)\`, \`slicer.pydeps.load\_requirements(...)\`
> 
> \### Why \`import slicer.pydeps\` is explicit
> 
> \`slicer.util\` is auto-imported at startup (via \`from slicer.util import \*\` in \`slicerqt.py\`), so \`slicer.util.pip\_install(...)\` works without an explicit import. We intentionally did \*not\* do the same for \`slicer.pydeps\`.
> 
> \`slicer.util\` gets away with auto-import because its top-level imports are all stdlib — the heavy dependencies (\`qt\`, \`ctk\`, \`slicer.app\`) are lazy imports inside function bodies, so the startup cost is essentially zero. \`slicer.pydeps\` is different: it uses proper top-level imports for \`packaging.markers\`, \`packaging.requirements\`, \`importlib.metadata\`, etc. These are lightweight individually, but there's no reason for every Slicer session to pay that cost when most sessions never install a Python package.
> 
> The tradeoff is worth it. Top-level imports in \`slicer.pydeps\` make the module easier to read, ensure import errors surface immediately rather than hiding inside deeply nested call stacks, and follow standard Python conventions. The cost is one \`import slicer.pydeps\` line in extension code that uses the new API — a trivial ask. And for the common case of \`slicer.util.pip\_install(...)\`, no explicit import is needed because the wrapper does its own lazy import internally.
> 
> \### Why \`pip\_uninstall\` was also modified
> 
> While the focus of this work is on dependency installation, \`pip\_uninstall\` was updated with the same non-blocking parameters (\`blocking\`, \`logCallback\`, \`completedCallback\`) for API consistency. Since both functions share the same underlying infrastructure (\`launchConsoleProcess\` and \`\_executePythonModule\`) in \`slicer.pydeps\`, extending non-blocking support to \`pip\_uninstall\` required minimal additional code and ensures users have a symmetric API for both operations.
> 
> \### Non-blocking implementation
> 
> The non-blocking mode uses a QTimer-based polling approach inspired by \[SlicerMONAIAuto3DSeg\](https://github.com/lassoan/SlicerMONAIAuto3DSeg). A background thread reads process output into a queue while QTimer polls from the main thread, keeping the Qt event loop responsive.
> 
> \### How the restart prompt works
> 
> After \`pip\_ensure\` installs packages, it snapshots all installed package versions (before and after) using \`importlib.metadata\`. For any packages whose version changed, it checks whether their top-level import names appear in \`sys.modules\` (meaning they were already imported in the current session). The distribution-to-import-name mapping uses \`importlib.metadata.packages\_distributions()\` (Python 3.11+). If any already-imported packages were updated, a "Restart Recommended" dialog shows the affected packages with their old → new versions. The \`prompt\_restart=False\` parameter disables this check.
> 
> \</details\>
> 
> \---
> 
> \<details\>
> \<summary\>\<strong\>Interactive Feature Tour (click to expand)\</strong\>\</summary\>
> 
> Build Slicer from the \`python-dependency-handling-improvements\` branch, launch it, open the Python console, and paste the script below. A menu lets you pick which demos to run — or select "Run All" for the full walkthrough.
> 
> \`\`\`python
> import importlib.metadata as \_md
> import os
> import tempfile
> 
> import qt
> from packaging.requirements import Requirement
> 
> import slicer
> import slicer.pydeps
> 
> \# ---------------------------------------------------------------------------
> \# Configuration — change these to try a different demo package
> \# ---------------------------------------------------------------------------
> 
> DEMO\_PACKAGE = "scikit-image" # pip install name
> DEMO\_IMPORT = "skimage" # Python import name
> DEMO\_CONSTRAINT = "\>=0.20,\<0.25" # version range for constraints demo
> DEMO\_SKIP\_DEP = "imageio" # dependency to skip in skip\_packages demo
> 
> \# ---------------------------------------------------------------------------
> \# State
> \# ---------------------------------------------------------------------------
> 
> try:
> pkg\_version\_before\_tour = \_md.version(DEMO\_PACKAGE)
> except \_md.PackageNotFoundError:
> pkg\_version\_before\_tour = None
> 
> 
> \# ---------------------------------------------------------------------------
> \# Helpers
> \# ---------------------------------------------------------------------------
> 
> def clear\_cache():
> """Purge the pip download cache so installs show real download progress."""
> slicer.pydeps.\_executePythonModule("pip", \["cache", "purge"\])
> 
> 
> def uninstall\_pkg():
> """Remove the demo package if present."""
> try:
> slicer.util.pip\_uninstall(DEMO\_PACKAGE)
> except Exception:
> pass
> 
> 
> def fresh\_slate():
> """Uninstall the demo package and clear the cache."""
> uninstall\_pkg()
> clear\_cache()
> 
> 
> \# ---------------------------------------------------------------------------
> \# Demo 1 — Loading Dependencies
> \# ---------------------------------------------------------------------------
> 
> def demo\_loading\_deps():
> """load\_requirements() and load\_pyproject\_dependencies()"""
> 
> # --- requirements.txt ---
> slicer.util.infoDisplay(
> "load\_requirements(path) parses a requirements.txt file into\\n"
> "Requirement objects, skipping comments, blanks, and pip options.\\n"
> "\\n"
> "Watch the Python console for parsed results.",
> windowTitle="Demo 1: Loading Dependencies — requirements.txt",
> )
> 
> with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
> f.write("# Example requirements file\\n")
> f.write("numpy\>=1.20\\n")
> f.write(f"{DEMO\_PACKAGE}\>=0.20\\n")
> f.write("nonexistent-package-xyz\>=1.0\\n")
> f.write("-c constraints.txt\\n")
> f.write("\\n")
> path = f.name
> 
> reqs = slicer.pydeps.load\_requirements(path)
> os.unlink(path)
> 
> lines = \[f" {r.name} {r.specifier}" for r in reqs\]
> print(f"\[Tour\] load\_requirements -\> {len(reqs)} requirements:\\n" + "\\n".join(lines))
> 
> # --- pyproject.toml ---
> with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
> f.write("\[project\]\\n")
> f.write("dependencies = \[\\n")
> f.write(' "numpy\>=1.20",\\n')
> f.write(f' "{DEMO\_PACKAGE}\>=0.20",\\n')
> f.write(' "nonexistent-package-xyz\>=1.0",\\n')
> f.write("\]\\n")
> path = f.name
> 
> reqs2 = slicer.pydeps.load\_pyproject\_dependencies(path)
> os.unlink(path)
> 
> lines2 = \[f" {r.name} {r.specifier}" for r in reqs2\]
> print(f"\[Tour\] load\_pyproject\_dependencies -\> {len(reqs2)} requirements:\\n" + "\\n".join(lines2))
> 
> slicer.util.infoDisplay(
> f"Loaded {len(reqs)} requirements from requirements.txt\\n"
> f"and {len(reqs2)} from pyproject.toml.\\n"
> "\\n"
> "Both return the same Requirement objects — the downstream\\n"
> "API (pip\_check, pip\_ensure) works identically with either.",
> windowTitle="Demo 1: Loading Dependencies — result",
> )
> 
> 
> \# ---------------------------------------------------------------------------
> \# Demo 2 — Checking Requirements
> \# ---------------------------------------------------------------------------
> 
> def demo\_checking\_reqs():
> """pip\_check() — pure-Python requirement validation"""
> 
> slicer.util.infoDisplay(
> "pip\_check(req) checks if a requirement is satisfied.\\n"
> "Pure Python, no subprocess — fast enough to call frequently.\\n"
> "\\n"
> "We will test several cases against the current environment.",
> windowTitle="Demo 2: Checking Requirements",
> )
> 
> checks = \[
> ("numpy\>=1.0", "Bundled with Slicer — should be satisfied"),
> ("numpy\>=99999.0", "Impossibly high version — should fail"),
> ("nonexistent-xyz\>=1.0", "Package not installed"),
> ('foo; sys\_platform == "nonexistent"', "Marker does not apply — treated as satisfied"),
> \]
> 
> results = \[\]
> for spec, desc in checks:
> req = Requirement(spec)
> ok = slicer.pydeps.pip\_check(req)
> mark = "SATISFIED" if ok else "NOT satisfied"
> results.append(f" \[{mark}\] {spec}\\n {desc}")
> 
> slicer.util.infoDisplay(
> "pip\_check results:\\n"
> "\\n" + "\\n\\n".join(results),
> windowTitle="Demo 2: Checking Requirements — results",
> )
> 
> 
> \# ---------------------------------------------------------------------------
> \# Demo 3 — Installing with Progress
> \# ---------------------------------------------------------------------------
> 
> def demo\_install\_progress():
> """pip\_install() — modal dialog and non-blocking status bar"""
> 
> # --- Part 1: Modal (blocking) ---
> fresh\_slate()
> 
> slicer.util.infoDisplay(
> "pip\_install() — modal progress dialog (the new default).\\n"
> "\\n"
> "Try expanding the Details section!",
> windowTitle="Demo 3a: Modal Progress Dialog",
> )
> 
> slicer.util.pip\_install(DEMO\_PACKAGE, requester="Feature Tour")
> 
> slicer.util.infoDisplay(
> "Modal install done.\\n"
> "\\n"
> "Now switching to non-blocking mode: the call returns\\n"
> "immediately and pip output appears in the status bar.",
> windowTitle="Demo 3a: Modal — done",
> )
> 
> # --- Part 2: Non-blocking ---
> fresh\_slate()
> 
> slicer.util.infoDisplay(
> "pip\_install() — non-blocking with status bar.\\n"
> "\\n"
> "Watch the STATUS BAR at the bottom. The UI stays interactive.\\n"
> "isPipInstallInProgress() will be printed to the console.",
> windowTitle="Demo 3b: Status Bar Mode",
> )
> 
> loop = qt.QEventLoop()
> \_result = \[None\]
> 
> def on\_complete(return\_code):
> \_result\[0\] = return\_code
> qt.QTimer.singleShot(0, loop.quit)
> 
> slicer.util.pip\_install(
> DEMO\_PACKAGE,
> blocking=False,
> show\_progress=True,
> requester="Feature Tour",
> completedCallback=on\_complete,
> )
> 
> # Check in-progress flag shortly after starting
> qt.QTimer.singleShot(500, lambda: print(
> f"\[Tour\] isPipInstallInProgress() = {slicer.pydeps.isPipInstallInProgress()}"
> ))
> 
> loop.exec\_()
> 
> slicer.util.infoDisplay(
> f"Non-blocking install finished (return code {\_result\[0\]}).\\n"
> "The status bar showed pip output while the UI stayed responsive.",
> windowTitle="Demo 3b: Status Bar — done",
> )
> 
> 
> \# ---------------------------------------------------------------------------
> \# Demo 4 — Smart Install Workflow
> \# ---------------------------------------------------------------------------
> 
> def demo\_smart\_install():
> """pip\_ensure() — check, prompt, install, restart detection"""
> 
> # --- Part 1: Normal pip\_ensure ---
> fresh\_slate()
> 
> slicer.util.infoDisplay(
> "pip\_ensure() — the recommended high-level API for extensions.\\n"
> "Checks requirements, shows confirmation, installs with progress.\\n"
> "\\n"
> "You will see a confirmation dialog, then a progress dialog.",
> windowTitle="Demo 4a: pip\_ensure",
> )
> 
> reqs = \[Requirement(f"{DEMO\_PACKAGE}\>=0.20")\]
> slicer.pydeps.pip\_ensure(reqs, requester="Feature Tour")
> 
> slicer.util.infoDisplay(
> "pip\_ensure done. Now we will call it again immediately.\\n"
> "\\n"
> "pip\_check sees the package is already installed, so\\n"
> "pip\_ensure skips everything — no dialogs, instant return.",
> windowTitle="Demo 4a: pip\_ensure — done",
> )
> 
> slicer.pydeps.pip\_ensure(reqs, requester="Feature Tour (no-op)")
> 
> slicer.util.infoDisplay(
> "pip\_ensure returned instantly — nothing to install.\\n"
> "\\n"
> "Now: restart prompt demo. We will import the package, uninstall\\n"
> "it, then pip\_ensure again. Since it is in memory, a restart\\n"
> "dialog will appear. (Click NO when asked to restart to continue the tour.)",
> windowTitle="Demo 4a: pip\_ensure — no-op verified",
> )
> 
> # --- Part 2: Restart prompt ---
> import importlib
> mod = importlib.import\_module(DEMO\_IMPORT)
> print(f"\[Tour\] Imported {DEMO\_IMPORT} {mod.\_\_version\_\_}")
> 
> uninstall\_pkg()
> clear\_cache()
> 
> slicer.pydeps.pip\_ensure(reqs, requester="Feature Tour (restart demo)")
> 
> slicer.util.infoDisplay(
> "Restart prompt demonstrated.\\n"
> "\\n"
> "This helps users know when a restart is needed after\\n"
> "updating packages that were already imported.",
> windowTitle="Demo 4b: Restart Prompt — done",
> )
> 
> 
> \# ---------------------------------------------------------------------------
> \# Demo 5 — Advanced Options
> \# ---------------------------------------------------------------------------
> 
> def demo\_advanced\_options():
> """Constraints file and skip\_packages"""
> 
> # --- Part 1: Constraints ---
> fresh\_slate()
> 
> slicer.util.infoDisplay(
> "Constraints file: limits which versions pip may install.\\n"
> f"\\n"
> f"Installing {DEMO\_PACKAGE} constrained to {DEMO\_CONSTRAINT}.",
> windowTitle="Demo 5a: Constraints",
> )
> 
> with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
> f.write(f"{DEMO\_PACKAGE}{DEMO\_CONSTRAINT}\\n")
> constraints\_path = f.name
> 
> slicer.util.pip\_install(
> DEMO\_PACKAGE,
> constraints=constraints\_path,
> requester="Feature Tour (constrained)",
> )
> os.unlink(constraints\_path)
> 
> ver = \_md.version(DEMO\_PACKAGE)
> slicer.util.infoDisplay(
> f"Installed {DEMO\_PACKAGE} {ver} (constrained to {DEMO\_CONSTRAINT}).\\n"
> "\\n"
> f"Now: skip\_packages demo — installing {DEMO\_PACKAGE}\\n"
> f"while skipping '{DEMO\_SKIP\_DEP}' (one of its dependencies).\\n"
> f"\\n"
> f"First we uninstall {DEMO\_SKIP\_DEP} (if present) so we can\\n"
> f"verify at the end that skip\_packages actually prevented it.",
> windowTitle="Demo 5a: Constraints — done",
> )
> 
> # --- Part 2: skip\_packages ---
> fresh\_slate()
> try:
> slicer.util.pip\_uninstall(DEMO\_SKIP\_DEP)
> except Exception:
> pass
> 
> skipped = slicer.util.pip\_install(
> DEMO\_PACKAGE,
> skip\_packages=\[DEMO\_SKIP\_DEP\],
> requester="Feature Tour (skip\_packages)",
> )
> 
> lines = \[f" {s}" for s in (skipped or \[\])\]
> print(f"\[Tour\] Skipped packages:\\n" + "\\n".join(lines))
> 
> # Verify the metadata scrub: re-install normally, check the skipped dep
> slicer.util.pip\_install(DEMO\_PACKAGE, requester="Feature Tour (verify scrub)")
> dep\_installed = slicer.pydeps.pip\_check(Requirement(DEMO\_SKIP\_DEP))
> 
> slicer.util.infoDisplay(
> f"Skipped {len(skipped or \[\])} package(s).\\n"
> f"{DEMO\_SKIP\_DEP} installed after normal re-install: {dep\_installed}\\n"
> "\\n"
> "Re-installing normally did NOT pull in the skipped dependency\\n"
> "because the metadata scrub removed it.",
> windowTitle="Demo 5b: skip\_packages — verified",
> )
> 
> 
> \# ---------------------------------------------------------------------------
> \# Cleanup
> \# ---------------------------------------------------------------------------
> 
> def do\_cleanup():
> """Restore the user's environment."""
> if pkg\_version\_before\_tour is not None:
> msg = (
> f"{DEMO\_PACKAGE} {pkg\_version\_before\_tour} was installed before\\n"
> "the tour. Restore it now?"
> )
> else:
> msg = f"Uninstall {DEMO\_PACKAGE} to leave your environment clean?"
> 
> if slicer.util.confirmYesNoDisplay(msg, windowTitle="Feature Tour — Cleanup"):
> if pkg\_version\_before\_tour is not None:
> uninstall\_pkg()
> slicer.util.pip\_install(
> f"{DEMO\_PACKAGE}=={pkg\_version\_before\_tour}",
> requester="Feature Tour (restore)",
> )
> else:
> uninstall\_pkg()
> 
> slicer.util.infoDisplay(
> "Tour complete! For full API docs:\\n"
> " help(slicer.pydeps.pip\_ensure)",
> windowTitle="Feature Tour — Done",
> )
> 
> 
> \# ---------------------------------------------------------------------------
> \# Menu and main loop
> \# ---------------------------------------------------------------------------
> 
> MENU\_ITEMS = \[
> "1. Loading Dependencies",
> "2. Checking Requirements",
> "3. Installing with Progress",
> "4. Smart Install Workflow",
> "5. Advanced Options",
> "---",
> "Run All",
> "Exit Tour",
> \]
> 
> DEMO\_FUNCS = {
> MENU\_ITEMS\[0\]: demo\_loading\_deps,
> MENU\_ITEMS\[1\]: demo\_checking\_reqs,
> MENU\_ITEMS\[2\]: demo\_install\_progress,
> MENU\_ITEMS\[3\]: demo\_smart\_install,
> MENU\_ITEMS\[4\]: demo\_advanced\_options,
> }
> 
> DEMO\_ORDER = MENU\_ITEMS\[:5\]
> 
> 
> def run\_tour():
> slicer.util.infoDisplay(
> "Welcome to the PR #9010 Feature Tour!\\n"
> "\\n"
> "Pick demos from the menu. Uses scikit-image as a demo package\\n"
> "(configurable via DEMO\_PACKAGE at the top of the script).\\n"
> 'Select "Run All" for the full experience.',
> windowTitle="Feature Tour — Welcome",
> )
> 
> while True:
> dialog = qt.QInputDialog(slicer.util.mainWindow())
> dialog.setWindowTitle("Feature Tour")
> dialog.setLabelText("Choose a demo:")
> dialog.setComboBoxItems(MENU\_ITEMS)
> dialog.setComboBoxEditable(False)
> 
> if dialog.exec\_() != qt.QDialog.Accepted:
> break
> 
> choice = dialog.textValue()
> 
> if choice == "Exit Tour" or choice == "---":
> break
> 
> if choice == "Run All":
> for key in DEMO\_ORDER:
> DEMO\_FUNCS\[key\]()
> else:
> func = DEMO\_FUNCS.get(choice)
> if func:
> func()
> 
> do\_cleanup()
> 
> 
> \# ---------------------------------------------------------------------------
> \# Start the tour
> \# ---------------------------------------------------------------------------
> 
> run\_tour()
> \`\`\`
> 
> \</details\>
> 
> \---
> 
> \<details\>
> \<summary\>\<strong\>References (click to expand)\</strong\>\</summary\>
> 
> \- This work is part of the \[44th Slicer project week\](https://projectweek.na-mic.org/PW44\_2026\_GranCanaria/Projects/PythonDependenciesInExtensions/)!
> \- \[#7171 — Improving Support for Python Package Dependencies in Slicer Extensions\](https://github.com/Slicer/Slicer/issues/7171) — This PR implements the "runtime installation" approach described in the issue: extensions declare dependencies via \`requirements.txt\`, and the new \`slicer.pydeps\` module handles checking and installation with optional constraints file support for coordinating versions across extensions.
> \- \[#7707 — Allow scripted modules to declare and lazily install pip requirements\](https://github.com/Slicer/Slicer/issues/7707) — Proposes a \`LazyImportGroup\` context manager that intercepts imports and triggers installation on first attribute access. This PR takes a simpler, more explicit approach: developers call \`pip\_ensure()\` at the point dependencies are needed, then import normally. The explicit pattern trades some elegance for transparency and easier debugging. The \`LazyImportGroup\` approach could potentially be built on top of the primitives provided by \`slicer.pydeps\` (\`load\_requirements\`, \`pip\_check\`, \`pip\_install\` with callbacks) if desired in the future. See #8181.
> \- At https://github.com/KitwareMedical/SlicerNNUnet/pull/21 is a demo of how some of the features here can simplify python dependency handling in the NNUnet extension.
> 
> \</details\>

Thibault brings up [a good point](https://github.com/Slicer/Slicer/pull/9010#issuecomment-4110422113) – should Slicer’s automatic import of everything `slicer.*` be fixed?

- It’s just for the python interactor. It makes sense to deliberately import certain `slicer.*` things. We should find out what is intended to be imported and import them deliberatey rather than

There seems to be [some](https://github.com/Slicer/Slicer/pull/9010#issuecomment-4110631907) [friction](https://github.com/Slicer/Slicer/pull/9010#issuecomment-4112444228) caused by the `skip` option that lets you skip some transitive dependencies. They have a point, and the reason it was needed [is perhaps gone](https://github.com/Slicer/Slicer/pull/9010#issuecomment-4113419759). Remove the skip functionality?

- It’s indeed not nice but it is a problem that needs to be solved and it’s okay to have the _option_ to solve it using this not-very-nice-method. It beats forking every dependency that contians unnecessary aspects. In the future virtual environment support will also be a nice way around these issues. Maybe could add some messaging that discourages the use of this option under normal circumstances.

## Telemetry (Ebrahim)

> <https://github.com/Slicer/SlicerTelemetry/issues/12>
>
> These steps assume you have a Linux server with sudo privileges (e.g., a VPS or …cloud instance). They install and configure everything needed to host the telemetry Flask app with Gunicorn and Nginx. @sjh26 could you please review the instructions?
> 
> 1 - Connect to the Server
> 
> \`ssh your-user@your-server-ip\`
> 
> 2 - Update the System
> 
> \`sudo apt update && sudo apt upgrade -y
> \`
> 3 - Install Required Packages
> 
> \`sudo apt install -y python3 python3-venv python3-pip nginx git\`
> 
> 4 - Clone Your Repository
> 
> \`\`\`
> sudo mkdir -p /opt/slicer-usage-stats-backend
> sudo chown $USER:$USER /opt/slicer-usage-stats-backend
> 
> git clone -b master https://github.com/BerDom-Ing/slicer-usage-stats-backend.git /opt/slicer-usage-stats-backend
> cd /opt/slicer-usage-stats-backend
> \`\`\`
> 
> 5 - Set Up a Python Virtual Environment
> 
> \`\`\`
> python3 -m venv venv
> source venv/bin/activate
> pip install --upgrade pip
> pip install -r requirements.txt
> \`\`\`
> 
> 6 - Test With Gunicorn Manually
> 
> Run this to confirm your app starts:
> 
> \`gunicorn --bind 0.0.0.0:8000 wsgi:application\`
> 
> Visit in your browser:
> 
> \`http://your-server-ip:8000/\`
> 
> You should see the working Flask app.
> 
> 7 - Create a Systemd Service for Gunicorn
> 
> Create a service file:
> 
> \`sudo nano /etc/systemd/system/slicer-usage-stats.service\`
> 
> Paste:
> 
> \`\`\`
> \[Unit\]
> Description=Gunicorn service for slicer-usage-stats-backend
> After=network.target
> 
> \[Service\]
> User=your-user
> Group=www-data
> WorkingDirectory=/opt/slicer-usage-stats-backend
> Environment="PATH=/opt/slicer-usage-stats-backend/venv/bin"
> ExecStart=/opt/slicer-usage-stats-backend/venv/bin/gunicorn \\
> --workers 3 \\
> --bind unix:/run/gunicorn-slicer.sock \\
> wsgi:application
> 
> \[Install\]
> WantedBy=multi-user.target
> \`\`\`
> 
> Reload and start:
> 
> \`\`\`
> sudo systemctl daemon-reload
> sudo systemctl enable --now slicer-usage-stats
> 
> \`\`\`
> Check status:
> 
> \`sudo systemctl status slicer-usage-stats\`
> 
> 8 - Configure Nginx
> 
> Create an Nginx site file:
> 
> \`sudo nano /etc/nginx/sites-available/slicer-usage-stats
> \`
> Paste:
> 
> \`\`\`
> server {
> listen 80;
> server\_name your-domain-or-ip;
> 
> location / {
> proxy\_pass http://unix:/run/gunicorn-slicer.sock;
> proxy\_set\_header Host $host;
> proxy\_set\_header X-Real-IP $remote\_addr;
> proxy\_set\_header X-Forwarded-For $proxy\_add\_x\_forwarded\_for;
> proxy\_set\_header X-Forwarded-Proto $scheme;
> }
> }
> \`\`\`
> 
> Enable and reload:
> 
> \`\`\`
> sudo ln -s /etc/nginx/sites-available/slicer-usage-stats /etc/nginx/sites-enabled/
> sudo nginx -t
> sudo systemctl reload nginx
> \`\`\`

Where to deploy?

- Idea is to get KW IT to get us a linux VM and deploy there.
- Look at the PR on SlicerTelemetry and integrate that first as it also fixes a bug.

## Extension dashbaord order

For macOS they can get hung up on tests and then take a long time to upload.

Currently the order is build-package-test-upload. We should build-package- **upload** -test. At least for nightly this makes sense. Even if tests fail we don’t want to hold back the upload, for nightly.

## Preview extensions not being uploaded to SPKC

> **[SlicerPreview - Build Errors](https://slicer.cdash.org/builds/4153345/errors)**
>
> CDash is an open source, web-based software testing server. CDash aggregates, analyzes, and displays the
> results of software testing processes submitted from clients located around the world. CDash is a part of a
> ...

Since a dicom metadata update over the weekend, packages have not been getting uploaded. We need to update the girder client on the factories.

- Need to get this fixed and integrated: [ENH: Add additional extension metadata by Sunderlandkyl · Pull Request #128 · girder/slicer\_package\_manager · GitHub](https://github.com/girder/slicer_package_manager/pull/128)
- Then update the factories and the SPKC server

In the future, giving the command line API the ability to ignore unrecognized commands with a warning rather than failing totally would be useful to avoid these issues when doing this kind of client-and-server update.
