File size: 28,093 Bytes
bfed822 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | """Private logic related to fields (the `Field()` function and `FieldInfo` class), and arguments to `Annotated`."""
from __future__ import annotations as _annotations
import dataclasses
import warnings
from collections.abc import Mapping
from functools import cache
from inspect import Parameter, ismethoddescriptor, signature
from re import Pattern
from typing import TYPE_CHECKING, Any, Callable, TypeVar
from pydantic_core import PydanticUndefined
from typing_extensions import TypeIs
from typing_inspection.introspection import AnnotationSource
from pydantic import PydanticDeprecatedSince211
from pydantic.errors import PydanticUserError
from ..aliases import AliasGenerator
from . import _generics, _typing_extra
from ._config import ConfigWrapper
from ._docs_extraction import extract_docstrings_from_cls
from ._import_utils import import_cached_base_model, import_cached_field_info
from ._namespace_utils import NsResolver
from ._repr import Representation
from ._utils import can_be_positional, get_first_not_none
if TYPE_CHECKING:
from annotated_types import BaseMetadata
from ..fields import FieldInfo
from ..main import BaseModel
from ._dataclasses import PydanticDataclass, StandardDataclass
from ._decorators import DecoratorInfos
class PydanticMetadata(Representation):
"""Base class for annotation markers like `Strict`."""
__slots__ = ()
def pydantic_general_metadata(**metadata: Any) -> BaseMetadata:
"""Create a new `_PydanticGeneralMetadata` class with the given metadata.
Args:
**metadata: The metadata to add.
Returns:
The new `_PydanticGeneralMetadata` class.
"""
return _general_metadata_cls()(metadata) # type: ignore
@cache
def _general_metadata_cls() -> type[BaseMetadata]:
"""Do it this way to avoid importing `annotated_types` at import time."""
from annotated_types import BaseMetadata
class _PydanticGeneralMetadata(PydanticMetadata, BaseMetadata):
"""Pydantic general metadata like `max_digits`."""
def __init__(self, metadata: Any):
self.__dict__ = metadata
return _PydanticGeneralMetadata # type: ignore
def _check_protected_namespaces(
protected_namespaces: tuple[str | Pattern[str], ...],
ann_name: str,
bases: tuple[type[Any], ...],
cls_name: str,
) -> None:
BaseModel = import_cached_base_model()
for protected_namespace in protected_namespaces:
ns_violation = False
if isinstance(protected_namespace, Pattern):
ns_violation = protected_namespace.match(ann_name) is not None
elif isinstance(protected_namespace, str):
ns_violation = ann_name.startswith(protected_namespace)
if ns_violation:
for b in bases:
if hasattr(b, ann_name):
if not (issubclass(b, BaseModel) and ann_name in getattr(b, '__pydantic_fields__', {})):
raise ValueError(
f'Field {ann_name!r} conflicts with member {getattr(b, ann_name)}'
f' of protected namespace {protected_namespace!r}.'
)
else:
valid_namespaces: list[str] = []
for pn in protected_namespaces:
if isinstance(pn, Pattern):
if not pn.match(ann_name):
valid_namespaces.append(f're.compile({pn.pattern!r})')
else:
if not ann_name.startswith(pn):
valid_namespaces.append(f"'{pn}'")
valid_namespaces_str = f'({", ".join(valid_namespaces)}{",)" if len(valid_namespaces) == 1 else ")"}'
warnings.warn(
f'Field {ann_name!r} in {cls_name!r} conflicts with protected namespace {protected_namespace!r}.\n\n'
f"You may be able to solve this by setting the 'protected_namespaces' configuration to {valid_namespaces_str}.",
UserWarning,
stacklevel=5,
)
def _update_fields_from_docstrings(cls: type[Any], fields: dict[str, FieldInfo], use_inspect: bool = False) -> None:
fields_docs = extract_docstrings_from_cls(cls, use_inspect=use_inspect)
for ann_name, field_info in fields.items():
if field_info.description is None and ann_name in fields_docs:
field_info.description = fields_docs[ann_name]
def _apply_field_title_generator_to_field_info(
title_generator: Callable[[str, FieldInfo], str],
field_name: str,
field_info: FieldInfo,
):
if field_info.title is None:
title = title_generator(field_name, field_info)
if not isinstance(title, str):
raise TypeError(f'field_title_generator {title_generator} must return str, not {title.__class__}')
field_info.title = title
def _apply_alias_generator_to_field_info(
alias_generator: Callable[[str], str] | AliasGenerator, field_name: str, field_info: FieldInfo
):
"""Apply an alias generator to aliases on a `FieldInfo` instance if appropriate.
Args:
alias_generator: A callable that takes a string and returns a string, or an `AliasGenerator` instance.
field_name: The name of the field from which to generate the alias.
field_info: The `FieldInfo` instance to which the alias generator is (maybe) applied.
"""
# Apply an alias_generator if
# 1. An alias is not specified
# 2. An alias is specified, but the priority is <= 1
if (
field_info.alias_priority is None
or field_info.alias_priority <= 1
or field_info.alias is None
or field_info.validation_alias is None
or field_info.serialization_alias is None
):
alias, validation_alias, serialization_alias = None, None, None
if isinstance(alias_generator, AliasGenerator):
alias, validation_alias, serialization_alias = alias_generator.generate_aliases(field_name)
elif callable(alias_generator):
alias = alias_generator(field_name)
if not isinstance(alias, str):
raise TypeError(f'alias_generator {alias_generator} must return str, not {alias.__class__}')
# if priority is not set, we set to 1
# which supports the case where the alias_generator from a child class is used
# to generate an alias for a field in a parent class
if field_info.alias_priority is None or field_info.alias_priority <= 1:
field_info.alias_priority = 1
# if the priority is 1, then we set the aliases to the generated alias
if field_info.alias_priority == 1:
field_info.serialization_alias = get_first_not_none(serialization_alias, alias)
field_info.validation_alias = get_first_not_none(validation_alias, alias)
field_info.alias = alias
# if any of the aliases are not set, then we set them to the corresponding generated alias
if field_info.alias is None:
field_info.alias = alias
if field_info.serialization_alias is None:
field_info.serialization_alias = get_first_not_none(serialization_alias, alias)
if field_info.validation_alias is None:
field_info.validation_alias = get_first_not_none(validation_alias, alias)
def update_field_from_config(config_wrapper: ConfigWrapper, field_name: str, field_info: FieldInfo) -> None:
"""Update the `FieldInfo` instance from the configuration set on the model it belongs to.
This will apply the title and alias generators from the configuration.
Args:
config_wrapper: The configuration from the model.
field_name: The field name the `FieldInfo` instance is attached to.
field_info: The `FieldInfo` instance to update.
"""
field_title_generator = field_info.field_title_generator or config_wrapper.field_title_generator
if field_title_generator is not None:
_apply_field_title_generator_to_field_info(field_title_generator, field_name, field_info)
if config_wrapper.alias_generator is not None:
_apply_alias_generator_to_field_info(config_wrapper.alias_generator, field_name, field_info)
_deprecated_method_names = {'dict', 'json', 'copy', '_iter', '_copy_and_set_values', '_calculate_keys'}
_deprecated_classmethod_names = {
'parse_obj',
'parse_raw',
'parse_file',
'from_orm',
'construct',
'schema',
'schema_json',
'validate',
'update_forward_refs',
'_get_value',
}
def collect_model_fields( # noqa: C901
cls: type[BaseModel],
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver | None,
*,
typevars_map: Mapping[TypeVar, Any] | None = None,
) -> tuple[dict[str, FieldInfo], set[str]]:
"""Collect the fields and class variables names of a nascent Pydantic model.
The fields collection process is *lenient*, meaning it won't error if string annotations
fail to evaluate. If this happens, the original annotation (and assigned value, if any)
is stored on the created `FieldInfo` instance.
The `rebuild_model_fields()` should be called at a later point (e.g. when rebuilding the model),
and will make use of these stored attributes.
Args:
cls: BaseModel or dataclass.
config_wrapper: The config wrapper instance.
ns_resolver: Namespace resolver to use when getting model annotations.
typevars_map: A dictionary mapping type variables to their concrete types.
Returns:
A two-tuple containing model fields and class variables names.
Raises:
NameError:
- If there is a conflict between a field name and protected namespaces.
- If there is a field other than `root` in `RootModel`.
- If a field shadows an attribute in the parent model.
"""
FieldInfo_ = import_cached_field_info()
BaseModel_ = import_cached_base_model()
bases = cls.__bases__
parent_fields_lookup: dict[str, FieldInfo] = {}
for base in reversed(bases):
if model_fields := getattr(base, '__pydantic_fields__', None):
parent_fields_lookup.update(model_fields)
type_hints = _typing_extra.get_model_type_hints(cls, ns_resolver=ns_resolver)
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
# annotations is only used for finding fields in parent classes
annotations = _typing_extra.safe_get_annotations(cls)
fields: dict[str, FieldInfo] = {}
class_vars: set[str] = set()
for ann_name, (ann_type, evaluated) in type_hints.items():
if ann_name == 'model_config':
# We never want to treat `model_config` as a field
# Note: we may need to change this logic if/when we introduce a `BareModel` class with no
# protected namespaces (where `model_config` might be allowed as a field name)
continue
_check_protected_namespaces(
protected_namespaces=config_wrapper.protected_namespaces,
ann_name=ann_name,
bases=bases,
cls_name=cls.__name__,
)
if _typing_extra.is_classvar_annotation(ann_type):
class_vars.add(ann_name)
continue
assigned_value = getattr(cls, ann_name, PydanticUndefined)
if assigned_value is not PydanticUndefined and (
# One of the deprecated instance methods was used as a field name (e.g. `dict()`):
any(getattr(BaseModel_, depr_name, None) is assigned_value for depr_name in _deprecated_method_names)
# One of the deprecated class methods was used as a field name (e.g. `schema()`):
or (
hasattr(assigned_value, '__func__')
and any(
getattr(getattr(BaseModel_, depr_name, None), '__func__', None) is assigned_value.__func__ # pyright: ignore[reportAttributeAccessIssue]
for depr_name in _deprecated_classmethod_names
)
)
):
# Then `assigned_value` would be the method, even though no default was specified:
assigned_value = PydanticUndefined
if not is_valid_field_name(ann_name):
continue
if cls.__pydantic_root_model__ and ann_name != 'root':
raise NameError(
f"Unexpected field with name {ann_name!r}; only 'root' is allowed as a field of a `RootModel`"
)
# when building a generic model with `MyModel[int]`, the generic_origin check makes sure we don't get
# "... shadows an attribute" warnings
generic_origin = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin')
for base in bases:
dataclass_fields = {
field.name for field in (dataclasses.fields(base) if dataclasses.is_dataclass(base) else ())
}
if hasattr(base, ann_name):
if base is generic_origin:
# Don't warn when "shadowing" of attributes in parametrized generics
continue
if ann_name in dataclass_fields:
# Don't warn when inheriting stdlib dataclasses whose fields are "shadowed" by defaults being set
# on the class instance.
continue
if ann_name not in annotations:
# Don't warn when a field exists in a parent class but has not been defined in the current class
continue
warnings.warn(
f'Field name "{ann_name}" in "{cls.__qualname__}" shadows an attribute in parent '
f'"{base.__qualname__}"',
UserWarning,
stacklevel=4,
)
if assigned_value is PydanticUndefined: # no assignment, just a plain annotation
if ann_name in annotations or ann_name not in parent_fields_lookup:
# field is either:
# - present in the current model's annotations (and *not* from parent classes)
# - not found on any base classes; this seems to be caused by fields bot getting
# generated due to models not being fully defined while initializing recursive models.
# Nothing stops us from just creating a `FieldInfo` for this type hint, so we do this.
field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS)
if not evaluated:
field_info._complete = False
# Store the original annotation that should be used to rebuild
# the field info later:
field_info._original_annotation = ann_type
else:
# The field was present on one of the (possibly multiple) base classes
# copy the field to make sure typevar substitutions don't cause issues with the base classes
field_info = parent_fields_lookup[ann_name]._copy()
else: # An assigned value is present (either the default value, or a `Field()` function)
if isinstance(assigned_value, FieldInfo_) and ismethoddescriptor(assigned_value.default):
# `assigned_value` was fetched using `getattr`, which triggers a call to `__get__`
# for descriptors, so we do the same if the `= field(default=...)` form is used.
# Note that we only do this for method descriptors for now, we might want to
# extend this to any descriptor in the future (by simply checking for
# `hasattr(assigned_value.default, '__get__')`).
default = assigned_value.default.__get__(None, cls)
assigned_value.default = default
assigned_value._attributes_set['default'] = default
field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value, _source=AnnotationSource.CLASS)
# Store the original annotation and assignment value that should be used to rebuild the field info later.
# Note that the assignment is always stored as the annotation might contain a type var that is later
# parameterized with an unknown forward reference (and we'll need it to rebuild the field info):
field_info._original_assignment = assigned_value
if not evaluated:
field_info._complete = False
field_info._original_annotation = ann_type
elif 'final' in field_info._qualifiers and not field_info.is_required():
warnings.warn(
f'Annotation {ann_name!r} is marked as final and has a default value. Pydantic treats {ann_name!r} as a '
'class variable, but it will be considered as a normal field in V3 to be aligned with dataclasses. If you '
f'still want {ann_name!r} to be considered as a class variable, annotate it as: `ClassVar[<type>] = <default>.`',
category=PydanticDeprecatedSince211,
# Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case:
stacklevel=4,
)
class_vars.add(ann_name)
continue
# attributes which are fields are removed from the class namespace:
# 1. To match the behaviour of annotation-only fields
# 2. To avoid false positives in the NameError check above
try:
delattr(cls, ann_name)
except AttributeError:
pass # indicates the attribute was on a parent class
# Use cls.__dict__['__pydantic_decorators__'] instead of cls.__pydantic_decorators__
# to make sure the decorators have already been built for this exact class
decorators: DecoratorInfos = cls.__dict__['__pydantic_decorators__']
if ann_name in decorators.computed_fields:
raise TypeError(
f'Field {ann_name!r} of class {cls.__name__!r} overrides symbol of same name in a parent class. '
'This override with a computed_field is incompatible.'
)
fields[ann_name] = field_info
if field_info._complete:
# If not complete, this will be called in `rebuild_model_fields()`:
update_field_from_config(config_wrapper, ann_name, field_info)
if typevars_map:
for field in fields.values():
if field._complete:
field.apply_typevars_map(typevars_map)
if config_wrapper.use_attribute_docstrings:
_update_fields_from_docstrings(cls, fields)
return fields, class_vars
def rebuild_model_fields(
cls: type[BaseModel],
*,
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
typevars_map: Mapping[TypeVar, Any],
) -> dict[str, FieldInfo]:
"""Rebuild the (already present) model fields by trying to reevaluate annotations.
This function should be called whenever a model with incomplete fields is encountered.
Raises:
NameError: If one of the annotations failed to evaluate.
Note:
This function *doesn't* mutate the model fields in place, as it can be called during
schema generation, where you don't want to mutate other model's fields.
"""
FieldInfo_ = import_cached_field_info()
rebuilt_fields: dict[str, FieldInfo] = {}
with ns_resolver.push(cls):
for f_name, field_info in cls.__pydantic_fields__.items():
if field_info._complete:
rebuilt_fields[f_name] = field_info
else:
existing_desc = field_info.description
ann = _typing_extra.eval_type(
field_info._original_annotation,
*ns_resolver.types_namespace,
)
ann = _generics.replace_types(ann, typevars_map)
if (assign := field_info._original_assignment) is PydanticUndefined:
new_field = FieldInfo_.from_annotation(ann, _source=AnnotationSource.CLASS)
else:
new_field = FieldInfo_.from_annotated_attribute(ann, assign, _source=AnnotationSource.CLASS)
# The description might come from the docstring if `use_attribute_docstrings` was `True`:
new_field.description = new_field.description if new_field.description is not None else existing_desc
update_field_from_config(config_wrapper, f_name, new_field)
rebuilt_fields[f_name] = new_field
return rebuilt_fields
def collect_dataclass_fields(
cls: type[StandardDataclass],
*,
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver | None = None,
typevars_map: dict[Any, Any] | None = None,
) -> dict[str, FieldInfo]:
"""Collect the fields of a dataclass.
Args:
cls: dataclass.
config_wrapper: The config wrapper instance.
ns_resolver: Namespace resolver to use when getting dataclass annotations.
Defaults to an empty instance.
typevars_map: A dictionary mapping type variables to their concrete types.
Returns:
The dataclass fields.
"""
FieldInfo_ = import_cached_field_info()
fields: dict[str, FieldInfo] = {}
ns_resolver = ns_resolver or NsResolver()
dataclass_fields = cls.__dataclass_fields__
# The logic here is similar to `_typing_extra.get_cls_type_hints`,
# although we do it manually as stdlib dataclasses already have annotations
# collected in each class:
for base in reversed(cls.__mro__):
if not dataclasses.is_dataclass(base):
continue
with ns_resolver.push(base):
for ann_name, dataclass_field in dataclass_fields.items():
base_anns = _typing_extra.safe_get_annotations(base)
if ann_name not in base_anns:
# `__dataclass_fields__`contains every field, even the ones from base classes.
# Only collect the ones defined on `base`.
continue
globalns, localns = ns_resolver.types_namespace
ann_type, evaluated = _typing_extra.try_eval_type(dataclass_field.type, globalns, localns)
if _typing_extra.is_classvar_annotation(ann_type):
continue
if (
not dataclass_field.init
and dataclass_field.default is dataclasses.MISSING
and dataclass_field.default_factory is dataclasses.MISSING
):
# TODO: We should probably do something with this so that validate_assignment behaves properly
# Issue: https://github.com/pydantic/pydantic/issues/5470
continue
if isinstance(dataclass_field.default, FieldInfo_):
if dataclass_field.default.init_var:
if dataclass_field.default.init is False:
raise PydanticUserError(
f'Dataclass field {ann_name} has init=False and init_var=True, but these are mutually exclusive.',
code='clashing-init-and-init-var',
)
# TODO: same note as above re validate_assignment
continue
field_info = FieldInfo_.from_annotated_attribute(
ann_type, dataclass_field.default, _source=AnnotationSource.DATACLASS
)
field_info._original_assignment = dataclass_field.default
else:
field_info = FieldInfo_.from_annotated_attribute(
ann_type, dataclass_field, _source=AnnotationSource.DATACLASS
)
field_info._original_assignment = dataclass_field
if not evaluated:
field_info._complete = False
field_info._original_annotation = ann_type
fields[ann_name] = field_info
update_field_from_config(config_wrapper, ann_name, field_info)
if field_info.default is not PydanticUndefined and isinstance(
getattr(cls, ann_name, field_info), FieldInfo_
):
# We need this to fix the default when the "default" from __dataclass_fields__ is a pydantic.FieldInfo
setattr(cls, ann_name, field_info.default)
if typevars_map:
for field in fields.values():
# We don't pass any ns, as `field.annotation`
# was already evaluated. TODO: is this method relevant?
# Can't we juste use `_generics.replace_types`?
field.apply_typevars_map(typevars_map)
if config_wrapper.use_attribute_docstrings:
_update_fields_from_docstrings(
cls,
fields,
# We can't rely on the (more reliable) frame inspection method
# for stdlib dataclasses:
use_inspect=not hasattr(cls, '__is_pydantic_dataclass__'),
)
return fields
def rebuild_dataclass_fields(
cls: type[PydanticDataclass],
*,
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
typevars_map: Mapping[TypeVar, Any],
) -> dict[str, FieldInfo]:
"""Rebuild the (already present) dataclass fields by trying to reevaluate annotations.
This function should be called whenever a dataclass with incomplete fields is encountered.
Raises:
NameError: If one of the annotations failed to evaluate.
Note:
This function *doesn't* mutate the dataclass fields in place, as it can be called during
schema generation, where you don't want to mutate other dataclass's fields.
"""
FieldInfo_ = import_cached_field_info()
rebuilt_fields: dict[str, FieldInfo] = {}
with ns_resolver.push(cls):
for f_name, field_info in cls.__pydantic_fields__.items():
if field_info._complete:
rebuilt_fields[f_name] = field_info
else:
existing_desc = field_info.description
ann = _typing_extra.eval_type(
field_info._original_annotation,
*ns_resolver.types_namespace,
)
ann = _generics.replace_types(ann, typevars_map)
new_field = FieldInfo_.from_annotated_attribute(
ann,
field_info._original_assignment,
_source=AnnotationSource.DATACLASS,
)
# The description might come from the docstring if `use_attribute_docstrings` was `True`:
new_field.description = new_field.description if new_field.description is not None else existing_desc
update_field_from_config(config_wrapper, f_name, new_field)
rebuilt_fields[f_name] = new_field
return rebuilt_fields
def is_valid_field_name(name: str) -> bool:
return not name.startswith('_')
def is_valid_privateattr_name(name: str) -> bool:
return name.startswith('_') and not name.startswith('__')
def takes_validated_data_argument(
default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any],
) -> TypeIs[Callable[[dict[str, Any]], Any]]:
"""Whether the provided default factory callable has a validated data parameter."""
try:
sig = signature(default_factory)
except (ValueError, TypeError):
# `inspect.signature` might not be able to infer a signature, e.g. with C objects.
# In this case, we assume no data argument is present:
return False
parameters = list(sig.parameters.values())
return len(parameters) == 1 and can_be_positional(parameters[0]) and parameters[0].default is Parameter.empty
|