Browse Source

🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

pull/14142/head
pre-commit-ci[bot] 10 months ago
parent
commit
f45531dfd5
  1. 67
      fastapi/_compat.py
  2. 57
      fastapi/schema.py
  3. 78
      tests/test_property_names.py

67
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

57
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
"""

78
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

Loading…
Cancel
Save