@@ -110,8 +110,13 @@ def _discover_resolver_files(
110110 found_files : set [Path ] = set ()
111111
112112 for pat in patterns :
113- # Add recursive prefix if needed
114- glob_pattern = f"**/{ pat } " if recursive and not pat .startswith ("**/" ) else pat
113+ # Handle recursive flag: add **/ prefix if recursive, strip **/ if not
114+ if recursive and not pat .startswith ("**/" ):
115+ glob_pattern = f"**/{ pat } "
116+ elif not recursive and pat .startswith ("**/" ):
117+ glob_pattern = pat [3 :] # Strip **/ prefix
118+ else :
119+ glob_pattern = pat
115120
116121 for file_path in root .glob (glob_pattern ):
117122 if file_path .is_file () and not _is_excluded (file_path , root , exclude ):
@@ -289,6 +294,7 @@ def __init__(
289294 self ._discovered_files : list [Path ] = []
290295 self ._resolver_name : str = "app"
291296 self ._on_conflict = on_conflict
297+ self ._cached_schema : dict [str , Any ] | None = None
292298
293299 def discover (
294300 self ,
@@ -339,15 +345,23 @@ def discover(
339345 return self ._discovered_files
340346
341347 def add_file (self , file_path : str | Path , resolver_name : str | None = None ) -> None :
342- """Add a specific file to be included in the merge."""
348+ """Add a specific file to be included in the merge.
349+
350+ Note: Must be called before get_openapi_schema(). Adding files after
351+ schema generation will not affect the cached result.
352+ """
343353 path = Path (file_path ).resolve ()
344354 if path not in self ._discovered_files :
345355 self ._discovered_files .append (path )
346356 if resolver_name :
347357 self ._resolver_name = resolver_name
348358
349359 def add_schema (self , schema : dict [str , Any ]) -> None :
350- """Add a pre-generated OpenAPI schema to be merged."""
360+ """Add a pre-generated OpenAPI schema to be merged.
361+
362+ Note: Must be called before get_openapi_schema(). Adding schemas after
363+ schema generation will not affect the cached result.
364+ """
351365 self ._schemas .append (_model_to_dict (schema ))
352366
353367 def get_openapi_schema (self ) -> dict [str , Any ]:
@@ -357,6 +371,8 @@ def get_openapi_schema(self) -> dict[str, Any]:
357371 Loads all discovered resolver files, extracts their OpenAPI schemas,
358372 and merges them into a single unified specification.
359373
374+ The schema is cached after the first generation for performance.
375+
360376 Returns
361377 -------
362378 dict[str, Any]
@@ -367,6 +383,9 @@ def get_openapi_schema(self) -> dict[str, Any]:
367383 OpenAPIMergeError
368384 If on_conflict="error" and duplicate path+method combinations are found.
369385 """
386+ if self ._cached_schema is not None :
387+ return self ._cached_schema
388+
370389 # Load schemas from discovered files
371390 for file_path in self ._discovered_files :
372391 try :
@@ -376,7 +395,8 @@ def get_openapi_schema(self) -> dict[str, Any]:
376395 except (ImportError , AttributeError , FileNotFoundError ) as e : # pragma: no cover
377396 logger .warning (f"Failed to load resolver from { file_path } : { e } " )
378397
379- return self ._merge_schemas ()
398+ self ._cached_schema = self ._merge_schemas ()
399+ return self ._cached_schema
380400
381401 def get_openapi_json_schema (self ) -> str :
382402 """
@@ -486,7 +506,12 @@ def _handle_conflict(self, method: str, path: str, target: dict, operation: Any)
486506 target [path ][method ] = operation
487507
488508 def _merge_components (self , source : dict [str , Any ], target : dict [str , dict [str , Any ]]) -> None :
489- """Merge components from source into target."""
509+ """Merge components from source into target.
510+
511+ Note: Components with the same name are silently overwritten (last wins).
512+ This is intentional as component conflicts are typically user errors
513+ (e.g., two handlers defining different 'User' schemas).
514+ """
490515 for component_type , components in source .items ():
491516 target .setdefault (component_type , {}).update (components )
492517
0 commit comments