Repository: napari/napari

Issue:
BUG: Plugins can downgrade napari via plugin GUI, without warning the user
## 🐛 Bug

If a plugin pins napari to an older version than the running napari with plugin installer GUI, the plugin will install and downgrade napari, silently without warning to the user.

## To Reproduce

Steps to reproduce the behavior:

1. install napari 0.4.16 or 0.4.17rc
2. use the plugin GUI to install `napari-n2v`

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

napari should resist version changes initiated by a plugin and should instead raise a warning to the user that such and such plugin is trying to downgrade napari and that to force install it—which may break your environment—please use the command line or make a fresh env.

## Environment

fresh conda-forge install of napari 0.4.16, python 3.9, macOS 12.6, arm64

## Additional context




Discussion:
Do you suggest that the plugin installer always adds `napari==<current version>` to the install command ? Because I guess a plugin could also downgrade numpy, or any other package. 
I suggest to use constraints file and generate it base on napari (and other plugins metadata.
One option would be to resist *any* version changes that installing a plugin tries to do. For pip the new `--dry-run` option (added in version 22.2: https://github.com/pypa/pip/pull/10771) might help here.
It was fixed on main, but need to be separately fixed on 0.4.18 release branch. 
@Czaki potentially this also should get bumped to 0.5?
(and therefore closed)

Resolving patch:
diff --git a/napari/_qt/dialogs/qt_plugin_dialog.py b/napari/_qt/dialogs/qt_plugin_dialog.py
--- a/napari/_qt/dialogs/qt_plugin_dialog.py
+++ b/napari/_qt/dialogs/qt_plugin_dialog.py
@@ -1,10 +1,12 @@
+import atexit
 import contextlib
 import os
 import sys
 from enum import Enum, auto
+from functools import lru_cache
 from importlib.metadata import PackageNotFoundError, metadata
 from pathlib import Path
-from tempfile import gettempdir
+from tempfile import gettempdir, mkstemp
 from typing import Callable, Dict, List, Literal, Optional, Sequence, Tuple
 
 from npe2 import PackageMetadata, PluginManager
@@ -39,6 +41,7 @@
 from superqt import QElidingLabel
 
 import napari.resources
+from napari import __version__
 from napari._qt.qt_resources import QColoredSVGIcon
 from napari._qt.qthreading import create_worker
 from napari._qt.widgets.qt_message_popup import WarnPopup
@@ -58,6 +61,23 @@
 InstallerTypes = Literal['pip', 'mamba']
 
 
+def _pip_constraints():
+    return [f"napari=={__version__}", "pydantic<2"]
+
+
+def _conda_constraints():
+    return [f"napari={__version__}", "pydantic<2.0a0"]
+
+
+@lru_cache(maxsize=3)
+def _create_constraints_file(constraints):
+    _, path = mkstemp("-napari-constraints.txt", text=True)
+    with open(path, "w") as f:
+        f.write("\n".join(constraints))
+    atexit.register(os.unlink, path)
+    return path
+
+
 # TODO: add error icon and handle pip install errors
 class Installer(QObject):
     started = Signal()
@@ -229,6 +249,10 @@ def _install(
                 cmd.extend(["-c", channel])
         else:
             cmd = ['-m', 'pip', 'install', '--upgrade']
+            cmd += [
+                "--constraint",
+                _create_constraints_file(_pip_constraints()),
+            ]
 
         if (
             running_as_bundled_app()


Test patch: