From 7d29c6d7faddcb53e7b3f37d1e0a482fe14d8cec Mon Sep 17 00:00:00 2001 From: David Montague <35119617+dmontagu@users.noreply.github.com> Date: Mon, 27 Mar 2023 22:32:43 -0600 Subject: [PATCH] Add warnings/errors for non-annotated fields --- docs/usage/model_config.md | 7 ++- pydantic/_internal/_model_construction.py | 47 +++++++++----- pydantic/config.py | 4 +- pydantic/main.py | 30 ++++++--- pydantic/mypy.py | 2 +- tests/test_aliases.py | 12 +--- tests/test_create_model.py | 74 +++++++++++++---------- tests/test_edge_cases.py | 66 +++++++++++--------- tests/test_generics.py | 16 +++-- tests/test_main.py | 52 +++++++++++++--- 10 files changed, 198 insertions(+), 112 deletions(-) diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md index a169bf0a9a..0ee7b394b9 100644 --- a/docs/usage/model_config.md +++ b/docs/usage/model_config.md @@ -151,9 +151,10 @@ for validation, for use with `from_attributes`; see [Data binding](models.md#dat **`alias_generator`** : a callable that takes a field name and returns an alias for it; see [the dedicated section](#alias-generator) -**`keep_untouched`** -: a tuple of types (e.g. descriptors) for a model's default values that should not be changed during model creation and will -not be included in the model schemas. **Note**: this means that attributes on the model with *defaults of this type*, not *annotations of this type*, will be left alone. +**`non_field_types`** +: a tuple of types that may occur as values of class attributes without annotations; this is typically used for +custom descriptors (classes that behave like `property`). If an attribute is set on a class without an annotation +and has a type that is not in this tuple (or otherwise recognized by pydantic), an error will be raised. **`schema_extra`** : a `dict` used to extend/update the generated JSON Schema, or a callable to post-process it; see [schema customization](schema.md#schema-customization) diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py index 667abde370..accb645c8a 100644 --- a/pydantic/_internal/_model_construction.py +++ b/pydantic/_internal/_model_construction.py @@ -4,7 +4,6 @@ from __future__ import annotations as _annotations import typing -import warnings from functools import partial from types import FunctionType from typing import Any, Callable @@ -15,6 +14,7 @@ from ..fields import FieldInfo, ModelPrivateAttr, PrivateAttr from ._core_metadata import build_metadata_dict from ._core_utils import consolidate_refs, define_expected_missing_refs +from ._decorators import PydanticDecoratorMarker from ._fields import Undefined, collect_fields from ._forward_ref import PydanticForwardRef from ._generate_schema import generate_config, get_model_self_schema, model_fields_schema @@ -30,8 +30,7 @@ __all__ = 'object_setattr', 'init_private_attributes', 'inspect_namespace', 'complete_model_class', 'MockValidator' - -IGNORED_TYPES: tuple[Any, ...] = (FunctionType, property, type, classmethod, staticmethod) +IGNORED_TYPES: tuple[Any, ...] = (FunctionType, property, type, classmethod, staticmethod, PydanticDecoratorMarker) object_setattr = object.__setattr__ @@ -47,15 +46,24 @@ def init_private_attributes(self_: Any, _context: Any) -> None: object_setattr(self_, name, default) -def inspect_namespace(namespace: dict[str, Any]) -> dict[str, ModelPrivateAttr]: +def inspect_namespace( # noqa C901 + namespace: dict[str, Any], + non_field_types: tuple[type[Any], ...], + base_class_vars: set[str], + base_class_fields: set[str], +) -> dict[str, ModelPrivateAttr]: """ iterate over the namespace and: * gather private attributes * check for items which look like fields but are not (e.g. have no annotation) and warn """ + all_non_field_types = non_field_types + IGNORED_TYPES + private_attributes: dict[str, ModelPrivateAttr] = {} raw_annotations = namespace.get('__annotations__', {}) for var_name, value in list(namespace.items()): + if var_name == 'model_config': + continue if isinstance(value, ModelPrivateAttr): if var_name.startswith('__'): raise NameError( @@ -69,19 +77,28 @@ def inspect_namespace(namespace: dict[str, Any]) -> dict[str, ModelPrivateAttr]: ) private_attributes[var_name] = value del namespace[var_name] - elif not single_underscore(var_name): + elif var_name.startswith('__'): continue elif var_name.startswith('_'): - if var_name in raw_annotations and is_classvar(raw_annotations[var_name]): - continue - private_attributes[var_name] = PrivateAttr(default=value) - del namespace[var_name] - elif var_name not in raw_annotations and not isinstance(value, IGNORED_TYPES): - warnings.warn( - f'All fields must include a type annotation; ' - f'{var_name!r} looks like a field but has no type annotation.', - DeprecationWarning, - ) + if var_name in raw_annotations and not is_classvar(raw_annotations[var_name]): + private_attributes[var_name] = PrivateAttr(default=value) + del namespace[var_name] + elif var_name in base_class_vars: + continue + elif var_name not in raw_annotations and not isinstance(value, all_non_field_types): + if var_name in base_class_fields: + raise PydanticUserError( + f'Field {var_name!r} defined on a base class was overridden by a non-annotated attribute. ' + f'All field definitions, including overrides, require a type annotation.', + ) + elif isinstance(value, FieldInfo): + raise PydanticUserError(f'Field {var_name!r} requires a type annotation') + else: + raise PydanticUserError( + f'A non-annotated attribute was detected: `{var_name} = {value!r}`. All model fields require a ' + f'type annotation; if {var_name!r} is not meant to be a field, you may be able to suppress this ' + f'warning by annotating it as a ClassVar or updating model_config["non_field_types"].', + ) for ann_name, ann_type in raw_annotations.items(): if single_underscore(ann_name) and ann_name not in private_attributes and not is_classvar(ann_type): diff --git a/pydantic/config.py b/pydantic/config.py index 2d9c3b9194..89828531b0 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -60,7 +60,7 @@ class _ConfigDict(TypedDict, total=False): undefined_types_warning: bool # TODO review docs from_attributes: bool alias_generator: Callable[[str], str] | None - keep_untouched: tuple[type, ...] # TODO remove?? + non_field_types: tuple[type, ...] # TODO remove?? json_loads: Callable[[str], Any] # TODO decide json_dumps: Callable[..., str] # TODO decide json_encoders: dict[type[Any] | str | ForwardRef, Callable[..., Any]] # TODO decide @@ -112,7 +112,7 @@ def __missing__(self, key: str) -> Any: undefined_types_warning=True, from_attributes=False, alias_generator=None, - keep_untouched=(), + non_field_types=(), json_loads=json.loads, json_dumps=json.dumps, json_encoders={}, diff --git a/pydantic/main.py b/pydantic/main.py index e561d01708..934f6af7c1 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -11,7 +11,7 @@ from enum import Enum from functools import partial from inspect import getdoc -from types import prepare_class, resolve_bases +from types import FunctionType, prepare_class, resolve_bases from typing import Any, Generic, TypeVar, overload import typing_extensions @@ -56,6 +56,8 @@ # the `BaseModel` class, since that's defined immediately after the metaclass. _base_class_defined = False +PYDANTIC_NON_FIELD_TYPES = (property, type, classmethod, staticmethod, FunctionType) + @typing_extensions.dataclass_transform(kw_only_default=True, field_specifiers=(Field,)) class ModelMetaclass(ABCMeta): @@ -71,9 +73,20 @@ def __new__( ) -> type: if _base_class_defined: class_vars: set[str] = set() + base_private_attributes: dict[str, ModelPrivateAttr] = {} + base_field_names: set[str] = set() + for base in bases: + if _base_class_defined and issubclass(base, BaseModel) and base != BaseModel: + class_vars.update(base.__class_vars__) + base_private_attributes.update(base.__private_attributes__) + # model_fields might not be defined yet in the case of generics, so we use getattr + base_field_names.update(getattr(base, 'model_fields', {}).keys()) + config_new = build_config(cls_name, bases, namespace, kwargs) namespace['model_config'] = config_new - private_attributes = _model_construction.inspect_namespace(namespace) + private_attributes = _model_construction.inspect_namespace( + namespace, config_new.get('non_field_types', ()), class_vars, base_field_names + ) if private_attributes: slots: set[str] = set(namespace.get('__slots__', ())) namespace['__slots__'] = slots | private_attributes.keys() @@ -93,11 +106,6 @@ def __pydantic_post_init__(self_: Any, context: Any) -> None: elif 'model_post_init' in namespace: namespace['__pydantic_post_init__'] = namespace['model_post_init'] - base_private_attributes: dict[str, ModelPrivateAttr] = {} - for base in bases: - if _base_class_defined and issubclass(base, BaseModel) and base != BaseModel: - base_private_attributes.update(base.__private_attributes__) - class_vars.update(base.__class_vars__) namespace['__class_vars__'] = class_vars namespace['__private_attributes__'] = {**base_private_attributes, **private_attributes} @@ -134,6 +142,14 @@ def hash_func(self_: Any) -> int: ) cls.__pydantic_model_complete__ = False # Ensure this specific class gets completed + + # preserve `__set_name__` protocol defined in https://peps.python.org/pep-0487 + # for attributes not in `new_namespace` (e.g. private attributes) + for name, obj in private_attributes.items(): + set_name = getattr(obj, '__set_name__', None) + if callable(set_name): + set_name(cls, name) + _model_construction.complete_model_class( cls, cls_name, diff --git a/pydantic/mypy.py b/pydantic/mypy.py index e05f23d188..b6b5bfc758 100644 --- a/pydantic/mypy.py +++ b/pydantic/mypy.py @@ -463,8 +463,8 @@ def collect_field_from_stmt(self, stmt: Statement, model_config: ModelConfigData and stmt.rvalue.callee.callee.fullname in DECORATOR_FULLNAMES ): # This is a (possibly-reused) validator or serializer, not a field - # TODO: We may want to inspect whatever replaces keep_untouched in the config # In particular, it looks something like: my_validator = validator('my_field', allow_reuse=True)(f) + # Eventually, we may want to attempt to respect model_config['non_field_types'] return None # The assignment does not have an annotation, and it's not anything else we recognize diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 413c9c3088..ff575a2d6d 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -37,16 +37,6 @@ class MyModel(BaseModel): assert str(e.value) == IsStr(regex="alias_generator must return str, not ") -@pytest.mark.xfail(reason='working on V2') -def test_cannot_infer_type_with_alias(): - # TODO: I don't think we've finalized the exact error that should be raised when fields are missing annotations, - # but this test should be made consistent with that once it is finalized - with pytest.raises(TypeError): - - class Model(BaseModel): - a = Field('foobar', alias='_a') - - def test_basic_alias(): class Model(BaseModel): a: str = Field('foobar', alias='_a') @@ -112,7 +102,7 @@ class Parent(BaseModel): x: int = Field(alias='x1', gt=0) class Child(Parent): - x = Field(..., alias='x2') + x: int = Field(..., alias='x2') y: int = Field(..., alias='y2') assert Parent.model_fields['x'].alias == 'x1' diff --git a/tests/test_create_model.py b/tests/test_create_model.py index d2b4576117..5eb6f1a25a 100644 --- a/tests/test_create_model.py +++ b/tests/test_create_model.py @@ -3,24 +3,27 @@ import pytest from pydantic import BaseModel, ConfigDict, Extra, Field, ValidationError, create_model, errors -from pydantic.decorators import field_validator +from pydantic.decorators import field_validator, validator from pydantic.fields import ModelPrivateAttr -@pytest.mark.xfail(reason='working on V2') def test_create_model(): - model = create_model('FooModel', foo=(str, ...), bar=123) + model = create_model('FooModel', foo=(str, ...), bar=(int, 123)) assert issubclass(model, BaseModel) assert model.model_config == BaseModel.model_config assert model.__name__ == 'FooModel' assert model.model_fields.keys() == {'foo', 'bar'} - assert model.__validators__ == {} + + assert not model.__pydantic_decorators__.validator + assert not model.__pydantic_decorators__.root_validator + assert not model.__pydantic_decorators__.field_validator + assert not model.__pydantic_decorators__.serializer + assert model.__module__ == 'pydantic.main' -@pytest.mark.xfail(reason='working on V2') def test_create_model_usage(): - model = create_model('FooModel', foo=(str, ...), bar=123) + model = create_model('FooModel', foo=(str, ...), bar=(int, 123)) m = model(foo='hello') assert m.foo == 'hello' assert m.bar == 123 @@ -42,7 +45,7 @@ def module(): from pydantic import create_model - FooModel = create_model('FooModel', foo=(str, ...), bar=123, __module__=__name__) + FooModel = create_model('FooModel', foo=(str, ...), bar=(int, 123), __module__=__name__) m = FooModel(foo='hello') d = pickle.dumps(m) @@ -69,11 +72,10 @@ def test_config_and_base(): create_model('FooModel', __config__=BaseModel.model_config, __base__=BaseModel) -@pytest.mark.xfail(reason='working on V2') def test_inheritance(): class BarModel(BaseModel): - x = 1 - y = 2 + x: int = 1 + y: int = 2 model = create_model('FooModel', foo=(str, ...), bar=(int, 123), __base__=BarModel) assert model.model_fields.keys() == {'foo', 'bar', 'x', 'y'} @@ -119,7 +121,6 @@ def test_custom_config_extras(): model(bar=654) -@pytest.mark.xfail(reason='working on V2') def test_inheritance_validators(): class BarModel(BaseModel): @field_validator('a', check_fields=False) @@ -129,14 +130,14 @@ def check_a(cls, v): raise ValueError('"foobar" not found in a') return v - model = create_model('FooModel', a='cake', __base__=BarModel) + model = create_model('FooModel', a=(str, 'cake'), __base__=BarModel) assert model().a == 'cake' assert model(a='this is foobar good').a == 'this is foobar good' with pytest.raises(ValidationError): model(a='something else') -@pytest.mark.xfail(reason='working on V2') +@pytest.mark.xfail(reason='validators do not support always=True') def test_inheritance_validators_always(): class BarModel(BaseModel): @field_validator('a', check_fields=False, always=True) @@ -146,7 +147,7 @@ def check_a(cls, v): raise ValueError('"foobar" not found in a') return v - model = create_model('FooModel', a='cake', __base__=BarModel) + model = create_model('FooModel', a=(str, 'cake'), __base__=BarModel) with pytest.raises(ValidationError): model() assert model(a='this is foobar good').a == 'this is foobar good' @@ -154,19 +155,19 @@ def check_a(cls, v): model(a='something else') -@pytest.mark.xfail(reason='working on V2') def test_inheritance_validators_all(): - class BarModel(BaseModel): - @field_validator('*') - @classmethod - def check_all(cls, v): - return v * 2 + with pytest.warns(DeprecationWarning, match='Pydantic V1 style `@validator` validators are deprecated'): + + class BarModel(BaseModel): + @validator('*') + @classmethod + def check_all(cls, v): + return v * 2 model = create_model('FooModel', a=(int, ...), b=(int, ...), __base__=BarModel) assert model(a=2, b=6).model_dump() == {'a': 4, 'b': 12} -@pytest.mark.xfail(reason='working on V2') def test_funky_name(): model = create_model('FooModel', **{'this-is-funky': (int, ...)}) m = model(**{'this-is-funky': '123'}) @@ -174,29 +175,28 @@ def test_funky_name(): with pytest.raises(ValidationError) as exc_info: model() assert exc_info.value.errors() == [ - {'loc': ('this-is-funky',), 'msg': 'field required', 'type': 'value_error.missing'} + {'input': {}, 'loc': ('this-is-funky',), 'msg': 'Field required', 'type': 'missing'} ] -@pytest.mark.xfail(reason='working on V2') def test_repeat_base_usage(): class Model(BaseModel): a: str assert Model.model_fields.keys() == {'a'} - model = create_model('FooModel', b=1, __base__=Model) + model = create_model('FooModel', b=(int, 1), __base__=Model) assert Model.model_fields.keys() == {'a'} assert model.model_fields.keys() == {'a', 'b'} - model2 = create_model('Foo2Model', c=1, __base__=Model) + model2 = create_model('Foo2Model', c=(int, 1), __base__=Model) assert Model.model_fields.keys() == {'a'} assert model.model_fields.keys() == {'a', 'b'} assert model2.model_fields.keys() == {'a', 'c'} - model3 = create_model('Foo2Model', d=1, __base__=model) + model3 = create_model('Foo2Model', d=(int, 1), __base__=model) assert Model.model_fields.keys() == {'a'} assert model.model_fields.keys() == {'a', 'b'} @@ -216,21 +216,29 @@ class A(BaseModel): assert A.model_fields[field_name].default == DynamicA.model_fields[field_name].default -@pytest.mark.xfail(reason='working on V2') def test_config_field_info_create_model(): # TODO fields doesn't exist anymore, remove test? # class Config: # fields = {'a': {'description': 'descr'}} - config = ConfigDict() + ConfigDict() - m1 = create_model('M1', __config__=config, a=(str, ...)) - assert m1.model_json_schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}} + m1 = create_model('M1', __config__={'title': 'abc'}, a=(str, ...)) + assert m1.model_json_schema() == { + 'properties': {'a': {'title': 'A', 'type': 'string'}}, + 'required': ['a'], + 'title': 'abc', + 'type': 'object', + } - m2 = create_model('M2', __config__=config, a=(str, Field(...))) - assert m2.model_json_schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}} + m2 = create_model('M2', __config__={}, a=(str, Field(description='descr'))) + assert m2.model_json_schema() == { + 'properties': {'a': {'description': 'descr', 'title': 'A', 'type': 'string'}}, + 'required': ['a'], + 'title': 'M2', + 'type': 'object', + } -@pytest.mark.xfail(reason='working on V2') @pytest.mark.parametrize('base', [ModelPrivateAttr, object]) def test_set_name(base): calls = [] diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 0eeb7d28eb..0d588c802a 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -926,11 +926,29 @@ def test_inheritance(): class Foo(BaseModel): a: float = ... - class Bar(Foo): + with pytest.raises( + TypeError, + match=( + "Field 'a' defined on a base class was overridden by a non-annotated attribute. " + 'All field definitions, including overrides, require a type annotation.' + ), + ): + + class Bar(Foo): + x: float = 12.3 + a = 123.0 + + class Bar2(Foo): + x: float = 12.3 + a: float = 123.0 + + assert Bar2().model_dump() == {'x': 12.3, 'a': 123.0} + + class Bar3(Foo): x: float = 12.3 - a = 123.0 + a: float = Field(default=123.0) - assert Bar().model_dump() == {'x': 12.3, 'a': 123.0} + assert Bar3().model_dump() == {'x': 12.3, 'a': 123.0} def test_inheritance_subclass_default(): @@ -949,7 +967,7 @@ class Base(BaseModel): y: str class Sub(Base): - x = MyStr('test') + x: str = MyStr('test') y: MyStr = MyStr('test') # force subtype model_config = dict(arbitrary_types_allowed=True) @@ -1058,18 +1076,13 @@ class Child(Parent): ] -@pytest.mark.xfail(reason='working on V2') def test_annotation_inheritance(): class A(BaseModel): integer: int = 1 class B(A): - integer = 2 + integer: int = 2 - if sys.version_info < (3, 10): - assert B.__annotations__['integer'] == int - else: - assert B.__annotations__ == {} assert B.model_fields['integer'].annotation == int class C(A): @@ -1078,20 +1091,17 @@ class C(A): assert C.__annotations__['integer'] == str assert C.model_fields['integer'].annotation == str - with pytest.raises(TypeError) as exc_info: + with pytest.raises( + TypeError, + match=( + "Field 'integer' defined on a base class was overridden by a non-annotated attribute. " + "All field definitions, including overrides, require a type annotation." + ), + ): class D(A): integer = 'G' - # TODO: Do we want any changes to this behavior in v2? (Currently, no error is raised) - # "I think it should be an error to redefine any field without an annotation - that way we - # don't need to start trying to infer the type of the default value." - # https://github.com/pydantic/pydantic/pull/5151#discussion_r1130681812 - assert str(exc_info.value) == ( - 'The type of D.integer differs from the new default value; ' - 'if you wish to change the type of this field, please use a type annotation' - ) - def test_string_none(): class Model(BaseModel): @@ -1167,19 +1177,19 @@ class InvalidValidatorModel(BaseModel): x: InvalidValidator = ... -@pytest.mark.xfail(reason='working on V2') def test_unable_to_infer(): - with pytest.raises(errors.PydanticUserError) as exc_info: + with pytest.raises( + errors.PydanticUserError, + match=re.escape( + "A non-annotated attribute was detected: `x = None`. All model fields require a type annotation; " + "if 'x' is not meant to be a field, you may be able to suppress this warning by annotating it as a " + "ClassVar or updating model_config[\"non_field_types\"]" + ), + ): class InvalidDefinitionModel(BaseModel): x = None - # TODO: Do we want any changes to this behavior in v2? (Currently, no error is raised) - # "x definitely shouldn't be a field, I guess an error would be best, - # but might be hard to identify 'non-field attributes reliable'?" - # https://github.com/pydantic/pydantic/pull/5151#discussion_r1130682562 - assert exc_info.value.args[0] == 'unable to infer type for attribute "x"' - def test_multiple_errors(): class Model(BaseModel): diff --git a/tests/test_generics.py b/tests/test_generics.py index 80233dab29..5869f8ac72 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -37,6 +37,7 @@ Field, Json, PositiveInt, + PydanticUserError, ValidationError, ValidationInfo, root_validator, @@ -217,15 +218,22 @@ class Result(BaseModel, Generic[T]): assert 'other' not in Result.model_fields -# TODO: Replace this test with a test that ensures the same warning message about non-annotated fields is raised -# for generic and non-generic models. Requires https://github.com/pydantic/pydantic/issues/5014 -@pytest.mark.xfail(reason='working on V2 - non-annotated fields - issue #5014') def test_non_annotated_field(): T = TypeVar('T') + with pytest.raises(PydanticUserError, match='A non-annotated attribute was detected: `other = True`'): + + class Result(BaseModel, Generic[T]): + data: T + other = True + + +def test_non_generic_field(): + T = TypeVar('T') + class Result(BaseModel, Generic[T]): data: T - other = True + other: bool = True assert 'other' in Result.model_fields assert 'other' in Result[int].model_fields diff --git a/tests/test_main.py b/tests/test_main.py index 94c6c5dc1b..3a59f1f2c7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,5 @@ import platform +import re import sys from collections import defaultdict from copy import deepcopy @@ -24,7 +25,17 @@ import pytest from typing_extensions import Final, Literal -from pydantic import BaseModel, ConfigDict, Extra, Field, PrivateAttr, SecretStr, ValidationError, constr +from pydantic import ( + BaseModel, + ConfigDict, + Extra, + Field, + PrivateAttr, + PydanticUserError, + SecretStr, + ValidationError, + constr, +) def test_success(): @@ -698,12 +709,12 @@ class BadModel(BaseModel): def test_value_field_name_shadows_attribute(): - class BadModel(BaseModel): - model_json_schema = ( - 'abc' # This conflicts with the BaseModel's model_json_schema() class method, but has no annotation - ) + with pytest.raises(PydanticUserError, match="A non-annotated attribute was detected: `model_json_schema = 'abc'`"): - assert len(BadModel.model_fields) == 0 + class BadModel(BaseModel): + model_json_schema = ( + 'abc' # This conflicts with the BaseModel's model_json_schema() class method, but has no annotation + ) def test_class_var(): @@ -853,7 +864,7 @@ def __get__(self, instance, owner): classproperty = _ClassPropertyDescriptor class Model(BaseModel): - model_config = ConfigDict(keep_untouched=(classproperty,)) + model_config = ConfigDict(non_field_types=(classproperty,)) @classproperty def class_name(cls) -> str: @@ -1232,6 +1243,31 @@ class Child(Parent): assert actual == expected, 'Unexpected model export result' +def test_untyped_fields_warning(): + with pytest.raises( + PydanticUserError, + match=re.escape( + "A non-annotated attribute was detected: `x = 1`. All model fields require a type annotation; " + "if 'x' is not meant to be a field, you may be able to suppress this warning by annotating it " + "as a ClassVar or updating model_config[\"non_field_types\"]." + ), + ): + + class WarningModel(BaseModel): + x = 1 + + # Prove that annotating with ClassVar prevents the warning + class NonWarningModel(BaseModel): + x: ClassVar = 1 + + +def test_untyped_fields_error(): + with pytest.raises(TypeError, match="Field 'a' requires a type annotation"): + + class Model(BaseModel): + a = Field('foobar') + + def test_custom_init_subclass_params(): class DerivedModel(BaseModel): def __init_subclass__(cls, something): @@ -1243,7 +1279,7 @@ def __init_subclass__(cls, something): # to allow the special method __init_subclass__ to be defined with custom # parameters on extended BaseModel classes. class NewModel(DerivedModel, something=2): - something = 1 + something: ClassVar = 1 assert NewModel.something == 2