From f45531dfd5e403415c909956b53c1d13e9885d21 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:08:16 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20format?= =?UTF-8?q?=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 67 +++++++++++++++++++++---------- fastapi/schema.py | 57 +++++++++++++------------- tests/test_property_names.py | 78 +++++++++++++++++++----------------- 3 files changed, 115 insertions(+), 87 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index e42eb5e21..c1079e2d6 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -205,17 +205,23 @@ if PYDANTIC_V2: json_schema["title"] = ( field.field_info.title or field.alias.title().replace("_", " ") ) - + # Check for PropertyNames constraint in field metadata try: - from fastapi.schema import get_property_names_constraint, apply_property_names_to_schema + from fastapi.schema import ( + apply_property_names_to_schema, + get_property_names_constraint, + ) + property_names_constraint = get_property_names_constraint(field.field_info) if property_names_constraint: - json_schema = apply_property_names_to_schema(json_schema, property_names_constraint) + json_schema = apply_property_names_to_schema( + json_schema, property_names_constraint + ) except ImportError: # Gracefully handle if schema module is not available pass - + return json_schema def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: @@ -247,12 +253,12 @@ if PYDANTIC_V2: if "description" in item_def: item_description = cast(str, item_def["description"]).split("\f")[0] item_def["description"] = item_description - + # Apply PropertyNames constraints to model definitions # Convert DefsRef keys to strings for compatibility string_definitions = {str(k): v for k, v in definitions.items()} _apply_property_names_to_definitions(string_definitions, fields) - + return field_mapping, definitions # type: ignore[return-value] def is_scalar_field(field: ModelField) -> bool: @@ -486,20 +492,29 @@ else: separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: # This expects that GenerateJsonSchema was already used to generate the definitions - json_schema = cast(Dict[str, Any], field_schema( - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0]) - + json_schema = cast( + Dict[str, Any], + field_schema(field, model_name_map=model_name_map, ref_prefix=REF_PREFIX)[ + 0 + ], + ) + # Check for PropertyNames constraint in field metadata try: - from fastapi.schema import get_property_names_constraint, apply_property_names_to_schema + from fastapi.schema import ( + apply_property_names_to_schema, + get_property_names_constraint, + ) + property_names_constraint = get_property_names_constraint(field.field_info) if property_names_constraint: - json_schema = apply_property_names_to_schema(json_schema, property_names_constraint) + json_schema = apply_property_names_to_schema( + json_schema, property_names_constraint + ) except ImportError: # Gracefully handle if schema module is not available pass - + return json_schema def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: @@ -690,18 +705,20 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: def _apply_property_names_to_definitions( - definitions: Dict[str, Dict[str, Any]], - fields: List[ModelField] + definitions: Dict[str, Dict[str, Any]], fields: List[ModelField] ) -> None: """ Apply PropertyNames constraints to model definitions by checking each model's fields. """ try: - from fastapi.schema import get_property_names_constraint, apply_property_names_to_schema - + from fastapi.schema import ( + apply_property_names_to_schema, + get_property_names_constraint, + ) + # Group fields by their model type model_fields_map: Dict[Type[BaseModel], List[ModelField]] = {} - + for field in fields: # Get the actual annotation (which might be the BaseModel class) annotation = field.field_info.annotation @@ -711,8 +728,10 @@ def _apply_property_names_to_definitions( if model_annotation not in model_fields_map: model_fields_map[model_annotation] = [] # Get fields from the model itself, not the field that references it - model_fields_map[model_annotation].extend(get_model_fields(model_annotation)) - + model_fields_map[model_annotation].extend( + get_model_fields(model_annotation) + ) + # Apply PropertyNames constraints to definitions for model_class, model_field_list in model_fields_map.items(): model_name = model_class.__name__ @@ -720,12 +739,16 @@ def _apply_property_names_to_definitions( definition = definitions[model_name] if "properties" in definition: for model_field in model_field_list: - property_names_constraint = get_property_names_constraint(model_field.field_info) + property_names_constraint = get_property_names_constraint( + model_field.field_info + ) if property_names_constraint: field_name = model_field.alias if field_name in definition["properties"]: field_schema = definition["properties"][field_name] - field_schema = apply_property_names_to_schema(field_schema, property_names_constraint) + field_schema = apply_property_names_to_schema( + field_schema, property_names_constraint + ) definition["properties"][field_name] = field_schema except ImportError: # Schema module not available diff --git a/fastapi/schema.py b/fastapi/schema.py index 3c0530d2a..27023282b 100644 --- a/fastapi/schema.py +++ b/fastapi/schema.py @@ -2,40 +2,40 @@ FastAPI schema utilities including PropertyNames constraint support. """ -from typing import Any, Dict, Optional, Union -from pydantic import BaseModel +from typing import Any, Dict, Optional + from pydantic.fields import FieldInfo class PropertyNames: """ A constraint class for specifying propertyNames validation in JSON Schema. - + This allows developers to specify patterns or schemas that all property names in a dictionary/object must match. - + Examples: # Pattern-based constraint PropertyNames(pattern="^prefix_") - - # Schema-based constraint + + # Schema-based constraint PropertyNames(min_length=3, max_length=50) - + # Complex schema constraint PropertyNames(schema={"type": "string", "enum": ["key1", "key2", "key3"]}) """ - + def __init__( self, pattern: Optional[str] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, schema: Optional[Dict[str, Any]] = None, - **kwargs: Any + **kwargs: Any, ): """ Initialize PropertyNames constraint. - + Args: pattern: Regular expression pattern that property names must match min_length: Minimum length for property names @@ -44,31 +44,33 @@ class PropertyNames: **kwargs: Additional JSON schema properties """ self.constraint_schema: Dict[str, Any] = {} - + if pattern is not None: self.constraint_schema["pattern"] = pattern - + if min_length is not None: self.constraint_schema["minLength"] = min_length - + if max_length is not None: self.constraint_schema["maxLength"] = max_length - + if schema is not None: self.constraint_schema.update(schema) - + # Add any additional properties for key, value in kwargs.items(): if value is not None: self.constraint_schema[key] = value - + if not self.constraint_schema: - raise ValueError("PropertyNames constraint must specify at least one validation rule") - + raise ValueError( + "PropertyNames constraint must specify at least one validation rule" + ) + def get_constraint_schema(self) -> Dict[str, Any]: """Get the JSON schema representation of this constraint.""" return self.constraint_schema.copy() - + def __repr__(self) -> str: return f"PropertyNames({self.constraint_schema})" @@ -76,34 +78,33 @@ class PropertyNames: def get_property_names_constraint(field_info: FieldInfo) -> Optional[Dict[str, Any]]: """ Extract PropertyNames constraint from field metadata. - + Args: field_info: Pydantic FieldInfo object - + Returns: PropertyNames constraint schema or None if not found """ - if not hasattr(field_info, 'metadata') or not field_info.metadata: + if not hasattr(field_info, "metadata") or not field_info.metadata: return None - + for metadata_item in field_info.metadata: if isinstance(metadata_item, PropertyNames): return metadata_item.get_constraint_schema() - + return None def apply_property_names_to_schema( - schema: Dict[str, Any], - property_names_constraint: Dict[str, Any] + schema: Dict[str, Any], property_names_constraint: Dict[str, Any] ) -> Dict[str, Any]: """ Apply PropertyNames constraint to a JSON schema. - + Args: schema: The JSON schema to modify property_names_constraint: The propertyNames constraint to apply - + Returns: Modified schema with propertyNames constraint """ diff --git a/tests/test_property_names.py b/tests/test_property_names.py index 058b69f5c..f837e618d 100644 --- a/tests/test_property_names.py +++ b/tests/test_property_names.py @@ -3,11 +3,11 @@ Tests for PropertyNames constraint support in FastAPI. """ from typing import Dict -from typing_extensions import Annotated + from fastapi import FastAPI from fastapi.schema import PropertyNames from pydantic import BaseModel -import json +from typing_extensions import Annotated def test_property_names_creation(): @@ -15,42 +15,43 @@ def test_property_names_creation(): # Test pattern constraint pn1 = PropertyNames(pattern="^test_") assert pn1.get_constraint_schema() == {"pattern": "^test_"} - + # Test multiple constraints pn2 = PropertyNames(min_length=5, max_length=20, pattern="^[a-z]+$") expected = {"minLength": 5, "maxLength": 20, "pattern": "^[a-z]+$"} assert pn2.get_constraint_schema() == expected - + # Test schema constraint pn3 = PropertyNames(schema={"type": "string", "format": "email"}) expected = {"type": "string", "format": "email"} assert pn3.get_constraint_schema() == expected - + # Test empty constraint should raise error try: PropertyNames() assert False, "Should have raised ValueError" except ValueError as e: - assert "PropertyNames constraint must specify at least one validation rule" in str(e) + assert ( + "PropertyNames constraint must specify at least one validation rule" + in str(e) + ) def test_property_names_pattern_constraint(): """Test PropertyNames with pattern constraint in FastAPI schema generation.""" + class ModelWithPropertyNames(BaseModel): - properties: Annotated[ - Dict[str, str], - PropertyNames(pattern="^prefix_") - ] - + properties: Annotated[Dict[str, str], PropertyNames(pattern="^prefix_")] + app = FastAPI() - + @app.post("/test") def test_endpoint(data: ModelWithPropertyNames): return data - + # Get the OpenAPI schema openapi_schema = app.openapi() - + # Find the schema for our model model_schema = None for schema_name, schema_def in openapi_schema["components"]["schemas"].items(): @@ -59,13 +60,13 @@ def test_property_names_pattern_constraint(): if "propertyNames" in properties_field: model_schema = properties_field break - + assert model_schema is not None, "Could not find model schema with propertyNames" - + # Verify the propertyNames constraint is present property_names_constraint = model_schema["propertyNames"] assert property_names_constraint["pattern"] == "^prefix_" - + # Verify the full schema structure expected_keys = {"type", "additionalProperties", "propertyNames", "title"} assert set(model_schema.keys()) >= expected_keys @@ -73,21 +74,22 @@ def test_property_names_pattern_constraint(): def test_property_names_complex_constraints(): """Test PropertyNames with multiple constraints.""" + class ComplexPropertyNames(BaseModel): config_vars: Annotated[ Dict[str, str], - PropertyNames(min_length=3, max_length=50, pattern="^[A-Z_]+$") + PropertyNames(min_length=3, max_length=50, pattern="^[A-Z_]+$"), ] - + app = FastAPI() - + @app.post("/test") def test_endpoint(data: ComplexPropertyNames): return data - + # Get the OpenAPI schema openapi_schema = app.openapi() - + # Find the schema for our model model_schema = None for schema_name, schema_def in openapi_schema["components"]["schemas"].items(): @@ -96,9 +98,9 @@ def test_property_names_complex_constraints(): if "propertyNames" in config_vars_field: model_schema = config_vars_field break - + assert model_schema is not None, "Could not find model schema with propertyNames" - + # Verify the propertyNames constraint has all expected properties property_names_constraint = model_schema["propertyNames"] assert property_names_constraint["pattern"] == "^[A-Z_]+$" @@ -108,21 +110,22 @@ def test_property_names_complex_constraints(): def test_property_names_schema_constraint(): """Test PropertyNames with custom schema constraint.""" + class SchemaPropertyNames(BaseModel): allowed_keys: Annotated[ Dict[str, int], - PropertyNames(schema={"type": "string", "enum": ["key1", "key2", "key3"]}) + PropertyNames(schema={"type": "string", "enum": ["key1", "key2", "key3"]}), ] - + app = FastAPI() - + @app.post("/test") def test_endpoint(data: SchemaPropertyNames): return data - + # Get the OpenAPI schema openapi_schema = app.openapi() - + # Find the schema for our model model_schema = None for schema_name, schema_def in openapi_schema["components"]["schemas"].items(): @@ -131,9 +134,9 @@ def test_property_names_schema_constraint(): if "propertyNames" in allowed_keys_field: model_schema = allowed_keys_field break - + assert model_schema is not None, "Could not find model schema with propertyNames" - + # Verify the propertyNames constraint with custom schema property_names_constraint = model_schema["propertyNames"] assert property_names_constraint["type"] == "string" @@ -142,18 +145,19 @@ def test_property_names_schema_constraint(): def test_property_names_no_constraint(): """Test that models without PropertyNames constraints work normally.""" + class NormalModel(BaseModel): properties: Dict[str, str] - + app = FastAPI() - + @app.post("/test") def test_endpoint(data: NormalModel): return data - + # Get the OpenAPI schema openapi_schema = app.openapi() - + # Find the schema for our model for schema_name, schema_def in openapi_schema["components"]["schemas"].items(): if schema_name == "NormalModel": @@ -169,7 +173,7 @@ def test_property_names_kwargs(): """Test PropertyNames with additional kwargs.""" pn = PropertyNames(format="email", description="Email addresses") constraint_schema = pn.get_constraint_schema() - + assert constraint_schema["format"] == "email" assert constraint_schema["description"] == "Email addresses" @@ -178,7 +182,7 @@ def test_property_names_repr(): """Test PropertyNames string representation.""" pn = PropertyNames(pattern="^test_") repr_str = repr(pn) - + assert "PropertyNames" in repr_str assert "pattern" in repr_str assert "^test_" in repr_str