mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
openapi: Improve pruning of unused defs
Defs that are used only by other unused defs were counted as used. Fix this by tracing what defs use other defs recursively.
This commit is contained in:
parent
a0242e1c68
commit
bbf8165636
2 changed files with 84 additions and 28 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def _prune_param(schema: dict, param: str) -> dict:
|
||||
|
|
@ -24,25 +25,77 @@ def _prune_param(schema: dict, param: str) -> dict:
|
|||
return schema
|
||||
|
||||
|
||||
def _prune_unused_defs(schema: dict) -> dict:
|
||||
"""Walk the schema and prune unused defs."""
|
||||
|
||||
root_defs: set[str] = set()
|
||||
referenced_by: defaultdict[str, list] = defaultdict(list)
|
||||
|
||||
defs = schema.get("$defs")
|
||||
if defs is None:
|
||||
return schema
|
||||
|
||||
def walk(
|
||||
node: object, current_def: str | None = None, skip_defs: bool = False
|
||||
) -> None:
|
||||
if isinstance(node, dict):
|
||||
# Process $ref for definition tracking
|
||||
ref = node.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
||||
def_name = ref.split("/")[-1]
|
||||
if current_def:
|
||||
referenced_by[def_name].append(current_def)
|
||||
else:
|
||||
root_defs.add(def_name)
|
||||
|
||||
# Walk children
|
||||
for k, v in node.items():
|
||||
if skip_defs and k == "$defs":
|
||||
continue
|
||||
|
||||
walk(v, current_def=current_def)
|
||||
|
||||
elif isinstance(node, list):
|
||||
for v in node:
|
||||
walk(v)
|
||||
|
||||
# Traverse the schema once, skipping the $defs
|
||||
walk(schema, skip_defs=True)
|
||||
|
||||
# Now figure out what defs reference other defs
|
||||
for def_name, value in defs.items():
|
||||
walk(value, current_def=def_name)
|
||||
|
||||
# Figure out what defs were referenced directly or recursively
|
||||
def def_is_referenced(def_name):
|
||||
if def_name in root_defs:
|
||||
return True
|
||||
references = referenced_by.get(def_name)
|
||||
if references:
|
||||
for reference in references:
|
||||
if def_is_referenced(reference):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Remove orphaned definitions if requested
|
||||
for def_name in list(defs):
|
||||
if not def_is_referenced(def_name):
|
||||
defs.pop(def_name)
|
||||
if not defs:
|
||||
schema.pop("$defs", None)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def _walk_and_prune(
|
||||
schema: dict,
|
||||
prune_defs: bool = False,
|
||||
prune_titles: bool = False,
|
||||
prune_additional_properties: bool = False,
|
||||
) -> dict:
|
||||
"""Walk the schema and optionally prune titles, unused definitions, and additionalProperties: false."""
|
||||
|
||||
# Will only be used if prune_defs is True
|
||||
used_defs: set[str] = set()
|
||||
"""Walk the schema and optionally prune titles and additionalProperties: false."""
|
||||
|
||||
def walk(node: object) -> None:
|
||||
if isinstance(node, dict):
|
||||
# Process $ref for definition tracking
|
||||
if prune_defs:
|
||||
ref = node.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
||||
used_defs.add(ref.split("/")[-1])
|
||||
|
||||
# Remove title if requested
|
||||
if prune_titles and "title" in node:
|
||||
node.pop("title")
|
||||
|
|
@ -62,18 +115,8 @@ def _walk_and_prune(
|
|||
for v in node:
|
||||
walk(v)
|
||||
|
||||
# Traverse the schema once
|
||||
walk(schema)
|
||||
|
||||
# Remove orphaned definitions if requested
|
||||
if prune_defs:
|
||||
defs = schema.get("$defs", {})
|
||||
for def_name in list(defs):
|
||||
if def_name not in used_defs:
|
||||
defs.pop(def_name)
|
||||
if not defs:
|
||||
schema.pop("$defs", None)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
|
|
@ -109,12 +152,13 @@ def compress_schema(
|
|||
schema = _prune_param(schema, param=param)
|
||||
|
||||
# Do a single walk to handle pruning operations
|
||||
if prune_defs or prune_titles or prune_additional_properties:
|
||||
if prune_titles or prune_additional_properties:
|
||||
schema = _walk_and_prune(
|
||||
schema,
|
||||
prune_defs=prune_defs,
|
||||
prune_titles=prune_titles,
|
||||
prune_additional_properties=prune_additional_properties,
|
||||
)
|
||||
if prune_defs:
|
||||
schema = _prune_unused_defs(schema)
|
||||
|
||||
return schema
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
from fastmcp.utilities.json_schema import (
|
||||
_prune_param,
|
||||
_prune_unused_defs,
|
||||
_walk_and_prune,
|
||||
compress_schema,
|
||||
)
|
||||
|
||||
|
||||
# Create wrappers for backward compatibility with tests
|
||||
def _prune_unused_defs(schema):
|
||||
"""Wrapper for _walk_and_prune that only prunes definitions."""
|
||||
return _walk_and_prune(schema, prune_defs=True)
|
||||
# Wrapper for backward compatibility with tests
|
||||
|
||||
|
||||
def _prune_additional_properties(schema):
|
||||
|
|
@ -95,6 +92,21 @@ class TestPruneUnusedDefs:
|
|||
assert "nested_def" in result["$defs"]
|
||||
assert "unused_def" not in result["$defs"]
|
||||
|
||||
def test_nested_references_removed(self):
|
||||
"""Test that definitions referenced via nesting in unused defs are removed."""
|
||||
schema = {
|
||||
"properties": {},
|
||||
"$defs": {
|
||||
"foo_def": {
|
||||
"type": "object",
|
||||
"properties": {"nested": {"$ref": "#/$defs/nested_def"}},
|
||||
},
|
||||
"nested_def": {"type": "string"},
|
||||
},
|
||||
}
|
||||
result = _prune_unused_defs(schema)
|
||||
assert "$defs" not in result
|
||||
|
||||
def test_array_references_kept(self):
|
||||
"""Test that definitions referenced in array items are kept."""
|
||||
schema = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue