pyprig 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyprig might be problematic. Click here for more details.

Files changed (112) hide show
  1. pyprig/__init__.py +464 -0
  2. pyprig/_internal/__init__.py +0 -0
  3. pyprig/_internal/_config.py +408 -0
  4. pyprig/_internal/_core_metadata.py +97 -0
  5. pyprig/_internal/_core_utils.py +156 -0
  6. pyprig/_internal/_dataclasses.py +327 -0
  7. pyprig/_internal/_decorators.py +906 -0
  8. pyprig/_internal/_decorators_v1.py +174 -0
  9. pyprig/_internal/_discriminated_union.py +510 -0
  10. pyprig/_internal/_docs_extraction.py +113 -0
  11. pyprig/_internal/_fields.py +883 -0
  12. pyprig/_internal/_forward_ref.py +23 -0
  13. pyprig/_internal/_generate_schema.py +2980 -0
  14. pyprig/_internal/_generics.py +553 -0
  15. pyprig/_internal/_git.py +27 -0
  16. pyprig/_internal/_import_utils.py +20 -0
  17. pyprig/_internal/_known_annotated_metadata.py +405 -0
  18. pyprig/_internal/_mock_val_ser.py +237 -0
  19. pyprig/_internal/_model_construction.py +833 -0
  20. pyprig/_internal/_namespace_utils.py +299 -0
  21. pyprig/_internal/_repr.py +122 -0
  22. pyprig/_internal/_schema_gather.py +49 -0
  23. pyprig/_internal/_schema_generation_shared.py +126 -0
  24. pyprig/_internal/_serializers.py +53 -0
  25. pyprig/_internal/_signature.py +193 -0
  26. pyprig/_internal/_type_refs.py +128 -0
  27. pyprig/_internal/_typing_extra.py +502 -0
  28. pyprig/_internal/_utils.py +433 -0
  29. pyprig/_internal/_validate_call.py +141 -0
  30. pyprig/_internal/_validators.py +541 -0
  31. pyprig/_migration.py +317 -0
  32. pyprig/alias_generators.py +62 -0
  33. pyprig/aliases.py +136 -0
  34. pyprig/annotated_handlers.py +123 -0
  35. pyprig/class_validators.py +5 -0
  36. pyprig/color.py +605 -0
  37. pyprig/config.py +1295 -0
  38. pyprig/dataclasses.py +385 -0
  39. pyprig/datetime_parse.py +5 -0
  40. pyprig/decorator.py +5 -0
  41. pyprig/deprecated/__init__.py +0 -0
  42. pyprig/deprecated/class_validators.py +257 -0
  43. pyprig/deprecated/config.py +72 -0
  44. pyprig/deprecated/copy_internals.py +224 -0
  45. pyprig/deprecated/decorator.py +286 -0
  46. pyprig/deprecated/json.py +142 -0
  47. pyprig/deprecated/parse.py +81 -0
  48. pyprig/deprecated/tools.py +104 -0
  49. pyprig/env_settings.py +5 -0
  50. pyprig/error_wrappers.py +5 -0
  51. pyprig/errors.py +185 -0
  52. pyprig/experimental/__init__.py +1 -0
  53. pyprig/experimental/arguments_schema.py +44 -0
  54. pyprig/experimental/missing_sentinel.py +29 -0
  55. pyprig/experimental/pipeline.py +684 -0
  56. pyprig/fields.py +2065 -0
  57. pyprig/functional_serializers.py +461 -0
  58. pyprig/functional_validators.py +920 -0
  59. pyprig/generics.py +5 -0
  60. pyprig/help.py +68 -0
  61. pyprig/json.py +5 -0
  62. pyprig/json_schema.py +3253 -0
  63. pyprig/main.py +1942 -0
  64. pyprig/mypy.py +1465 -0
  65. pyprig/networks.py +1417 -0
  66. pyprig/parse.py +5 -0
  67. pyprig/plugin/__init__.py +190 -0
  68. pyprig/plugin/_loader.py +58 -0
  69. pyprig/plugin/_schema_validator.py +152 -0
  70. pyprig/py.typed +0 -0
  71. pyprig/root_model.py +186 -0
  72. pyprig/schema.py +5 -0
  73. pyprig/tools.py +5 -0
  74. pyprig/type_adapter.py +776 -0
  75. pyprig/types.py +3405 -0
  76. pyprig/typing.py +5 -0
  77. pyprig/utils.py +5 -0
  78. pyprig/v1/__init__.py +131 -0
  79. pyprig/v1/_hypothesis_plugin.py +391 -0
  80. pyprig/v1/annotated_types.py +72 -0
  81. pyprig/v1/class_validators.py +361 -0
  82. pyprig/v1/color.py +494 -0
  83. pyprig/v1/config.py +191 -0
  84. pyprig/v1/dataclasses.py +500 -0
  85. pyprig/v1/datetime_parse.py +248 -0
  86. pyprig/v1/decorator.py +264 -0
  87. pyprig/v1/env_settings.py +350 -0
  88. pyprig/v1/error_wrappers.py +161 -0
  89. pyprig/v1/errors.py +646 -0
  90. pyprig/v1/fields.py +1253 -0
  91. pyprig/v1/generics.py +400 -0
  92. pyprig/v1/json.py +112 -0
  93. pyprig/v1/main.py +1130 -0
  94. pyprig/v1/mypy.py +949 -0
  95. pyprig/v1/networks.py +747 -0
  96. pyprig/v1/parse.py +66 -0
  97. pyprig/v1/py.typed +0 -0
  98. pyprig/v1/schema.py +1163 -0
  99. pyprig/v1/tools.py +92 -0
  100. pyprig/v1/types.py +1205 -0
  101. pyprig/v1/typing.py +627 -0
  102. pyprig/v1/utils.py +807 -0
  103. pyprig/v1/validators.py +768 -0
  104. pyprig/v1/version.py +38 -0
  105. pyprig/validate_call_decorator.py +121 -0
  106. pyprig/validators.py +5 -0
  107. pyprig/version.py +118 -0
  108. pyprig/warnings.py +130 -0
  109. pyprig-1.2.0.dist-info/METADATA +111 -0
  110. pyprig-1.2.0.dist-info/RECORD +112 -0
  111. pyprig-1.2.0.dist-info/WHEEL +4 -0
  112. pyprig-1.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,408 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ import warnings
4
+ from collections.abc import Callable
5
+ from contextlib import contextmanager
6
+ from re import Pattern
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Literal,
11
+ cast,
12
+ )
13
+
14
+ from pydantic_core import core_schema
15
+ from typing_extensions import Self
16
+
17
+ from ..aliases import AliasGenerator
18
+ from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable
19
+ from ..errors import PydanticUserError
20
+ from ..warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince210
21
+
22
+ if TYPE_CHECKING:
23
+ from .._internal._schema_generation_shared import GenerateSchema
24
+ from ..fields import ComputedFieldInfo, FieldInfo
25
+
26
+ DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.'
27
+
28
+ # Maps the `ConfigDict` keys that have a `CoreConfig` counterpart to the corresponding
29
+ # `CoreConfig` key (`'title'` is omitted as it requires special handling, see `ConfigWrapper.core_config()`):
30
+ _config_dict_to_core_config_key: dict[str, str] = {
31
+ 'extra': 'extra_fields_behavior',
32
+ 'allow_inf_nan': 'allow_inf_nan',
33
+ 'str_strip_whitespace': 'str_strip_whitespace',
34
+ 'str_to_lower': 'str_to_lower',
35
+ 'str_to_upper': 'str_to_upper',
36
+ 'strict': 'strict',
37
+ 'ser_json_timedelta': 'ser_json_timedelta',
38
+ 'ser_json_temporal': 'ser_json_temporal',
39
+ 'val_temporal_unit': 'val_temporal_unit',
40
+ 'ser_json_bytes': 'ser_json_bytes',
41
+ 'val_json_bytes': 'val_json_bytes',
42
+ 'ser_json_inf_nan': 'ser_json_inf_nan',
43
+ 'from_attributes': 'from_attributes',
44
+ 'loc_by_alias': 'loc_by_alias',
45
+ 'revalidate_instances': 'revalidate_instances',
46
+ 'validate_default': 'validate_default',
47
+ 'str_max_length': 'str_max_length',
48
+ 'str_min_length': 'str_min_length',
49
+ 'hide_input_in_errors': 'hide_input_in_errors',
50
+ 'coerce_numbers_to_str': 'coerce_numbers_to_str',
51
+ 'regex_engine': 'regex_engine',
52
+ 'validation_error_cause': 'validation_error_cause',
53
+ 'cache_strings': 'cache_strings',
54
+ 'validate_by_alias': 'validate_by_alias',
55
+ 'validate_by_name': 'validate_by_name',
56
+ 'serialize_by_alias': 'serialize_by_alias',
57
+ 'url_preserve_empty_path': 'url_preserve_empty_path',
58
+ 'polymorphic_serialization': 'polymorphic_serialization',
59
+ }
60
+
61
+
62
+ class ConfigWrapper:
63
+ """Internal wrapper for Config which exposes ConfigDict items as attributes."""
64
+
65
+ __slots__ = ('config_dict', 'effective_config')
66
+
67
+ config_dict: ConfigDict
68
+ """The configuration as declared by the user.
69
+
70
+ This is the mapping exposed as the `model_config` attribute of models (and the
71
+ `__pydantic_config__` attribute of dataclasses), and the one merged with the
72
+ configuration of the bases when subclassing. As such, it must never be mutated.
73
+ """
74
+
75
+ effective_config: ConfigDict
76
+ """The configuration actually in effect, with the deprecated settings applied.
77
+
78
+ This is what every consumer of the configuration should read (both the attribute
79
+ access on this wrapper and `core_config()` do), as `config_dict` alone does not
80
+ account for the deprecated settings (see `_build_effective_config()`).
81
+ """
82
+
83
+ # all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they
84
+ # stop matching
85
+ title: str | None
86
+ str_to_lower: bool
87
+ str_to_upper: bool
88
+ str_strip_whitespace: bool
89
+ str_min_length: int
90
+ str_max_length: int | None
91
+ extra: ExtraValues | None
92
+ frozen: bool
93
+ populate_by_name: bool
94
+ use_enum_values: bool
95
+ validate_assignment: bool
96
+ arbitrary_types_allowed: bool
97
+ from_attributes: bool
98
+ # whether to use the actual key provided in the data (e.g. alias or first alias for "field required" errors) instead of field_names
99
+ # to construct error `loc`s, default `True`
100
+ loc_by_alias: bool
101
+ alias_generator: Callable[[str], str] | AliasGenerator | None
102
+ model_title_generator: Callable[[type], str] | None
103
+ field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None
104
+ ignored_types: tuple[type, ...]
105
+ allow_inf_nan: bool
106
+ json_schema_extra: JsonDict | JsonSchemaExtraCallable | None
107
+ json_encoders: dict[type[object], JsonEncoder] | None
108
+
109
+ # new in V2
110
+ strict: bool
111
+ # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
112
+ revalidate_instances: Literal['always', 'never', 'subclass-instances']
113
+ ser_json_timedelta: Literal['iso8601', 'float']
114
+ ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds']
115
+ val_temporal_unit: Literal['seconds', 'milliseconds', 'infer']
116
+ ser_json_bytes: Literal['utf8', 'base64', 'hex']
117
+ val_json_bytes: Literal['utf8', 'base64', 'hex']
118
+ ser_json_inf_nan: Literal['null', 'constants', 'strings']
119
+ # whether to validate default values during validation, default False
120
+ validate_default: bool
121
+ validate_return: bool
122
+ protected_namespaces: tuple[str | Pattern[str], ...]
123
+ hide_input_in_errors: bool
124
+ defer_build: bool
125
+ plugin_settings: dict[str, object] | None
126
+ schema_generator: type[GenerateSchema] | None
127
+ json_schema_serialization_defaults_required: bool
128
+ json_schema_mode_override: Literal['validation', 'serialization', None]
129
+ coerce_numbers_to_str: bool
130
+ regex_engine: Literal['rust-regex', 'python-re']
131
+ validation_error_cause: bool
132
+ use_attribute_docstrings: bool
133
+ cache_strings: bool | Literal['all', 'keys', 'none']
134
+ validate_by_alias: bool
135
+ validate_by_name: bool
136
+ serialize_by_alias: bool
137
+ url_preserve_empty_path: bool
138
+ polymorphic_serialization: bool
139
+
140
+ def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True):
141
+ if check:
142
+ self.config_dict = prepare_config(config)
143
+ else:
144
+ self.config_dict = cast(ConfigDict, config)
145
+ self.effective_config = _build_effective_config(self.config_dict)
146
+
147
+ @classmethod
148
+ def for_model(
149
+ cls,
150
+ bases: tuple[type[Any], ...],
151
+ namespace: dict[str, Any],
152
+ kwargs: dict[str, Any],
153
+ ) -> Self:
154
+ """Build a new `ConfigWrapper` instance for a `BaseModel`.
155
+
156
+ The config wrapper built based on (in descending order of priority):
157
+ - options from `kwargs`
158
+ - options from the `namespace`
159
+ - options from the base classes (`bases`)
160
+
161
+ Args:
162
+ bases: A tuple of base classes.
163
+ namespace: The namespace of the class being created.
164
+ kwargs: The kwargs passed to the class being created.
165
+
166
+ Returns:
167
+ A `ConfigWrapper` instance for `BaseModel`.
168
+ """
169
+ config_new = ConfigDict()
170
+ for base in bases:
171
+ config = getattr(base, 'model_config', None)
172
+ if config:
173
+ config_new.update(config.copy())
174
+
175
+ config_class_from_namespace = namespace.get('Config')
176
+ config_dict_from_namespace = namespace.get('model_config')
177
+
178
+ if config_class_from_namespace and config_dict_from_namespace:
179
+ raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both')
180
+
181
+ config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace)
182
+
183
+ config_new.update(config_from_namespace)
184
+
185
+ for k in list(kwargs.keys()):
186
+ if k in config_keys:
187
+ config_new[k] = kwargs.pop(k)
188
+
189
+ return cls(config_new)
190
+
191
+ # we don't show `__getattr__` to type checkers so missing attributes cause errors
192
+ if not TYPE_CHECKING: # pragma: no branch
193
+
194
+ def __getattr__(self, name: str) -> Any:
195
+ try:
196
+ return self.effective_config[name]
197
+ except KeyError:
198
+ try:
199
+ return config_defaults[name]
200
+ except KeyError:
201
+ raise AttributeError(f'Config has no attribute {name!r}') from None
202
+
203
+ def core_config(self, title: str | None) -> core_schema.CoreConfig:
204
+ """Create a pydantic-core config.
205
+
206
+ We don't use getattr here since we don't want to populate with defaults.
207
+
208
+ Args:
209
+ title: The title to use if not set in config.
210
+
211
+ Returns:
212
+ A `CoreConfig` object created from config.
213
+ """
214
+ config = self.effective_config
215
+
216
+ if not config:
217
+ # Fast path for the common case of an empty (default) config:
218
+ return core_schema.CoreConfig(title=title) if title else core_schema.CoreConfig()
219
+
220
+ core_config_values: dict[str, Any] = {}
221
+ for k, v in config.items():
222
+ if v is not None and (core_config_key := _config_dict_to_core_config_key.get(k)) is not None:
223
+ core_config_values[core_config_key] = v
224
+ title = config.get('title') or title
225
+ if title:
226
+ core_config_values['title'] = title
227
+
228
+ return cast(core_schema.CoreConfig, core_config_values)
229
+
230
+ def __repr__(self):
231
+ c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items())
232
+ return f'ConfigWrapper({c})'
233
+
234
+
235
+ class ConfigWrapperStack:
236
+ """A stack of `ConfigWrapper` instances."""
237
+
238
+ def __init__(self, config_wrapper: ConfigWrapper):
239
+ self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper]
240
+
241
+ @property
242
+ def tail(self) -> ConfigWrapper:
243
+ return self._config_wrapper_stack[-1]
244
+
245
+ @contextmanager
246
+ def push(self, config_wrapper: ConfigWrapper | ConfigDict | None):
247
+ if config_wrapper is None:
248
+ yield
249
+ return
250
+
251
+ if not isinstance(config_wrapper, ConfigWrapper):
252
+ config_wrapper = ConfigWrapper(config_wrapper, check=False)
253
+
254
+ self._config_wrapper_stack.append(config_wrapper)
255
+ try:
256
+ yield
257
+ finally:
258
+ self._config_wrapper_stack.pop()
259
+
260
+
261
+ config_defaults = ConfigDict(
262
+ title=None,
263
+ str_to_lower=False,
264
+ str_to_upper=False,
265
+ str_strip_whitespace=False,
266
+ str_min_length=0,
267
+ str_max_length=None,
268
+ # let the model / dataclass decide how to handle it
269
+ extra=None,
270
+ frozen=False,
271
+ populate_by_name=False,
272
+ use_enum_values=False,
273
+ validate_assignment=False,
274
+ arbitrary_types_allowed=False,
275
+ from_attributes=False,
276
+ loc_by_alias=True,
277
+ alias_generator=None,
278
+ model_title_generator=None,
279
+ field_title_generator=None,
280
+ ignored_types=(),
281
+ allow_inf_nan=True,
282
+ json_schema_extra=None,
283
+ strict=False,
284
+ revalidate_instances='never',
285
+ ser_json_timedelta='iso8601',
286
+ ser_json_temporal='iso8601',
287
+ val_temporal_unit='infer',
288
+ ser_json_bytes='utf8',
289
+ val_json_bytes='utf8',
290
+ ser_json_inf_nan='null',
291
+ validate_default=False,
292
+ validate_return=False,
293
+ protected_namespaces=('model_validate', 'model_dump'),
294
+ hide_input_in_errors=False,
295
+ json_encoders=None,
296
+ defer_build=False,
297
+ schema_generator=None,
298
+ plugin_settings=None,
299
+ json_schema_serialization_defaults_required=False,
300
+ json_schema_mode_override=None,
301
+ coerce_numbers_to_str=False,
302
+ regex_engine='rust-regex',
303
+ validation_error_cause=False,
304
+ use_attribute_docstrings=False,
305
+ cache_strings=True,
306
+ validate_by_alias=True,
307
+ validate_by_name=False,
308
+ serialize_by_alias=False,
309
+ url_preserve_empty_path=False,
310
+ polymorphic_serialization=False,
311
+ )
312
+
313
+
314
+ def _build_effective_config(config: ConfigDict) -> ConfigDict:
315
+ """Build the configuration in effect from the configuration as declared.
316
+
317
+ Raises:
318
+ PydanticUserError: If validation can happen by neither alias nor name.
319
+ """
320
+ if config.get('validate_by_name') is None:
321
+ if (populate_by_name := config.get('populate_by_name')) is not None:
322
+ config = cast(ConfigDict, {**config, 'validate_by_alias': True, 'validate_by_name': populate_by_name})
323
+ elif config.get('validate_by_alias') is False:
324
+ config = cast(ConfigDict, {**config, 'validate_by_name': True})
325
+
326
+ if (not config.get('validate_by_alias', True)) and (not config.get('validate_by_name', False)):
327
+ raise PydanticUserError(
328
+ 'At least one of `validate_by_alias` or `validate_by_name` must be set to True.',
329
+ code='validate-by-alias-and-name-false',
330
+ )
331
+
332
+ return config
333
+
334
+
335
+ def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict:
336
+ """Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None.
337
+
338
+ Args:
339
+ config: The input config.
340
+
341
+ Returns:
342
+ A ConfigDict object created from config.
343
+ """
344
+ if config is None:
345
+ return ConfigDict()
346
+
347
+ if not isinstance(config, dict):
348
+ warnings.warn(DEPRECATION_MESSAGE, PydanticDeprecatedSince20, stacklevel=4)
349
+ config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')}
350
+ else:
351
+ # Copy, so that the `ConfigWrapper` owns its configuration and can never
352
+ # mutate the mapping provided by the user:
353
+ config = config.copy()
354
+
355
+ config_dict = cast(ConfigDict, config)
356
+ check_deprecated(config_dict)
357
+ return config_dict
358
+
359
+
360
+ config_keys = set(ConfigDict.__annotations__.keys())
361
+
362
+
363
+ V2_REMOVED_KEYS = {
364
+ 'allow_mutation',
365
+ 'error_msg_templates',
366
+ 'fields',
367
+ 'getter_dict',
368
+ 'smart_union',
369
+ 'underscore_attrs_are_private',
370
+ 'json_loads',
371
+ 'json_dumps',
372
+ 'copy_on_model_validation',
373
+ 'post_init_call',
374
+ }
375
+ V2_RENAMED_KEYS = {
376
+ 'allow_population_by_field_name': 'validate_by_name',
377
+ 'anystr_lower': 'str_to_lower',
378
+ 'anystr_strip_whitespace': 'str_strip_whitespace',
379
+ 'anystr_upper': 'str_to_upper',
380
+ 'keep_untouched': 'ignored_types',
381
+ 'max_anystr_length': 'str_max_length',
382
+ 'min_anystr_length': 'str_min_length',
383
+ 'orm_mode': 'from_attributes',
384
+ 'schema_extra': 'json_schema_extra',
385
+ 'validate_all': 'validate_default',
386
+ }
387
+
388
+
389
+ def check_deprecated(config_dict: ConfigDict) -> None:
390
+ """Check for deprecated config keys and warn the user.
391
+
392
+ Args:
393
+ config_dict: The input config.
394
+ """
395
+ if config_dict.get('schema_generator') is not None:
396
+ warnings.warn(
397
+ 'The `schema_generator` setting has been deprecated since v2.10. This setting no longer has any effect.',
398
+ PydanticDeprecatedSince210,
399
+ )
400
+
401
+ deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys()
402
+ deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys()
403
+ if deprecated_removed_keys or deprecated_renamed_keys:
404
+ renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)}
405
+ renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()]
406
+ removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)]
407
+ message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets)
408
+ warnings.warn(message, UserWarning)
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
4
+ from warnings import warn
5
+
6
+ if TYPE_CHECKING:
7
+ from ..config import JsonDict, JsonSchemaExtraCallable
8
+ from ._schema_generation_shared import (
9
+ GetJsonSchemaFunction,
10
+ )
11
+
12
+
13
+ class CoreMetadata(TypedDict, total=False):
14
+ """A `TypedDict` for holding the metadata dict of the schema.
15
+
16
+ Attributes:
17
+ pydantic_js_functions: List of JSON schema functions that resolve refs during application.
18
+ pydantic_js_annotation_functions: List of JSON schema functions that don't resolve refs during application.
19
+ pydantic_js_prefer_positional_arguments: Whether JSON schema generator will
20
+ prefer positional over keyword arguments for an 'arguments' schema.
21
+ custom validation function. Only applies to before, plain, and wrap validators.
22
+ pydantic_js_updates: key / value pair updates to apply to the JSON schema for a type.
23
+ pydantic_js_extra: WIP, either key/value pair updates to apply to the JSON schema, or a custom callable.
24
+ pydantic_internal_union_tag_key: Used internally by the `Tag` metadata to specify the tag used for a discriminated union.
25
+ pydantic_internal_union_discriminator: Used internally to specify the discriminator value for a discriminated union
26
+ when the discriminator was applied to a `'definition-ref'` schema, and that reference was missing at the time
27
+ of the annotation application.
28
+
29
+ TODO: Perhaps we should move this structure to pydantic-core. At the moment, though,
30
+ it's easier to iterate on if we leave it in pydantic until we feel there is a semi-stable API.
31
+
32
+ TODO: It's unfortunate how functionally oriented JSON schema generation is, especially that which occurs during
33
+ the core schema generation process. It's inevitable that we need to store some json schema related information
34
+ on core schemas, given that we generate JSON schemas directly from core schemas. That being said, debugging related
35
+ issues is quite difficult when JSON schema information is disguised via dynamically defined functions.
36
+ """
37
+
38
+ pydantic_js_functions: list[GetJsonSchemaFunction]
39
+ pydantic_js_annotation_functions: list[GetJsonSchemaFunction]
40
+ pydantic_js_prefer_positional_arguments: bool
41
+ pydantic_js_updates: JsonDict
42
+ pydantic_js_extra: JsonDict | JsonSchemaExtraCallable
43
+ pydantic_internal_union_tag_key: str
44
+ pydantic_internal_union_discriminator: str
45
+
46
+
47
+ def update_core_metadata(
48
+ core_metadata: Any,
49
+ /,
50
+ *,
51
+ pydantic_js_functions: list[GetJsonSchemaFunction] | None = None,
52
+ pydantic_js_annotation_functions: list[GetJsonSchemaFunction] | None = None,
53
+ pydantic_js_updates: JsonDict | None = None,
54
+ pydantic_js_extra: JsonDict | JsonSchemaExtraCallable | None = None,
55
+ ) -> None:
56
+ from ..json_schema import PydanticJsonSchemaWarning
57
+
58
+ """Update CoreMetadata instance in place. When we make modifications in this function, they
59
+ take effect on the `core_metadata` reference passed in as the first (and only) positional argument.
60
+
61
+ First, cast to `CoreMetadata`, then finish with a cast to `dict[str, Any]` for core schema compatibility.
62
+ We do this here, instead of before / after each call to this function so that this typing hack
63
+ can be easily removed if/when we move `CoreMetadata` to `pydantic-core`.
64
+
65
+ For parameter descriptions, see `CoreMetadata` above.
66
+ """
67
+ core_metadata = cast(CoreMetadata, core_metadata)
68
+
69
+ if pydantic_js_functions:
70
+ core_metadata.setdefault('pydantic_js_functions', []).extend(pydantic_js_functions)
71
+
72
+ if pydantic_js_annotation_functions:
73
+ core_metadata.setdefault('pydantic_js_annotation_functions', []).extend(pydantic_js_annotation_functions)
74
+
75
+ if pydantic_js_updates:
76
+ if (existing_updates := core_metadata.get('pydantic_js_updates')) is not None:
77
+ core_metadata['pydantic_js_updates'] = {**existing_updates, **pydantic_js_updates}
78
+ else:
79
+ core_metadata['pydantic_js_updates'] = pydantic_js_updates
80
+
81
+ if pydantic_js_extra is not None:
82
+ existing_pydantic_js_extra = core_metadata.get('pydantic_js_extra')
83
+ if existing_pydantic_js_extra is None:
84
+ core_metadata['pydantic_js_extra'] = pydantic_js_extra
85
+ if isinstance(existing_pydantic_js_extra, dict):
86
+ if isinstance(pydantic_js_extra, dict):
87
+ core_metadata['pydantic_js_extra'] = {**existing_pydantic_js_extra, **pydantic_js_extra}
88
+ if callable(pydantic_js_extra):
89
+ warn(
90
+ 'Composing `dict` and `callable` type `json_schema_extra` is not supported.'
91
+ 'The `callable` type is being ignored.'
92
+ "If you'd like support for this behavior, please open an issue on pydantic.",
93
+ PydanticJsonSchemaWarning,
94
+ )
95
+ if callable(existing_pydantic_js_extra):
96
+ # if ever there's a case of a callable, we'll just keep the last json schema extra spec
97
+ core_metadata['pydantic_js_extra'] = pydantic_js_extra
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections.abc import Mapping, Sequence
5
+ from typing import TYPE_CHECKING, Any, TypeAlias, TypeGuard
6
+
7
+ from pydantic_core import CoreSchema, core_schema
8
+
9
+ if TYPE_CHECKING:
10
+ from rich.console import Console
11
+
12
+ AnyFunctionSchema: TypeAlias = (
13
+ core_schema.AfterValidatorFunctionSchema
14
+ | core_schema.BeforeValidatorFunctionSchema
15
+ | core_schema.WrapValidatorFunctionSchema
16
+ | core_schema.PlainValidatorFunctionSchema
17
+ )
18
+
19
+
20
+ FunctionSchemaWithInnerSchema: TypeAlias = (
21
+ core_schema.AfterValidatorFunctionSchema
22
+ | core_schema.BeforeValidatorFunctionSchema
23
+ | core_schema.WrapValidatorFunctionSchema
24
+ )
25
+
26
+
27
+ CoreSchemaField: TypeAlias = (
28
+ core_schema.ModelField | core_schema.DataclassField | core_schema.TypedDictField | core_schema.ComputedField
29
+ )
30
+
31
+ CoreSchemaOrField: TypeAlias = core_schema.CoreSchema | CoreSchemaField
32
+
33
+ _CORE_SCHEMA_FIELD_TYPES = {'typed-dict-field', 'dataclass-field', 'model-field', 'computed-field'}
34
+ _FUNCTION_WITH_INNER_SCHEMA_TYPES = {'function-before', 'function-after', 'function-wrap'}
35
+ _LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES = {'list', 'deque', 'set', 'frozenset'}
36
+
37
+
38
+ def is_core_schema(
39
+ schema: CoreSchemaOrField,
40
+ ) -> TypeGuard[CoreSchema]:
41
+ return schema['type'] not in _CORE_SCHEMA_FIELD_TYPES
42
+
43
+
44
+ def is_core_schema_field(
45
+ schema: CoreSchemaOrField,
46
+ ) -> TypeGuard[CoreSchemaField]:
47
+ return schema['type'] in _CORE_SCHEMA_FIELD_TYPES
48
+
49
+
50
+ def is_function_with_inner_schema(
51
+ schema: CoreSchemaOrField,
52
+ ) -> TypeGuard[FunctionSchemaWithInnerSchema]:
53
+ return schema['type'] in _FUNCTION_WITH_INNER_SCHEMA_TYPES
54
+
55
+
56
+ def as_ser_schema(schema: CoreSchema) -> core_schema.SerSchema:
57
+ """Return a schema suitable for the `'serialization'` key of a core schema, from an arbitrary core schema.
58
+
59
+ Any core schema can be used as a serialization schema, except `'function-plain'` and `'function-wrap'`
60
+ schemas, as these types are already used by the plain and wrap serializer *function* schemas.
61
+ For these, mimic what pydantic-core would do if they were used as the *main* schema:
62
+
63
+ - if the function schema has a `'serialization'` schema, use it.
64
+ - otherwise, a `'function-plain'` schema is serialized as `'any'`, and a `'function-wrap'` schema
65
+ is serialized using its inner schema.
66
+
67
+ Note that `schema` is expected to be a *core* schema (e.g. as returned by the schema generator),
68
+ not an existing `'serialization'` schema.
69
+ """
70
+ if schema['type'] == 'function-plain' or schema['type'] == 'function-wrap':
71
+ if (ser_schema := schema.get('serialization')) is not None:
72
+ return ser_schema
73
+ if schema['type'] == 'function-plain':
74
+ return core_schema.any_schema()
75
+ return as_ser_schema(schema['schema'])
76
+ return schema
77
+
78
+
79
+ def is_list_like_schema_with_items_schema(
80
+ schema: CoreSchema,
81
+ ) -> TypeGuard[core_schema.ListSchema | core_schema.SetSchema | core_schema.FrozenSetSchema]:
82
+ return schema['type'] in _LIST_LIKE_SCHEMA_WITH_ITEMS_TYPES
83
+
84
+
85
+ def get_ref(s: core_schema.CoreSchema) -> None | str:
86
+ """Get the ref from the schema if it has one.
87
+ This exists just for type checking to work correctly.
88
+ """
89
+ return s.get('ref', None)
90
+
91
+
92
+ def _clean_schema_for_pretty_print(obj: Any, strip_metadata: bool = True) -> Any: # pragma: no cover
93
+ """A utility function to remove irrelevant information from a core schema."""
94
+ if isinstance(obj, Mapping):
95
+ new_dct = {}
96
+ for k, v in obj.items():
97
+ if k == 'metadata' and strip_metadata:
98
+ new_metadata = {}
99
+
100
+ for meta_k, meta_v in v.items():
101
+ if meta_k in ('pydantic_js_functions', 'pydantic_js_annotation_functions'):
102
+ new_metadata['js_metadata'] = '<stripped>'
103
+ else:
104
+ new_metadata[meta_k] = _clean_schema_for_pretty_print(meta_v, strip_metadata=strip_metadata)
105
+
106
+ if list(new_metadata.keys()) == ['js_metadata']:
107
+ new_metadata = {'<stripped>'}
108
+
109
+ new_dct[k] = new_metadata
110
+ # Remove some defaults:
111
+ elif k in ('custom_init', 'root_model') and not v:
112
+ continue
113
+ else:
114
+ new_dct[k] = _clean_schema_for_pretty_print(v, strip_metadata=strip_metadata)
115
+
116
+ return new_dct
117
+ elif isinstance(obj, Sequence) and not isinstance(obj, str):
118
+ return [_clean_schema_for_pretty_print(v, strip_metadata=strip_metadata) for v in obj]
119
+ else:
120
+ return obj
121
+
122
+
123
+ def pretty_print_core_schema(
124
+ val: Any,
125
+ *,
126
+ console: Console | None = None,
127
+ max_depth: int | None = None,
128
+ strip_metadata: bool = True,
129
+ ) -> None: # pragma: no cover
130
+ """Pretty-print a core schema using the `rich` library.
131
+
132
+ Args:
133
+ val: The core schema to print, or a Pydantic model/dataclass/type adapter
134
+ (in which case the cached core schema is fetched and printed).
135
+ console: A rich console to use when printing. Defaults to the global rich console instance.
136
+ max_depth: The number of nesting levels which may be printed.
137
+ strip_metadata: Whether to strip metadata in the output. If `True` any known core metadata
138
+ attributes will be stripped (but custom attributes are kept). Defaults to `True`.
139
+ """
140
+ # lazy import:
141
+ from rich.pretty import pprint
142
+
143
+ # circ. imports:
144
+ from pyprig import BaseModel, TypeAdapter
145
+ from pyprig.dataclasses import is_pydantic_dataclass
146
+
147
+ if (inspect.isclass(val) and issubclass(val, BaseModel)) or is_pydantic_dataclass(val):
148
+ val = val.__pydantic_core_schema__
149
+ if isinstance(val, TypeAdapter):
150
+ val = val.core_schema
151
+ cleaned_schema = _clean_schema_for_pretty_print(val, strip_metadata=strip_metadata)
152
+
153
+ pprint(cleaned_schema, console=console, max_depth=max_depth)
154
+
155
+
156
+ pps = pretty_print_core_schema