mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 04:54:17 +02:00
Make PluginMeta.version optional; bundled plugins default to None (#3991)
This commit is contained in:
parent
67f226d453
commit
34cb2218dc
2 changed files with 32 additions and 13 deletions
|
|
@ -75,8 +75,14 @@ class PluginMeta(BaseModel):
|
|||
name: str
|
||||
"""Plugin name. Required. Must be unique within a server."""
|
||||
|
||||
version: str
|
||||
"""Plugin version (plugin's own semver, independent of fastmcp)."""
|
||||
version: str | None = None
|
||||
"""Plugin's independent semver, if it has one. `None` means the
|
||||
plugin is bundled with its containing package (typically fastmcp
|
||||
itself) and doesn't track a separate release cadence — which is the
|
||||
correct answer for first-party plugins that ship in-tree. Published
|
||||
plugins derive this from their PyPI distribution via
|
||||
`PluginMeta.from_package(...)`.
|
||||
"""
|
||||
|
||||
description: str | None = None
|
||||
"""Short human-readable description."""
|
||||
|
|
@ -250,9 +256,6 @@ class PluginMeta(BaseModel):
|
|||
return cls(**derived)
|
||||
|
||||
|
||||
_DEFAULT_PLUGIN_VERSION = "0.1.0"
|
||||
|
||||
|
||||
class _EmptyConfig(BaseModel):
|
||||
"""Default config for plugins that don't declare their own via the
|
||||
`Plugin[ConfigType]` generic parameter."""
|
||||
|
|
@ -349,7 +352,7 @@ class Plugin(Generic[C]):
|
|||
Subclass to define a plugin. A subclass may optionally declare a
|
||||
class-level `meta` attribute (a `PluginMeta` instance); if omitted,
|
||||
a default is derived from the class name (kebab-cased, trailing
|
||||
`Plugin` stripped) with version `0.1.0`. Declare `meta` explicitly
|
||||
`Plugin` stripped) and no independent version. Declare `meta` explicitly
|
||||
when publishing or when Horizon/registry-facing metadata matters.
|
||||
|
||||
**Config typing.** Parameterize `Plugin` with a pydantic model to
|
||||
|
|
@ -402,10 +405,7 @@ class Plugin(Generic[C]):
|
|||
# meta from an intermediate subclass isn't treated as a local
|
||||
# declaration — each concrete Plugin class gets its own name.
|
||||
if "meta" not in cls.__dict__:
|
||||
cls.meta = PluginMeta(
|
||||
name=_derive_plugin_name(cls.__name__),
|
||||
version=_DEFAULT_PLUGIN_VERSION,
|
||||
)
|
||||
cls.meta = PluginMeta(name=_derive_plugin_name(cls.__name__))
|
||||
# Resolve the Config model from the generic parameter. We walk the
|
||||
# `__orig_bases__` chain and propagate TypeVar substitutions, so
|
||||
# both direct parameterization (`class P(Plugin[Cfg])`) and
|
||||
|
|
|
|||
|
|
@ -50,9 +50,11 @@ class TestPluginMeta:
|
|||
"""PluginMeta is the source-of-truth metadata model."""
|
||||
|
||||
def test_required_fields(self):
|
||||
meta = PluginMeta(name="x", version="0.1.0")
|
||||
meta = PluginMeta(name="x")
|
||||
assert meta.name == "x"
|
||||
assert meta.version == "0.1.0"
|
||||
# `version` is optional — bundled plugins don't track a separate
|
||||
# release cadence from their container.
|
||||
assert meta.version is None
|
||||
assert meta.description is None
|
||||
assert meta.tags == []
|
||||
assert meta.dependencies == []
|
||||
|
|
@ -78,6 +80,22 @@ class TestPluginMeta:
|
|||
meta = AcmeMeta(name="x", version="0.1.0", owning_team="platform")
|
||||
assert meta.owning_team == "platform"
|
||||
|
||||
def test_version_is_optional_and_defaults_to_none(self):
|
||||
"""Bundled plugins don't have an independent version; `None` is
|
||||
the honest answer and avoids both lockstep lies (phantom bumps)
|
||||
and sentinel strings like "bundled" that break semver consumers."""
|
||||
meta = PluginMeta(name="bundled")
|
||||
assert meta.version is None
|
||||
# Manifest emission keeps the field — consumers see `null` and
|
||||
# can render "bundled" or similar at the presentation layer.
|
||||
assert meta.model_dump()["version"] is None
|
||||
|
||||
def test_explicit_version_still_accepted(self):
|
||||
"""Published plugins set a real semver, typically via
|
||||
`PluginMeta.from_package(...)`; the field still accepts any string."""
|
||||
meta = PluginMeta(name="published", version="1.2.3")
|
||||
assert meta.version == "1.2.3"
|
||||
|
||||
|
||||
class TestFromPackage:
|
||||
"""PluginMeta.from_package() derives metadata from importlib.metadata."""
|
||||
|
|
@ -257,7 +275,8 @@ class TestPluginConstruction:
|
|||
p = ChannelPlugin()
|
||||
# Class name is kebab-cased and the trailing "Plugin" suffix stripped.
|
||||
assert p.meta.name == "channel"
|
||||
assert p.meta.version == "0.1.0"
|
||||
# Bundled plugins have no independent version.
|
||||
assert p.meta.version is None
|
||||
|
||||
def test_plugin_meta_auto_derivation_handles_acronyms(self):
|
||||
class PIIRedactor(Plugin):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue