pymmcore-plus 0.15.4__py3-none-any.whl → 0.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -130,7 +130,10 @@ class UniMMCore(CMMCorePlus):
130
130
 
131
131
  def reset(self) -> None:
132
132
  with suppress(TimeoutError):
133
- self.waitForSystem()
133
+ self._pydevices.wait_for_device_type(
134
+ DeviceType.AnyType, self.getTimeoutMs(), parallel=False
135
+ )
136
+ super().waitForDeviceType(DeviceType.AnyType)
134
137
  self.unloadAllDevices()
135
138
  self._pycore.reset_current()
136
139
  super().reset()
pymmcore_plus/install.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import os
4
5
  import re
5
6
  import shutil
@@ -7,6 +8,7 @@ import ssl
7
8
  import subprocess
8
9
  import sys
9
10
  import tempfile
11
+ import zipfile
10
12
  from contextlib import contextmanager, nullcontext
11
13
  from functools import cache
12
14
  from pathlib import Path
@@ -16,6 +18,7 @@ from urllib.request import urlopen, urlretrieve
16
18
 
17
19
  import typer
18
20
 
21
+ from pymmcore_plus import _pymmcore
19
22
  from pymmcore_plus._util import USER_DATA_MM_PATH
20
23
 
21
24
  if TYPE_CHECKING:
@@ -62,11 +65,12 @@ MACH = machine()
62
65
  BASE_URL = "https://download.micro-manager.org"
63
66
  plat = {"Darwin": "Mac", "Windows": "Windows", "Linux": "Linux"}.get(PLATFORM)
64
67
  DOWNLOADS_URL = f"{BASE_URL}/nightly/2.0/{plat}/"
65
-
68
+ TEST_ADAPTERS_REPO = "micro-manager/mm-test-adapters"
69
+ PYMMCORE_DIV = _pymmcore.version_info.device_interface
66
70
 
67
71
  # Dates of release for each interface version.
68
72
  # generally running `mmcore install -r <some_date>` will bring in devices with
69
- # the NEW interface.
73
+ # the NEW interface, introduced that day.
70
74
  INTERFACES: dict[int, str] = {
71
75
  74: "20250815",
72
76
  73: "20250318",
@@ -173,11 +177,19 @@ def _mac_install(dmg: Path, dest: Path, log_msg: _MsgLogger) -> None:
173
177
 
174
178
  @cache
175
179
  def available_versions() -> dict[str, str]:
176
- """Return a map of version -> url available for download."""
180
+ """Return a map of version -> url available for download.
181
+
182
+ Returns a dict like:
183
+ {
184
+ '20220906': 'https://download.micro-manager.org/nightly/2.0/Mac/Micro-Manager-2.0.1-20220906.dmg',
185
+ '20220901': 'https://download.micro-manager.org/nightly/2.0/Mac/Micro-Manager-2.0.1-20220901.dmg',
186
+ '20220823': 'https://download.micro-manager.org/nightly/2.0/Mac/Micro-Manager-2.0.1-20220823.dmg',
187
+ }
188
+ """
177
189
  with urlopen(DOWNLOADS_URL) as resp:
178
190
  html = resp.read().decode("utf-8")
179
191
 
180
- all_links = re.findall(r"href=\"([^\"]+)\"", html)
192
+ all_links = re.findall(r'href="([^"]+)"', html)
181
193
  delim = "_" if PLATFORM == "Windows" else "-"
182
194
  return {
183
195
  ref.rsplit(delim, 1)[-1].split(".")[0]: BASE_URL + ref
@@ -186,6 +198,122 @@ def available_versions() -> dict[str, str]:
186
198
  }
187
199
 
188
200
 
201
+ @cache
202
+ def _available_test_adapter_releases() -> dict[str, str]:
203
+ """Get available releases from GitHub mm-test-adapters repository.
204
+
205
+ Returns a dict like:
206
+ {
207
+ '20250825': '74.20250825',
208
+ ...
209
+ }
210
+ """
211
+ github_api_url = f"https://api.github.com/repos/{TEST_ADAPTERS_REPO}/releases"
212
+
213
+ with urlopen(github_api_url) as resp:
214
+ releases = json.loads(resp.read().decode("utf-8"))
215
+
216
+ release_map = {}
217
+ for release in releases:
218
+ tag_name = release["tag_name"] # e.g., "74.20250825"
219
+ if "." in tag_name:
220
+ _, date_part = tag_name.split(".", 1) # Extract YYYYMMDD part
221
+ release_map[date_part] = tag_name
222
+
223
+ return release_map
224
+
225
+
226
+ def _get_platform_arch_string() -> str:
227
+ """Get platform and architecture string for GitHub releases."""
228
+ if PLATFORM == "Darwin":
229
+ arch = "ARM64" if MACH == "arm64" else "X64"
230
+ return f"macOS-{arch}"
231
+ elif PLATFORM == "Windows":
232
+ return "Windows-X64"
233
+ elif PLATFORM == "Linux":
234
+ return "Linux-X64"
235
+ else: # pragma: no cover
236
+ raise ValueError(f"Unsupported platform: {PLATFORM}")
237
+
238
+
239
+ def _test_dev_install(
240
+ dest: Path | str = USER_DATA_MM_PATH,
241
+ release: str = "latest",
242
+ ) -> None:
243
+ """Install just the test devices into dest.
244
+
245
+ From https://github.com/micro-manager/mm-test-adapters/releases/latest
246
+ (Releases on github are versioned as: DIV.YYYYMMDD)
247
+
248
+ If release is `latest-compatible`, it will install the latest compatible version
249
+ of the test devices for the current device interface version.
250
+
251
+ Parameters
252
+ ----------
253
+ dest : Path | str, optional
254
+ Where to install Micro-Manager, by default will install to a pymmcore-plus
255
+ folder in the user's data directory: `appdirs.user_data_dir()`.
256
+ release : str, optional
257
+ Which release to install, by default "latest-compatible". Should be a date
258
+ in the form YYYYMMDD, "latest" to install the latest nightly release, or
259
+ "latest-compatible" to install the latest nightly release that is
260
+ compatible with the device interface version of the current pymmcore version.
261
+ """
262
+ dest = Path(dest).expanduser().resolve()
263
+ dest.mkdir(parents=True, exist_ok=True)
264
+
265
+ # Get available GitHub releases
266
+ if not (github_releases := _available_test_adapter_releases()): # pragma: no cover
267
+ raise ValueError("No test device releases found on GitHub")
268
+
269
+ # Build download URL
270
+ filename = f"mm-test-adapters-{_get_platform_arch_string()}.zip"
271
+ base_url = f"https://github.com/{TEST_ADAPTERS_REPO}/releases"
272
+ if release == "latest-compatible":
273
+ # Find the latest release compatible with the current device interface
274
+ # this is easier with test devices because their version starts with the DIV
275
+ available = sorted(github_releases.values(), reverse=True)
276
+ for version in available:
277
+ if version.startswith(f"{PYMMCORE_DIV}."):
278
+ # Found a compatible version
279
+ download_url = f"{base_url}/download/{version}/{filename}"
280
+ tag_name = version
281
+ break
282
+ else: # pragma: no cover
283
+ raise ValueError(
284
+ f"No compatible releases found for device interface {PYMMCORE_DIV}. "
285
+ f"Found: {available}"
286
+ )
287
+ elif release == "latest":
288
+ download_url = f"{base_url}/latest/download/{filename}"
289
+ tag_name = sorted(github_releases.values(), reverse=True)[0]
290
+ else:
291
+ if release not in github_releases:
292
+ _available = ", ".join(sorted(github_releases.keys(), reverse=True))
293
+ raise ValueError(
294
+ f"Release {release!r} not found. Available releases: {_available}"
295
+ )
296
+ tag_name = github_releases[release]
297
+ download_url = f"{base_url}/download/{tag_name}/{filename}"
298
+
299
+ _dest = dest / f"Micro-Manager-{tag_name}"
300
+ with tempfile.TemporaryDirectory() as tmpdir:
301
+ # Download the zip file
302
+ zip_path = Path(tmpdir) / filename
303
+ _download_url(
304
+ url=download_url, output_path=zip_path, show_progress=progress is not None
305
+ )
306
+
307
+ # Extract the zip file
308
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
309
+ zip_ref.extractall(_dest)
310
+
311
+ # On macOS, remove quarantine attribute
312
+ if PLATFORM == "Darwin":
313
+ cmd = ["xattr", "-r", "-d", "com.apple.quarantine", str(_dest)]
314
+ subprocess.run(cmd, check=False) # Don't fail if xattr fails
315
+
316
+
189
317
  def _download_url(url: str, output_path: Path, show_progress: bool = True) -> None:
190
318
  """Download `url` to `output_path` with a nice progress bar."""
191
319
  ctx = ssl.create_default_context()
@@ -219,6 +347,7 @@ def install(
219
347
  dest: Path | str = USER_DATA_MM_PATH,
220
348
  release: str = "latest-compatible",
221
349
  log_msg: _MsgLogger = _pretty_print,
350
+ test_adapters: bool = False,
222
351
  ) -> None:
223
352
  """Install Micro-Manager to `dest`.
224
353
 
@@ -236,30 +365,32 @@ def install(
236
365
  Callback to log messages, must have signature:
237
366
  `def logger(text: str, color: str = "", emoji: str = ""): ...`
238
367
  May ignore color and emoji.
368
+ test_adapters : bool, optional
369
+ Whether to install test adapters, by default False.
239
370
  """
371
+ if test_adapters:
372
+ _test_dev_install(dest=dest, release=release)
373
+ return
374
+
240
375
  if PLATFORM not in ("Darwin", "Windows") or (
241
376
  PLATFORM == "Darwin" and MACH == "arm64"
242
- ): # pragma: no cover
377
+ ):
243
378
  log_msg(
244
- f"Unsupported platform/architecture: {PLATFORM}/{MACH}", "bold red", ":x:"
379
+ f"Unsupported platform/architecture for nightly build: {PLATFORM}/{MACH}\n"
380
+ " (Downloading just test adapters) ...",
381
+ "bold magenta",
382
+ ":exclamation:",
245
383
  )
246
- log_msg(
247
- "Consider building from source (mmcore build-dev).",
248
- "bold yellow",
249
- ":light_bulb:",
250
- )
251
- raise sys.exit(1)
384
+ _test_dev_install(dest=dest, release=release)
385
+ return
252
386
 
253
387
  if release == "latest-compatible":
254
- from pymmcore_plus import _pymmcore
255
-
256
- div = _pymmcore.version_info.device_interface
257
388
  # date when the device interface version FOLLOWING the version that this
258
389
  # pymmcore supports was released.
259
- next_div_date = INTERFACES.get(div + 1, None)
390
+ next_div_date = INTERFACES.get(PYMMCORE_DIV + 1, None)
260
391
 
261
392
  # if div is equal to the greatest known interface version, use latest
262
- if div == max(INTERFACES.keys()) or next_div_date is None:
393
+ if PYMMCORE_DIV == max(INTERFACES.keys()) or next_div_date is None:
263
394
  release = "latest"
264
395
  else: # pragma: no cover
265
396
  # otherwise, find the date of the release in available_versions() that
@@ -273,7 +404,7 @@ def install(
273
404
  # fallback to latest if no compatible versions found
274
405
  raise ValueError(
275
406
  "Unable to find a compatible release for device interface"
276
- f"{div} at {DOWNLOADS_URL} "
407
+ f"{PYMMCORE_DIV} at {DOWNLOADS_URL} "
277
408
  )
278
409
 
279
410
  if release == "latest":