npe2 0.7.9rc0__py3-none-any.whl → 0.8.0rc0__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.
Files changed (42) hide show
  1. npe2/_command_registry.py +6 -5
  2. npe2/_dynamic_plugin.py +25 -27
  3. npe2/_inspection/_compile.py +9 -8
  4. npe2/_inspection/_fetch.py +18 -30
  5. npe2/_inspection/_from_npe1.py +26 -32
  6. npe2/_inspection/_setuputils.py +14 -14
  7. npe2/_inspection/_visitors.py +26 -21
  8. npe2/_plugin_manager.py +45 -57
  9. npe2/_pydantic_util.py +53 -0
  10. npe2/_pytest_plugin.py +3 -4
  11. npe2/_setuptools_plugin.py +9 -9
  12. npe2/cli.py +25 -21
  13. npe2/implements.py +13 -10
  14. npe2/implements.pyi +3 -2
  15. npe2/io_utils.py +40 -44
  16. npe2/manifest/_bases.py +15 -14
  17. npe2/manifest/_npe1_adapter.py +3 -3
  18. npe2/manifest/_package_metadata.py +40 -47
  19. npe2/manifest/contributions/_commands.py +16 -14
  20. npe2/manifest/contributions/_configuration.py +22 -20
  21. npe2/manifest/contributions/_contributions.py +13 -14
  22. npe2/manifest/contributions/_icon.py +3 -5
  23. npe2/manifest/contributions/_json_schema.py +86 -89
  24. npe2/manifest/contributions/_keybindings.py +5 -6
  25. npe2/manifest/contributions/_menus.py +11 -9
  26. npe2/manifest/contributions/_readers.py +10 -8
  27. npe2/manifest/contributions/_sample_data.py +16 -15
  28. npe2/manifest/contributions/_submenu.py +2 -4
  29. npe2/manifest/contributions/_themes.py +18 -22
  30. npe2/manifest/contributions/_widgets.py +6 -5
  31. npe2/manifest/contributions/_writers.py +22 -18
  32. npe2/manifest/schema.py +82 -70
  33. npe2/manifest/utils.py +24 -28
  34. npe2/plugin_manager.py +17 -14
  35. npe2/types.py +16 -19
  36. {npe2-0.7.9rc0.dist-info → npe2-0.8.0rc0.dist-info}/METADATA +13 -7
  37. npe2-0.8.0rc0.dist-info/RECORD +49 -0
  38. {npe2-0.7.9rc0.dist-info → npe2-0.8.0rc0.dist-info}/WHEEL +1 -1
  39. npe2/_pydantic_compat.py +0 -54
  40. npe2-0.7.9rc0.dist-info/RECORD +0 -49
  41. {npe2-0.7.9rc0.dist-info → npe2-0.8.0rc0.dist-info}/entry_points.txt +0 -0
  42. {npe2-0.7.9rc0.dist-info → npe2-0.8.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,11 @@
1
1
  import ast
2
2
  import inspect
3
3
  from abc import ABC, abstractmethod
4
+ from collections import defaultdict
4
5
  from importlib.metadata import Distribution
5
6
  from pathlib import Path
6
7
  from types import ModuleType
7
- from typing import TYPE_CHECKING, Any, DefaultDict, Dict, List, Tuple, Type, Union
8
+ from typing import TYPE_CHECKING, Any
8
9
 
9
10
  from npe2.manifest import contributions
10
11
 
@@ -12,7 +13,7 @@ if TYPE_CHECKING:
12
13
  from pydantic import BaseModel
13
14
 
14
15
 
15
- CONTRIB_MAP: Dict[str, Tuple[Type["BaseModel"], str]] = {
16
+ CONTRIB_MAP: dict[str, tuple[type["BaseModel"], str]] = {
16
17
  "writer": (contributions.WriterContribution, "writers"),
17
18
  "reader": (contributions.ReaderContribution, "readers"),
18
19
  "sample_data_generator": (contributions.SampleDataGenerator, "sample_data"),
@@ -43,7 +44,7 @@ class _DecoratorVisitor(ast.NodeVisitor, ABC):
43
44
  def __init__(self, module_name: str, match: str) -> None:
44
45
  self.module_name = module_name
45
46
  self._match = match
46
- self._names: Dict[str, str] = {}
47
+ self._names: dict[str, str] = {}
47
48
 
48
49
  def visit_Import(self, node: ast.Import) -> Any:
49
50
  # https://docs.python.org/3/library/ast.html#ast.Import
@@ -68,7 +69,7 @@ class _DecoratorVisitor(ast.NodeVisitor, ABC):
68
69
  def visit_ClassDef(self, node: ast.ClassDef) -> Any:
69
70
  self._find_decorators(node)
70
71
 
71
- def _find_decorators(self, node: Union[ast.ClassDef, ast.FunctionDef]):
72
+ def _find_decorators(self, node: ast.ClassDef | ast.FunctionDef):
72
73
  # for each in the decorator list ...
73
74
  for call in node.decorator_list:
74
75
  # https://docs.python.org/3/library/ast.html#ast.Call
@@ -116,14 +117,14 @@ class _DecoratorVisitor(ast.NodeVisitor, ABC):
116
117
  self._process_decorated(call.func.id, node, kwargs)
117
118
  return super().generic_visit(node)
118
119
 
119
- def _keywords_to_kwargs(self, keywords: List[ast.keyword]) -> Dict[str, Any]:
120
+ def _keywords_to_kwargs(self, keywords: list[ast.keyword]) -> dict[str, Any]:
120
121
  return {str(k.arg): ast.literal_eval(k.value) for k in keywords}
121
122
 
122
123
  @abstractmethod
123
124
  def _process_decorated(
124
125
  self,
125
126
  decorator_name: str,
126
- node: Union[ast.ClassDef, ast.FunctionDef],
127
+ node: ast.ClassDef | ast.FunctionDef,
127
128
  decorator_kwargs: dict,
128
129
  ):
129
130
  """Process a decorated function.
@@ -159,37 +160,37 @@ class NPE2PluginModuleVisitor(_DecoratorVisitor):
159
160
  ) -> None:
160
161
  super().__init__(module_name, match)
161
162
  self.plugin_name = plugin_name
162
- self.contribution_points: Dict[str, List[dict]] = {}
163
+ self.contribution_points: dict[str, list[dict]] = {}
163
164
 
164
165
  def _process_decorated(
165
166
  self,
166
167
  decorator_name: str,
167
- node: Union[ast.ClassDef, ast.FunctionDef],
168
+ node: ast.ClassDef | ast.FunctionDef,
168
169
  decorator_kwargs: dict,
169
170
  ):
170
171
  self._store_contrib(decorator_name, node.name, decorator_kwargs)
171
172
 
172
- def _store_contrib(self, contrib_type: str, name: str, kwargs: Dict[str, Any]):
173
+ def _store_contrib(self, contrib_type: str, name: str, kwargs: dict[str, Any]):
173
174
  from npe2.implements import CHECK_ARGS_PARAM # circ import
174
175
 
175
176
  kwargs.pop(CHECK_ARGS_PARAM, None)
176
177
  ContribClass, contrib_name = CONTRIB_MAP[contrib_type]
177
178
  contrib = ContribClass(**self._store_command(name, kwargs))
178
- existing: List[dict] = self.contribution_points.setdefault(contrib_name, [])
179
- existing.append(contrib.dict(exclude_unset=True))
179
+ existing: list[dict] = self.contribution_points.setdefault(contrib_name, [])
180
+ existing.append(contrib.model_dump(exclude_unset=True))
180
181
 
181
- def _store_command(self, name: str, kwargs: Dict[str, Any]) -> Dict[str, Any]:
182
+ def _store_command(self, name: str, kwargs: dict[str, Any]) -> dict[str, Any]:
182
183
  cmd_params = inspect.signature(contributions.CommandContribution).parameters
183
184
 
184
185
  cmd_kwargs = {k: kwargs.pop(k) for k in list(kwargs) if k in cmd_params}
185
186
  cmd_kwargs["python_name"] = self._qualified_pyname(name)
186
187
  cmd = contributions.CommandContribution(**cmd_kwargs)
187
188
  if cmd.id.startswith(self.plugin_name):
188
- n = len(self.plugin_name)
189
+ n = len(self.plugin_name) + 1
189
190
  cmd.id = cmd.id[n:]
190
191
  cmd.id = f"{self.plugin_name}.{cmd.id.lstrip('.')}"
191
- cmd_contribs: List[dict] = self.contribution_points.setdefault("commands", [])
192
- cmd_contribs.append(cmd.dict(exclude_unset=True))
192
+ cmd_contribs: list[dict] = self.contribution_points.setdefault("commands", [])
193
+ cmd_contribs.append(cmd.model_dump(exclude_unset=True))
193
194
  kwargs["command"] = cmd.id
194
195
  return kwargs
195
196
 
@@ -203,13 +204,13 @@ class NPE1PluginModuleVisitor(_DecoratorVisitor):
203
204
  def __init__(self, plugin_name: str, module_name: str) -> None:
204
205
  super().__init__(module_name, "napari_plugin_engine.napari_hook_implementation")
205
206
  self.plugin_name = plugin_name
206
- self.contribution_points: DefaultDict[str, list] = DefaultDict(list)
207
+ self.contribution_points: defaultdict[str, list] = defaultdict(list)
207
208
 
208
209
  def _process_decorated(
209
210
  self,
210
211
  decorator_name: str,
211
- node: Union[ast.ClassDef, ast.FunctionDef],
212
- decorator_kwargs: Dict[str, Any],
212
+ node: ast.ClassDef | ast.FunctionDef,
213
+ decorator_kwargs: dict[str, Any],
213
214
  ):
214
215
  self.generic_visit(node) # do this to process any imports in the function
215
216
  hookname = decorator_kwargs.get("specname", node.name)
@@ -270,7 +271,7 @@ class NPE1PluginModuleVisitor(_DecoratorVisitor):
270
271
  )
271
272
 
272
273
  contrib: contributions.SampleDataContribution
273
- for key, val in zip(return_.value.keys, return_.value.values):
274
+ for key, val in zip(return_.value.keys, return_.value.values, strict=True):
274
275
  if isinstance(val, ast.Dict):
275
276
  raise NotImplementedError("npe1 sample dicts-of-dicts not supported")
276
277
 
@@ -372,7 +373,7 @@ class NPE1PluginModuleVisitor(_DecoratorVisitor):
372
373
 
373
374
 
374
375
  def find_npe2_module_contributions(
375
- path: Union[ModuleType, str, Path], plugin_name: str, module_name: str = ""
376
+ path: ModuleType | str | Path, plugin_name: str, module_name: str = ""
376
377
  ) -> contributions.ContributionPoints:
377
378
  """Visit an npe2 module and extract contribution points.
378
379
 
@@ -401,7 +402,11 @@ def find_npe2_module_contributions(
401
402
  if "commands" in visitor.contribution_points:
402
403
  compress = {tuple(i.items()) for i in visitor.contribution_points["commands"]}
403
404
  visitor.contribution_points["commands"] = [dict(i) for i in compress]
404
- return contributions.ContributionPoints(**visitor.contribution_points)
405
+ res = contributions.ContributionPoints(**visitor.contribution_points)
406
+ for name in visitor.contribution_points:
407
+ for command in getattr(res, name):
408
+ command._plugin_name = plugin_name
409
+ return res
405
410
 
406
411
 
407
412
  def find_npe1_module_contributions(
npe2/_plugin_manager.py CHANGED
@@ -1,29 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import builtins
3
4
  import contextlib
4
5
  import os
5
6
  import warnings
6
7
  from collections import Counter, defaultdict
8
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Set
7
9
  from fnmatch import fnmatch
8
10
  from importlib import metadata
9
11
  from logging import getLogger
10
12
  from pathlib import Path
11
13
  from typing import (
12
14
  TYPE_CHECKING,
13
- AbstractSet,
14
15
  Any,
15
- Callable,
16
- DefaultDict,
17
- Dict,
18
- Iterable,
19
- Iterator,
20
- List,
21
- Mapping,
22
- Optional,
23
- Sequence,
24
- Set,
25
- Tuple,
26
- Union,
27
16
  )
28
17
  from urllib import parse
29
18
 
@@ -47,11 +36,11 @@ if TYPE_CHECKING:
47
36
  WidgetContribution,
48
37
  )
49
38
 
50
- IntStr = Union[int, str]
51
- AbstractSetIntStr = AbstractSet[IntStr]
52
- DictIntStrAny = Dict[IntStr, Any]
39
+ IntStr = int | str
40
+ SetIntStr = Set[IntStr]
41
+ DictIntStrAny = dict[IntStr, Any]
53
42
  MappingIntStrAny = Mapping[IntStr, Any]
54
- InclusionSet = Union[AbstractSetIntStr, MappingIntStrAny, None]
43
+ InclusionSet = SetIntStr | MappingIntStrAny | None
55
44
  DisposeFunction = Callable[[], None]
56
45
 
57
46
  logger = getLogger(__name__)
@@ -62,13 +51,13 @@ PluginName = str # this is `PluginManifest.name`
62
51
 
63
52
  class _ContributionsIndex:
64
53
  def __init__(self) -> None:
65
- self._indexed: Set[str] = set()
66
- self._commands: Dict[str, Tuple[CommandContribution, PluginName]] = {}
67
- self._readers: List[Tuple[str, ReaderContribution]] = []
68
- self._writers: List[Tuple[LayerType, int, int, WriterContribution]] = []
54
+ self._indexed: set[str] = set()
55
+ self._commands: dict[str, tuple[CommandContribution, PluginName]] = {}
56
+ self._readers: list[tuple[str, ReaderContribution]] = []
57
+ self._writers: list[tuple[LayerType, int, int, WriterContribution]] = []
69
58
 
70
59
  # DEPRECATED: only here for napari <= 0.4.15 compat.
71
- self._samples: DefaultDict[str, List[SampleDataContribution]] = DefaultDict(
60
+ self._samples: defaultdict[str, list[SampleDataContribution]] = defaultdict(
72
61
  list
73
62
  )
74
63
 
@@ -126,7 +115,7 @@ class _ContributionsIndex:
126
115
  def get_command(self, command_id: str) -> CommandContribution:
127
116
  return self._commands[command_id][0]
128
117
 
129
- def iter_compatible_readers(self, paths: List[str]) -> Iterator[ReaderContribution]:
118
+ def iter_compatible_readers(self, paths: list[str]) -> Iterator[ReaderContribution]:
130
119
  assert isinstance(paths, list)
131
120
  if not paths:
132
121
  return # pragma: no cover
@@ -170,7 +159,7 @@ class _ContributionsIndex:
170
159
  # this to get candidate writers compatible with the requested count.
171
160
  counts = Counter(layer_types)
172
161
 
173
- def _get_candidates(lt: LayerType) -> Set[WriterContribution]:
162
+ def _get_candidates(lt: LayerType) -> set[WriterContribution]:
174
163
  return {
175
164
  w
176
165
  for layer, min_, max_, w in self._writers
@@ -185,7 +174,7 @@ class _ContributionsIndex:
185
174
  else:
186
175
  break
187
176
 
188
- def _writer_key(writer: WriterContribution) -> Tuple[bool, int]:
177
+ def _writer_key(writer: WriterContribution) -> tuple[bool, int]:
189
178
  # 1. writers with no file extensions (like directory writers) go last
190
179
  no_ext = len(writer.filename_extensions) == 0
191
180
 
@@ -222,21 +211,21 @@ class PluginManagerEvents(SignalGroup):
222
211
 
223
212
 
224
213
  class PluginManager:
225
- __instance: Optional[PluginManager] = None # a global instance
214
+ __instance: PluginManager | None = None # a global instance
226
215
  _contrib: _ContributionsIndex
227
216
  events: PluginManagerEvents
228
217
 
229
218
  def __init__(
230
- self, *, disable: Iterable[str] = (), reg: Optional[CommandRegistry] = None
219
+ self, *, disable: Iterable[str] = (), reg: CommandRegistry | None = None
231
220
  ) -> None:
232
- self._disabled_plugins: Set[PluginName] = set(disable)
221
+ self._disabled_plugins: set[PluginName] = set(disable)
233
222
  self._command_registry = reg or CommandRegistry()
234
- self._contexts: Dict[PluginName, PluginContext] = {}
223
+ self._contexts: dict[PluginName, PluginContext] = {}
235
224
  self._contrib = _ContributionsIndex()
236
- self._manifests: Dict[PluginName, PluginManifest] = {}
225
+ self._manifests: dict[PluginName, PluginManifest] = {}
237
226
  self.events = PluginManagerEvents(self)
238
- self._npe1_adapters: List[NPE1Adapter] = []
239
- self._command_menu_map: Dict[str, Dict[str, Dict[str, List[MenuCommand]]]] = (
227
+ self._npe1_adapters: list[NPE1Adapter] = []
228
+ self._command_menu_map: dict[str, dict[str, dict[str, list[MenuCommand]]]] = (
240
229
  # for each manifest, maps command IDs to menu IDs to list of MenuCommands
241
230
  # belonging to that menu
242
231
  # i.e. manifest -> command -> menu_id -> list[MenuCommand]
@@ -323,13 +312,13 @@ class PluginManager:
323
312
  self._contrib.index_contributions(self._npe1_adapters.pop())
324
313
 
325
314
  def register(
326
- self, manifest_or_package: Union[PluginManifest, str], warn_disabled=True
315
+ self, manifest_or_package: PluginManifest | str, warn_disabled=True
327
316
  ) -> None:
328
317
  """Register a plugin manifest, path to manifest file, or a package name.
329
318
 
330
319
  Parameters
331
320
  ----------
332
- manifest_or_package : Union[PluginManifest, str]
321
+ manifest_or_package : PluginManifest | str
333
322
  Either a PluginManifest instance or a string. If a string, should be either
334
323
  the name of a plugin package, or a path to a plugin manifest file.
335
324
  warn_disabled : bool, optional
@@ -507,9 +496,7 @@ class PluginManager:
507
496
  raise KeyError(msg)
508
497
  return self._manifests[key]
509
498
 
510
- def iter_manifests(
511
- self, disabled: Optional[bool] = None
512
- ) -> Iterator[PluginManifest]:
499
+ def iter_manifests(self, disabled: bool | None = None) -> Iterator[PluginManifest]:
513
500
  """Iterate through registered manifests.
514
501
 
515
502
  Parameters
@@ -532,12 +519,12 @@ class PluginManager:
532
519
  def dict(
533
520
  self,
534
521
  *,
535
- include: Optional[InclusionSet] = None,
536
- exclude: Optional[InclusionSet] = None,
537
- ) -> Dict[str, Any]:
522
+ include: InclusionSet | None = None,
523
+ exclude: InclusionSet | None = None,
524
+ ) -> builtins.dict[str, Any]:
538
525
  """Return a dictionary with the state of the plugin manager.
539
526
 
540
- `include` and `exclude` will be passed to each `PluginManifest.dict()`
527
+ `include` and `exclude` will be passed to each `PluginManifest.model_dump()`
541
528
  See pydantic documentation for details:
542
529
  https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict
543
530
 
@@ -568,15 +555,16 @@ class PluginManager:
568
555
  Dict[str, Any]
569
556
  Dictionary with the state of the plugin manager. Keys will include
570
557
 
571
- - `'plugins'`: dict of `{name: manifest.dict()} for discovered plugins
558
+ - `'plugins'`: dict of `{name: manifest.model_dump()} for
559
+ discovered plugins
572
560
  - `'disabled'`: set of disabled plugins
573
561
  - `'activated'`: set of activated plugins
574
562
 
575
563
  """
576
564
  # _include =
577
- out: Dict[str, Any] = {
565
+ out: dict[str, Any] = {
578
566
  "plugins": {
579
- mf.name: mf.dict(
567
+ mf.name: mf.model_dump(
580
568
  include=_expand_dotted_set(include),
581
569
  exclude=_expand_dotted_set(exclude),
582
570
  )
@@ -619,9 +607,9 @@ class PluginManager:
619
607
  for mf in self.iter_manifests(disabled=disabled):
620
608
  yield from mf.contributions.menus.get(menu_key, ())
621
609
 
622
- def menus(self, disabled=False) -> Dict[str, List[MenuItem]]:
610
+ def menus(self, disabled=False) -> builtins.dict[str, list[MenuItem]]:
623
611
  """Return all registered menu_key -> List[MenuItems]."""
624
- _menus: DefaultDict[str, List[MenuItem]] = DefaultDict(list)
612
+ _menus: defaultdict[str, list[MenuItem]] = defaultdict(list)
625
613
  for mf in self.iter_manifests(disabled=disabled):
626
614
  for key, menus in mf.contributions.menus.items():
627
615
  _menus[key].extend(menus)
@@ -633,13 +621,13 @@ class PluginManager:
633
621
  yield from mf.contributions.themes or ()
634
622
 
635
623
  def iter_compatible_readers(
636
- self, path: Union[PathLike, Sequence[str]]
624
+ self, path: PathLike | Sequence[str]
637
625
  ) -> Iterator[ReaderContribution]:
638
626
  """Iterate over ReaderContributions compatible with `path`.
639
627
 
640
628
  Parameters
641
629
  ----------
642
- path : Union[PathLike, Sequence[str]]
630
+ path : PathLike | Sequence[str]
643
631
  Pathlike or list of pathlikes, with file(s) to read.
644
632
  """
645
633
  if isinstance(path, (str, Path)):
@@ -666,15 +654,15 @@ class PluginManager:
666
654
 
667
655
  def iter_sample_data(
668
656
  self,
669
- ) -> Iterator[Tuple[PluginName, List[SampleDataContribution]]]:
657
+ ) -> Iterator[tuple[PluginName, list[SampleDataContribution]]]:
670
658
  """Iterates over (plugin_name, [sample_contribs])."""
671
659
  for mf in self.iter_manifests(disabled=False):
672
660
  if mf.contributions.sample_data:
673
661
  yield mf.name, mf.contributions.sample_data
674
662
 
675
663
  def get_writer(
676
- self, path: str, layer_types: Sequence[str], plugin_name: Optional[str] = None
677
- ) -> Tuple[Optional[WriterContribution], str]:
664
+ self, path: str, layer_types: Sequence[str], plugin_name: str | None = None
665
+ ) -> tuple[WriterContribution | None, str]:
678
666
  """Get Writer contribution appropriate for `path`, and `layer_types`.
679
667
 
680
668
  When `path` has a file extension, find a compatible writer that has
@@ -715,7 +703,7 @@ class PluginManager:
715
703
  # Nothing got found
716
704
  return None, path
717
705
 
718
- def get_shimmed_plugins(self) -> List[str]:
706
+ def get_shimmed_plugins(self) -> list[str]:
719
707
  """Return a list of all shimmed plugin names."""
720
708
  return [mf.name for mf in self.iter_manifests() if mf.npe1_shim]
721
709
 
@@ -726,14 +714,14 @@ class PluginContext:
726
714
  # stores all created contexts (currently cleared by `PluginManager.deactivate`)
727
715
 
728
716
  def __init__(
729
- self, plugin_key: PluginName, reg: Optional[CommandRegistry] = None
717
+ self, plugin_key: PluginName, reg: CommandRegistry | None = None
730
718
  ) -> None:
731
719
  self._activated = False
732
720
  self.plugin_key = plugin_key
733
721
  self._command_registry = reg or PluginManager.instance().commands
734
- self._imports: Set[str] = set() # modules that were imported by this plugin
722
+ self._imports: set[str] = set() # modules that were imported by this plugin
735
723
  # functions to call when deactivating
736
- self._disposables: Set[DisposeFunction] = set()
724
+ self._disposables: set[DisposeFunction] = set()
737
725
 
738
726
  def _dispose(self):
739
727
  while self._disposables:
@@ -742,7 +730,7 @@ class PluginContext:
742
730
  except Exception as e:
743
731
  logger.warning(f"Error while disposing {self.plugin_key}; {e}")
744
732
 
745
- def register_command(self, id: str, command: Optional[Callable] = None):
733
+ def register_command(self, id: str, command: Callable | None = None):
746
734
  """Associate a callable with a command id."""
747
735
 
748
736
  def _inner(command):
@@ -787,7 +775,7 @@ def _expand_dotted_set(inclusion_set: InclusionSet) -> InclusionSet:
787
775
  ):
788
776
  return inclusion_set
789
777
 
790
- result: Dict[IntStr, Any] = {}
778
+ result: dict[IntStr, Any] = {}
791
779
  # sort the strings based on the number of dots,
792
780
  # so that higher level keys take precedence
793
781
  # e.g. {'a.b', 'a.d.e', 'a'} -> {'a'}
npe2/_pydantic_util.py ADDED
@@ -0,0 +1,53 @@
1
+ from types import NoneType, UnionType
2
+ from typing import Annotated, Dict, List, Union, get_args, get_origin # noqa
3
+
4
+
5
+ def iter_inner_types(type_):
6
+ origin = get_origin(type_)
7
+ args = get_args(type_)
8
+ if origin in (list, List): # noqa
9
+ yield from iter_inner_types(args[0])
10
+ elif origin in (dict, Dict): # noqa
11
+ yield from iter_inner_types(args[1])
12
+ elif origin is Annotated:
13
+ yield from iter_inner_types(args[0])
14
+ elif origin in (UnionType, Union):
15
+ for arg in args:
16
+ yield from iter_inner_types(arg)
17
+ elif type_ is not NoneType:
18
+ yield type_
19
+
20
+
21
+ def get_inner_type(type_):
22
+ """Roughly replacing pydantic.v1 Field.type_"""
23
+ return Union[tuple(iter_inner_types(type_))] # noqa
24
+
25
+
26
+ def get_outer_type(type_):
27
+ """Roughly replacing pydantic.v1 Field.outer_type_"""
28
+ origin = get_origin(type_)
29
+ args = get_args(type_)
30
+ if origin in (UnionType, Union):
31
+ # filter args to remove optional None
32
+ args = tuple(filter(lambda t: t is not NoneType, get_args(type_)))
33
+ if len(args) == 1:
34
+ # it was just optional, pretend there was no None
35
+ return get_outer_type(args[0])
36
+ # It's an actual union of types, so there's no "outer type"
37
+ return None
38
+ if origin is not None:
39
+ return origin
40
+ return type_
41
+
42
+
43
+ def is_list_type(type_):
44
+ """Roughly replacing pydantic.v1 comparison to SHAPE_LIST"""
45
+ return get_outer_type(type_) is list
46
+
47
+
48
+ __all__ = (
49
+ "get_inner_type",
50
+ "get_outer_type",
51
+ "is_list_type",
52
+ "iter_inner_types",
53
+ )
npe2/_pytest_plugin.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import warnings
3
- from typing import Optional, Union
4
3
  from unittest.mock import patch
5
4
 
6
5
  import pytest
@@ -23,9 +22,9 @@ class TestPluginManager(PluginManager):
23
22
 
24
23
  def tmp_plugin(
25
24
  self,
26
- manifest: Optional[Union[PluginManifest, str]] = None,
27
- package: Optional[str] = None,
28
- name: Optional[str] = None,
25
+ manifest: PluginManifest | str | None = None,
26
+ package: str | None = None,
27
+ name: str | None = None,
29
28
  ) -> DynamicPlugin:
30
29
  """Create a DynamicPlugin instance using this plugin manager.
31
30
 
@@ -13,16 +13,16 @@ import os
13
13
  import re
14
14
  import sys
15
15
  import warnings
16
- from typing import TYPE_CHECKING, Optional, Tuple, cast
16
+ from typing import TYPE_CHECKING, cast
17
17
 
18
18
  from setuptools import Distribution
19
19
  from setuptools.command.build_py import build_py
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from distutils.cmd import Command
23
- from typing import Any, Union
23
+ from typing import Any
24
24
 
25
- PathT = Union["os.PathLike[str]", str]
25
+ PathT = "os.PathLike[str]" | str
26
26
 
27
27
  NPE2_ENTRY = "napari.manifest"
28
28
  DEBUG = bool(os.environ.get("SETUPTOOLS_NPE2_DEBUG"))
@@ -54,7 +54,7 @@ def _read_dist_name_from_setup_cfg() -> str | None:
54
54
  return parser.get("metadata", "name", fallback=None)
55
55
 
56
56
 
57
- def _check_absolute_root(root: PathT, relative_to: PathT | None) -> str:
57
+ def _check_absolute_root(root: PathT, relative_to: PathT | None) -> str: # type: ignore
58
58
  trace("abs root", repr(locals()))
59
59
  if relative_to:
60
60
  if (
@@ -85,9 +85,9 @@ class Configuration:
85
85
 
86
86
  def __init__(
87
87
  self,
88
- relative_to: PathT | None = None,
89
- root: PathT = ".",
90
- write_to: PathT | None = None,
88
+ relative_to: PathT | None = None, # type: ignore
89
+ root: PathT = ".", # type: ignore
90
+ write_to: PathT | None = None, # type: ignore
91
91
  write_to_template: str | None = None,
92
92
  dist_name: str | None = None,
93
93
  template: str | None = None,
@@ -109,7 +109,7 @@ class Configuration:
109
109
  return self._root
110
110
 
111
111
  @root.setter
112
- def root(self, value: PathT) -> None:
112
+ def root(self, value: PathT) -> None: # type: ignore
113
113
  self._absolute_root = _check_absolute_root(value, self._relative_to)
114
114
  self._root = os.fspath(value)
115
115
  trace("root", repr(self._absolute_root))
@@ -152,7 +152,7 @@ class Configuration:
152
152
  return cls(dist_name=dist_name, **section, **kwargs)
153
153
 
154
154
 
155
- def _mf_entry_from_dist(dist: Distribution) -> Optional[Tuple[str, str]]:
155
+ def _mf_entry_from_dist(dist: Distribution) -> tuple[str, str] | None:
156
156
  """Return (module, attr) for a distribution's npe2 entry point."""
157
157
  eps: dict = getattr(dist, "entry_points", {})
158
158
  if napari_entrys := eps.get(NPE2_ENTRY, []):