npe2 0.7.9__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.9.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.9.dist-info → npe2-0.8.0rc0.dist-info}/WHEEL +1 -1
  39. npe2/_pydantic_compat.py +0 -54
  40. npe2-0.7.9.dist-info/RECORD +0 -49
  41. {npe2-0.7.9.dist-info → npe2-0.8.0rc0.dist-info}/entry_points.txt +0 -0
  42. {npe2-0.7.9.dist-info → npe2-0.8.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -1,38 +1,32 @@
1
1
  from importlib import metadata
2
- from typing import Dict, List, Literal, Optional, Union
2
+ from typing import Literal
3
3
 
4
- from npe2._pydantic_compat import (
5
- SHAPE_LIST,
6
- BaseModel,
7
- Extra,
8
- Field,
9
- constr,
10
- root_validator,
11
- )
4
+ from pydantic import BaseModel, ConfigDict, Field, constr, model_validator
5
+
6
+ from npe2._pydantic_util import is_list_type
12
7
 
13
8
  # https://packaging.python.org/specifications/core-metadata/
14
9
 
15
10
  MetadataVersion = Literal["1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3"]
16
11
  _alphanum = "[a-zA-Z0-9]"
17
- PackageName = constr(regex=f"^{_alphanum}[a-zA-Z0-9._-]*{_alphanum}$")
12
+ PackageName = constr(pattern=f"^{_alphanum}[a-zA-Z0-9._-]*{_alphanum}$")
18
13
 
19
14
 
20
15
  class PackageMetadata(BaseModel):
21
16
  """Pydantic model for standard python package metadata.
22
17
 
23
18
  https://www.python.org/dev/peps/pep-0566/
24
- https://packaging.python.org/specifications/core-metadata/
19
+ https://packaging.python.org/pattern/core-metadata/
25
20
 
26
21
  The `importlib.metadata` provides the `metadata()` function,
27
22
  but it returns a somewhat awkward `email.message.Message` object.
28
23
  """
29
24
 
30
- class Config:
31
- extra = Extra.ignore
25
+ model_config = ConfigDict(extra="ignore")
32
26
 
33
27
  # allow str as a fallback in case the metata-version specification has been
34
28
  # updated and we haven't updated the code yet
35
- metadata_version: Union[MetadataVersion, str] = Field(
29
+ metadata_version: MetadataVersion | str = Field(
36
30
  "1.0", description="Version of the file format"
37
31
  )
38
32
  name: PackageName = Field( # type: ignore
@@ -48,34 +42,34 @@ class PackageMetadata(BaseModel):
48
42
  description="A string containing the distribution's version number. "
49
43
  "This field must be in the format specified in PEP 440.",
50
44
  )
51
- dynamic: Optional[List[str]] = Field(
45
+ dynamic: list[str] | None = Field(
52
46
  None,
53
47
  description="A string containing the name of another core metadata "
54
48
  "field. The field names Name and Version may not be specified in this field.",
55
49
  min_ver="2.2",
56
50
  )
57
- platform: Optional[List[str]] = Field(
51
+ platform: list[str] | None = Field(
58
52
  None,
59
53
  description="A Platform specification describing an operating system "
60
54
  "supported by the distribution which is not listed in the “Operating System” "
61
55
  "Trove classifiers. See “Classifier” below.",
62
56
  )
63
- supported_platform: Optional[List[str]] = Field(
57
+ supported_platform: list[str] | None = Field(
64
58
  None,
65
59
  description="Binary distributions containing a PKG-INFO file will use the "
66
60
  "Supported-Platform field in their metadata to specify the OS and CPU for "
67
61
  "which the binary distribution was compiled",
68
62
  min_ver="1.1",
69
63
  )
70
- summary: Optional[str] = Field(
64
+ summary: str | None = Field(
71
65
  None, description="A one-line summary of what the distribution does."
72
66
  )
73
- description: Optional[str] = Field(
67
+ description: str | None = Field(
74
68
  None,
75
69
  description="A longer description of the distribution that can "
76
70
  "run to several paragraphs.",
77
71
  )
78
- description_content_type: Optional[str] = Field(
72
+ description_content_type: str | None = Field(
79
73
  None,
80
74
  description="A string stating the markup syntax (if any) used in the "
81
75
  "distribution's description, so that tools can intelligently render the "
@@ -83,45 +77,45 @@ class PackageMetadata(BaseModel):
83
77
  "text/plain, text/x-rst, text/markdown",
84
78
  min_ver="2.1",
85
79
  )
86
- keywords: Optional[str] = Field(
80
+ keywords: str | None = Field(
87
81
  None,
88
82
  description="A list of additional keywords, separated by commas, to be used "
89
83
  "to assist searching for the distribution in a larger catalog.",
90
84
  )
91
- home_page: Optional[str] = Field(
85
+ home_page: str | None = Field(
92
86
  None,
93
87
  description="A string containing the URL for the distribution's home page.",
94
88
  )
95
- download_url: Optional[str] = Field(
89
+ download_url: str | None = Field(
96
90
  None,
97
91
  description="A string containing the URL from which THIS version of the "
98
92
  "distribution can be downloaded.",
99
93
  min_ver="1.1",
100
94
  )
101
- author: Optional[str] = Field(
95
+ author: str | None = Field(
102
96
  None,
103
97
  description="A string containing the author's name at a minimum; "
104
98
  "additional contact information may be provided.",
105
99
  )
106
- author_email: Optional[str] = Field(
100
+ author_email: str | None = Field(
107
101
  None,
108
102
  description="A string containing the author's e-mail address. It can contain "
109
103
  "a name and e-mail address in the legal forms for a RFC-822 From: header.",
110
104
  )
111
- maintainer: Optional[str] = Field(
105
+ maintainer: str | None = Field(
112
106
  None,
113
107
  description="A string containing the maintainer's name at a minimum; "
114
108
  "additional contact information may be provided.",
115
109
  min_ver="1.2",
116
110
  )
117
- maintainer_email: Optional[str] = Field(
111
+ maintainer_email: str | None = Field(
118
112
  None,
119
113
  description="A string containing the maintainer's e-mail address. It can "
120
114
  "contain a name and e-mail address in the legal forms for a "
121
115
  "RFC-822 From: header.",
122
116
  min_ver="1.2",
123
117
  )
124
- license: Optional[str] = Field(
118
+ license: str | None = Field(
125
119
  None,
126
120
  description="Text indicating the license covering the distribution where the "
127
121
  "license is not a selection from the “License” Trove classifiers. See "
@@ -129,21 +123,21 @@ class PackageMetadata(BaseModel):
129
123
  "version of a license which is named via the Classifier field, or to "
130
124
  "indicate a variation or exception to such a license.",
131
125
  )
132
- classifier: Optional[List[str]] = Field(
126
+ classifier: list[str] | None = Field(
133
127
  None,
134
128
  description="Each entry is a string giving a single classification value for "
135
129
  "the distribution. Classifiers are described in PEP 301, and the Python "
136
130
  "Package Index publishes a dynamic list of currently defined classifiers.",
137
131
  min_ver="1.1",
138
132
  )
139
- requires_dist: Optional[List[str]] = Field(
133
+ requires_dist: list[str] | None = Field(
140
134
  None,
141
135
  description="The field format specification was relaxed to accept the syntax "
142
136
  "used by popular publishing tools. Each entry contains a string naming some "
143
137
  "other distutils project required by this distribution.",
144
138
  min_ver="1.2",
145
139
  )
146
- requires_python: Optional[str] = Field(
140
+ requires_python: str | None = Field(
147
141
  None,
148
142
  description="This field specifies the Python version(s) that the distribution "
149
143
  "is guaranteed to be compatible with. Installation tools may look at this "
@@ -151,7 +145,7 @@ class PackageMetadata(BaseModel):
151
145
  "The value must be in the format specified in Version specifiers (PEP 440).",
152
146
  min_ver="1.2",
153
147
  )
154
- requires_external: Optional[List[str]] = Field(
148
+ requires_external: list[str] | None = Field(
155
149
  None,
156
150
  description="The field format specification was relaxed to accept the syntax "
157
151
  "used by popular publishing tools. Each entry contains a string describing "
@@ -160,13 +154,13 @@ class PackageMetadata(BaseModel):
160
154
  "has no semantics which are meaningful to the distutils distribution.",
161
155
  min_ver="1.2",
162
156
  )
163
- project_url: Optional[List[str]] = Field(
157
+ project_url: list[str] | None = Field(
164
158
  None,
165
159
  description="A string containing a browsable URL for the project and a label "
166
160
  "for it, separated by a comma.",
167
161
  min_ver="1.2",
168
162
  )
169
- provides_extra: Optional[List[str]] = Field(
163
+ provides_extra: list[str] | None = Field(
170
164
  None,
171
165
  description="A string containing the name of an optional feature. Must be a "
172
166
  "valid Python identifier. May be used to make a dependency conditional on "
@@ -175,19 +169,18 @@ class PackageMetadata(BaseModel):
175
169
  )
176
170
 
177
171
  # rarely_used
178
- provides_dist: Optional[List[str]] = Field(None, min_ver="1.2")
179
- obsoletes_dist: Optional[List[str]] = Field(None, min_ver="1.2")
172
+ provides_dist: list[str] | None = Field(None, min_ver="1.2")
173
+ obsoletes_dist: list[str] | None = Field(None, min_ver="1.2")
180
174
 
181
- @root_validator(pre=True)
175
+ @model_validator(mode="before")
176
+ @classmethod
182
177
  def _validate_root(cls, values):
183
178
  if "metadata_version" not in values:
184
- fields = cls.__fields__
185
- mins = {
186
- fields[n].field_info.extra.get("min_ver", "1.0")
187
- for n in values
188
- if n in fields
189
- }
190
- values["metadata_version"] = str(max(float(x) for x in mins))
179
+ min_vers = {"1.0"}
180
+ for n, info in cls.model_fields.items():
181
+ if n in values and info.json_schema_extra is not None:
182
+ min_vers.add(info.json_schema_extra.get("min_ver", "1.0"))
183
+ values["metadata_version"] = str(max(float(x) for x in min_vers))
191
184
  return values
192
185
 
193
186
  @classmethod
@@ -200,8 +193,8 @@ class PackageMetadata(BaseModel):
200
193
  @classmethod
201
194
  def from_dist_metadata(cls, meta: "metadata.PackageMetadata") -> "PackageMetadata":
202
195
  """Generate PackageMetadata from importlib.metadata.PackageMetdata."""
203
- manys = [f.name for f in cls.__fields__.values() if f.shape == SHAPE_LIST]
204
- d: Dict[str, Union[str, List[str]]] = {}
196
+ manys = [n for n, f in cls.model_fields.items() if is_list_type(f.annotation)]
197
+ d: dict[str, str | list[str]] = {}
205
198
  # looks like py3.10 changed the public protocol of metadata.PackageMetadata
206
199
  # and they don't want you to rely on the Mapping interface... however, the
207
200
  # __iter__ method doesn't iterate key value pairs, just keys, and I can't figure
@@ -213,7 +206,7 @@ class PackageMetadata(BaseModel):
213
206
  d.setdefault(key, []).append(value) # type: ignore
214
207
  else:
215
208
  d[key] = value
216
- return cls.parse_obj(d)
209
+ return cls.model_validate(d)
217
210
 
218
211
  def __hash__(self) -> int:
219
212
  return id(self)
@@ -1,6 +1,9 @@
1
- from typing import TYPE_CHECKING, Any, Optional, Union
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Annotated, Any
4
+
5
+ from pydantic import AfterValidator, BaseModel, ConfigDict, Field
2
6
 
3
- from npe2._pydantic_compat import BaseModel, Extra, Field, validator
4
7
  from npe2.manifest import _validators
5
8
  from npe2.types import PythonName
6
9
 
@@ -33,7 +36,7 @@ class CommandContribution(BaseModel):
33
36
  ```
34
37
  """
35
38
 
36
- id: str = Field(
39
+ id: Annotated[str, AfterValidator(_validators.command_id)] = Field(
37
40
  ...,
38
41
  description="A unique identifier used to reference this command. While this may"
39
42
  " look like a python fully qualified name this does *not* refer to a python "
@@ -41,7 +44,6 @@ class CommandContribution(BaseModel):
41
44
  "the name of the package, and include only alphanumeric characters, plus "
42
45
  "dashes and underscores.",
43
46
  )
44
- _valid_id = validator("id", allow_reuse=True)(_validators.command_id)
45
47
 
46
48
  title: str = Field(
47
49
  ...,
@@ -49,26 +51,27 @@ class CommandContribution(BaseModel):
49
51
  "for example, when searching in a command palette. Examples: 'Generate lily "
50
52
  "sample', 'Read tiff image', 'Open gaussian blur widget'. ",
51
53
  )
52
- python_name: Optional[PythonName] = Field(
54
+ python_name: (
55
+ Annotated[PythonName, AfterValidator(_validators.python_name)] | None
56
+ ) = Field(
53
57
  None,
54
58
  description="Fully qualified name to a callable python object "
55
59
  "implementing this command. This usually takes the form of "
56
60
  "`{obj.__module__}:{obj.__qualname__}` "
57
61
  "(e.g. `my_package.a_module:some_function`)",
58
62
  )
59
- _valid_pyname = validator("python_name", allow_reuse=True)(_validators.python_name)
60
63
 
61
- short_title: Optional[str] = Field(
64
+ short_title: str | None = Field(
62
65
  None,
63
66
  description="Short title by which the command is represented in "
64
67
  "the UI. Menus pick either `title` or `short_title` depending on the context "
65
68
  "in which they show commands.",
66
69
  )
67
- category: Optional[str] = Field(
70
+ category: str | None = Field(
68
71
  None,
69
72
  description="Category string by which the command may be grouped in the UI.",
70
73
  )
71
- icon: Optional[Union[str, Icon]] = Field(
74
+ icon: str | Icon | None = Field(
72
75
  None,
73
76
  description="Icon used to represent this command in the UI, on "
74
77
  "buttons or in menus. These may be [superqt](https://github.com/napari/superqt)"
@@ -76,7 +79,7 @@ class CommandContribution(BaseModel):
76
79
  "expected to depend on any fonticon libraries they use, e.g "
77
80
  "[fonticon-fontawesome6](https://github.com/tlambert03/fonticon-fontawesome6).",
78
81
  )
79
- enablement: Optional[str] = Field(
82
+ enablement: str | None = Field(
80
83
  None,
81
84
  description=(
82
85
  "Expression which must evaluate as true to enable the command in the UI "
@@ -85,14 +88,13 @@ class CommandContribution(BaseModel):
85
88
  ),
86
89
  )
87
90
 
88
- class Config:
89
- extra = Extra.forbid
91
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
90
92
 
91
93
  def exec(
92
94
  self,
93
95
  args: tuple = (),
94
- kwargs: Optional[dict] = None,
95
- _registry: Optional["CommandRegistry"] = None,
96
+ kwargs: dict | None = None,
97
+ _registry: CommandRegistry | None = None,
96
98
  ) -> Any:
97
99
  if kwargs is None:
98
100
  kwargs = {}
@@ -1,6 +1,6 @@
1
- from typing import Any, Dict, List, Literal, Optional, Union
1
+ from typing import Annotated, Any, Literal
2
2
 
3
- from npe2._pydantic_compat import BaseModel, Field, conlist, root_validator, validator
3
+ from pydantic import BaseModel, BeforeValidator, Field, conlist, model_validator
4
4
 
5
5
  from ._json_schema import (
6
6
  Draft07JsonSchema,
@@ -18,21 +18,23 @@ class ConfigurationProperty(Draft07JsonSchema):
18
18
  https://json-schema.org/understanding-json-schema/reference/index.html
19
19
  """
20
20
 
21
- type: Union[JsonType, JsonTypeArray] = Field(
22
- None,
23
- description="The type of this variable. Either JSON Schema type names ('array',"
24
- " 'boolean', 'object', ...) or python type names ('list', 'bool', 'dict', ...) "
25
- "may be used, but they will be coerced to JSON Schema types. Numbers, strings, "
26
- "and booleans will be editable in the UI, other types (lists, dicts) *may* be "
27
- "editable in the UI depending on their content, but maby will only be editable "
28
- "as text in the napari settings file. For boolean entries, the description "
29
- "(or markdownDescription) will be used as the label for the checkbox.",
21
+ type: Annotated[JsonType | JsonTypeArray, BeforeValidator(_coerce_type_name)] = (
22
+ Field(
23
+ None,
24
+ description="The type of this variable. Either JSON Schema type names "
25
+ "('array', 'boolean', 'object', ...) or python type names ('list', 'bool', "
26
+ "'dict', ...) may be used, but they will be coerced to JSON Schema types. "
27
+ "Numbers, strings, and booleans will be editable in the UI, other types "
28
+ "(lists, dicts) *may* be editable in the UI depending on their content, "
29
+ "but maby will only be editable as text in the napari settings file. For "
30
+ "boolean entries, the description (or markdownDescription) will be used as"
31
+ " the label for the checkbox.",
32
+ )
30
33
  )
31
- _coerce_type_name = validator("type", pre=True, allow_reuse=True)(_coerce_type_name)
32
34
 
33
35
  default: Any = Field(None, description="The default value for this property.")
34
36
 
35
- description: Optional[str] = Field(
37
+ description: str | None = Field(
36
38
  None,
37
39
  description="Your `description` appears after the title and before the input "
38
40
  "field, except for booleans, where the description is used as the label for "
@@ -45,12 +47,12 @@ class ConfigurationProperty(Draft07JsonSchema):
45
47
  "plain text, set this value to `plain`.",
46
48
  )
47
49
 
48
- enum: Optional[conlist(Any, min_items=1, unique_items=True)] = Field( # type: ignore
50
+ enum: conlist(Any, min_length=1) | None = Field( # type: ignore
49
51
  None,
50
52
  description="A list of valid options for this field. If you provide this field,"
51
53
  "the settings UI will render a dropdown menu.",
52
54
  )
53
- enum_descriptions: List[str] = Field(
55
+ enum_descriptions: list[str] = Field(
54
56
  default_factory=list,
55
57
  description="If you provide a list of items under the `enum` field, you may "
56
58
  "provide `enum_descriptions` to add descriptive text for each enum.",
@@ -62,7 +64,7 @@ class ConfigurationProperty(Draft07JsonSchema):
62
64
  "plain text, set this value to `plain`.",
63
65
  )
64
66
 
65
- deprecation_message: Optional[str] = Field(
67
+ deprecation_message: str | None = Field(
66
68
  None,
67
69
  description="If you set deprecationMessage, the setting will get a warning "
68
70
  "underline with your specified message. It won't show up in the settings "
@@ -80,7 +82,7 @@ class ConfigurationProperty(Draft07JsonSchema):
80
82
  description="By default, string settings will be rendered with a single-line "
81
83
  "editor. To render with a multi-line editor, set this value to `multiline`.",
82
84
  )
83
- order: Optional[int] = Field(
85
+ order: int | None = Field(
84
86
  None,
85
87
  description="When specified, gives the order of this setting relative to other "
86
88
  "settings within the same category. Settings with an order property will be "
@@ -88,14 +90,14 @@ class ConfigurationProperty(Draft07JsonSchema):
88
90
  " will be placed in alphabetical order.",
89
91
  )
90
92
 
91
- pattern_error_message: Optional[str] = Field(
93
+ pattern_error_message: str | None = Field(
92
94
  None,
93
95
  description="When restricting string types to a given regular expression with "
94
96
  "the `pattern` field, this field may be used to provide a custom error when "
95
97
  "the pattern does not match.",
96
98
  )
97
99
 
98
- @root_validator(pre=True)
100
+ @model_validator(mode="before")
99
101
  def _validate_root(cls, values):
100
102
  values = super()._validate_root(values)
101
103
 
@@ -135,7 +137,7 @@ class ConfigurationContribution(BaseModel):
135
137
  '"Plugin", "Configuration", and "Settings" are redundant and should not be'
136
138
  "used in your title.",
137
139
  )
138
- properties: Dict[str, ConfigurationProperty] = Field(
140
+ properties: dict[str, ConfigurationProperty] = Field(
139
141
  ...,
140
142
  description="Configuration properties. In the settings UI, your configuration "
141
143
  "key will be used to namespace and construct a title. Though a plugin can "
@@ -1,6 +1,4 @@
1
- from typing import Dict, List, Optional
2
-
3
- from npe2._pydantic_compat import BaseModel, Field, validator
1
+ from pydantic import BaseModel, Field, field_validator
4
2
 
5
3
  from ._commands import CommandContribution
6
4
  from ._configuration import ConfigurationContribution
@@ -30,13 +28,13 @@ __all__ = [
30
28
 
31
29
 
32
30
  class ContributionPoints(BaseModel):
33
- commands: Optional[List[CommandContribution]]
34
- readers: Optional[List[ReaderContribution]]
35
- writers: Optional[List[WriterContribution]]
36
- widgets: Optional[List[WidgetContribution]]
37
- sample_data: Optional[List[SampleDataContribution]]
38
- themes: Optional[List[ThemeContribution]]
39
- menus: Dict[str, List[MenuItem]] = Field(
31
+ commands: list[CommandContribution] | None = None
32
+ readers: list[ReaderContribution] | None = None
33
+ writers: list[WriterContribution] | None = None
34
+ widgets: list[WidgetContribution] | None = None
35
+ sample_data: list[SampleDataContribution] | None = None
36
+ themes: list[ThemeContribution] | None = None
37
+ menus: dict[str, list[MenuItem]] = Field(
40
38
  default_factory=dict,
41
39
  description="Add menu items to existing napari menus."
42
40
  "A menu item can be a command, such as open a widget, or a submenu."
@@ -44,10 +42,10 @@ class ContributionPoints(BaseModel):
44
42
  "This allows you to organize your plugin's contributions within"
45
43
  "napari's menu structure.",
46
44
  )
47
- submenus: Optional[List[SubmenuContribution]]
48
- keybindings: Optional[List[KeyBindingContribution]] = Field(None, hide_docs=True)
45
+ submenus: list[SubmenuContribution] | None = None
46
+ keybindings: list[KeyBindingContribution] | None = Field(None, hide_docs=True)
49
47
 
50
- configuration: List[ConfigurationContribution] = Field(
48
+ configuration: list[ConfigurationContribution] = Field(
51
49
  default_factory=list,
52
50
  hide_docs=True,
53
51
  description="Configuration options for this plugin."
@@ -58,6 +56,7 @@ class ContributionPoints(BaseModel):
58
56
  "keys will be used for the submenu entry names.",
59
57
  )
60
58
 
61
- @validator("configuration", pre=True)
59
+ @field_validator("configuration", mode="before")
60
+ @classmethod
62
61
  def _to_list(cls, v):
63
62
  return v if isinstance(v, list) else [v]
@@ -1,8 +1,6 @@
1
- from typing import Optional
2
-
3
- from npe2._pydantic_compat import BaseModel
1
+ from pydantic import BaseModel
4
2
 
5
3
 
6
4
  class Icon(BaseModel):
7
- light: Optional[str] = None
8
- dark: Optional[str] = None
5
+ light: str | None = None
6
+ dark: str | None = None