fh-pydantic-form 0.2.3__tar.gz → 0.2.5__tar.gz

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 fh-pydantic-form might be problematic. Click here for more details.

Files changed (20) hide show
  1. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/PKG-INFO +1 -1
  2. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/RELEASE_NOTES.md +6 -0
  3. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/pyproject.toml +2 -1
  4. fh_pydantic_form-0.2.5/src/fh_pydantic_form/constants.py +12 -0
  5. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/defaults.py +24 -2
  6. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/field_renderers.py +1 -1
  7. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/form_parser.py +71 -31
  8. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/form_renderer.py +25 -41
  9. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/type_helpers.py +79 -13
  10. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/.github/workflows/build.yaml +0 -0
  11. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/.github/workflows/publish.yaml +0 -0
  12. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/.gitignore +0 -0
  13. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/.pre-commit-config.yaml +0 -0
  14. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/LICENSE +0 -0
  15. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/README.md +0 -0
  16. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/__init__.py +0 -0
  17. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/list_path.py +0 -0
  18. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/py.typed +0 -0
  19. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/registry.py +0 -0
  20. {fh_pydantic_form-0.2.3 → fh_pydantic_form-0.2.5}/src/fh_pydantic_form/ui_style.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-pydantic-form
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form
5
5
  Project-URL: Homepage, https://github.com/Marcura/fh-pydantic-form
6
6
  Project-URL: Repository, https://github.com/Marcura/fh-pydantic-form
@@ -1,5 +1,11 @@
1
1
  # Release Notes
2
2
 
3
+ ## Version 0.2.5 (2025-06-19)
4
+
5
+ - Fix bug with empty lists. Now should parse correctly to empty lists instead of returning defaults.
6
+ ## Version 0.2.4 (2025-06-18)
7
+
8
+ - Added support for SkipJsonSchema fields. They will automatically be excluded from the form and defaults used for validation.
3
9
  ## Version 0.2.3 (2025-06-16 )
4
10
 
5
11
  - Removed the custom css injection for compact spacing. Instead applying to components directly.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fh-pydantic-form"
3
- version = "0.2.3"
3
+ version = "0.2.5"
4
4
  description = "a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -49,6 +49,7 @@ markers = [
49
49
  "slow: tests that take longer to run",
50
50
  "enum: tests specifically for enum field functionality",
51
51
  "unit: fast unit tests with minimal dependencies",
52
+ "e2e: end to end tests, somewhat slow"
52
53
  ]
53
54
 
54
55
  [build-system]
@@ -0,0 +1,12 @@
1
+ """
2
+ Shared constants and sentinel values used across the library.
3
+ """
4
+
5
+
6
+ class _Unset:
7
+ """Sentinel class to indicate an unset value."""
8
+
9
+ pass
10
+
11
+
12
+ _UNSET = _Unset()
@@ -2,11 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import datetime as _dt
4
4
  from enum import Enum
5
- from typing import Any, get_args, get_origin, Literal
5
+ from typing import Any, Literal, get_args, get_origin
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
- from .type_helpers import _UNSET, get_default, _is_optional_type
9
+ from fh_pydantic_form.constants import _UNSET
10
+ from fh_pydantic_form.type_helpers import (
11
+ _is_optional_type,
12
+ _is_skip_json_schema_field,
13
+ get_default,
14
+ )
10
15
 
11
16
 
12
17
  def _today():
@@ -122,6 +127,23 @@ def default_dict_for_model(model_cls: type[BaseModel]) -> dict[str, Any]:
122
127
  continue
123
128
  # --------------------------------------------------------------------
124
129
 
130
+ # Check if this is a SkipJsonSchema field - if so, always get its default
131
+ if _is_skip_json_schema_field(field):
132
+ default_val = get_default(field)
133
+ if default_val is not _UNSET:
134
+ # Handle BaseModel defaults by converting to dict
135
+ if hasattr(default_val, "model_dump"):
136
+ out[name] = default_val.model_dump()
137
+ # Convert enum instances to their values
138
+ elif isinstance(default_val, Enum):
139
+ out[name] = default_val.value
140
+ else:
141
+ out[name] = default_val
142
+ else:
143
+ # No default for SkipJsonSchema field - use smart default
144
+ out[name] = default_for_annotation(field.annotation)
145
+ continue
146
+
125
147
  # 1. Check for model-supplied default or factory
126
148
  default_val = get_default(field) # returns _UNSET if no default
127
149
  if default_val is not _UNSET:
@@ -15,9 +15,9 @@ from fastcore.xml import FT
15
15
  from pydantic import ValidationError
16
16
  from pydantic.fields import FieldInfo
17
17
 
18
+ from fh_pydantic_form.constants import _UNSET
18
19
  from fh_pydantic_form.registry import FieldRendererRegistry
19
20
  from fh_pydantic_form.type_helpers import (
20
- _UNSET,
21
21
  _get_underlying_type_if_optional,
22
22
  _is_optional_type,
23
23
  get_default,
@@ -7,6 +7,8 @@ from typing import (
7
7
  Optional,
8
8
  Tuple,
9
9
  Union,
10
+ get_origin,
11
+ get_args,
10
12
  )
11
13
 
12
14
  from fh_pydantic_form.type_helpers import (
@@ -14,6 +16,7 @@ from fh_pydantic_form.type_helpers import (
14
16
  _is_enum_type,
15
17
  _is_literal_type,
16
18
  _is_optional_type,
19
+ _is_skip_json_schema_field,
17
20
  )
18
21
 
19
22
  logger = logging.getLogger(__name__)
@@ -32,17 +35,16 @@ def _identify_list_fields(model_class) -> Dict[str, Dict[str, Any]]:
32
35
  list_fields = {}
33
36
  for field_name, field_info in model_class.model_fields.items():
34
37
  annotation = getattr(field_info, "annotation", None)
35
- if (
36
- annotation is not None
37
- and hasattr(annotation, "__origin__")
38
- and annotation.__origin__ is list
39
- ):
40
- item_type = annotation.__args__[0]
41
- list_fields[field_name] = {
42
- "item_type": item_type,
43
- "is_model_type": hasattr(item_type, "model_fields"),
44
- "field_info": field_info, # Store for later use if needed
45
- }
38
+ if annotation is not None:
39
+ # Handle Optional[List[...]] by unwrapping the Optional
40
+ base_ann = _get_underlying_type_if_optional(annotation)
41
+ if get_origin(base_ann) is list:
42
+ item_type = get_args(base_ann)[0]
43
+ list_fields[field_name] = {
44
+ "item_type": item_type,
45
+ "is_model_type": hasattr(item_type, "model_fields"),
46
+ "field_info": field_info, # Store for later use if needed
47
+ }
46
48
  return list_fields
47
49
 
48
50
 
@@ -77,6 +79,11 @@ def _parse_non_list_fields(
77
79
  if field_name in exclude_fields:
78
80
  continue
79
81
 
82
+ # Skip SkipJsonSchema fields - they should not be parsed from form data
83
+ if _is_skip_json_schema_field(field_info):
84
+ logger.debug(f"Skipping SkipJsonSchema field during parsing: {field_name}")
85
+ continue
86
+
80
87
  # Create full key with prefix
81
88
  full_key = f"{base_prefix}{field_name}"
82
89
 
@@ -91,11 +98,21 @@ def _parse_non_list_fields(
91
98
 
92
99
  # Handle Literal fields (including Optional[Literal[...]])
93
100
  elif _is_literal_type(annotation):
94
- result[field_name] = _parse_literal_field(full_key, form_data, field_info)
101
+ if full_key in form_data: # User sent it
102
+ result[field_name] = _parse_literal_field(
103
+ full_key, form_data, field_info
104
+ )
105
+ elif _is_optional_type(annotation): # Optional but omitted
106
+ result[field_name] = None
107
+ # otherwise leave the key out – defaults will be injected later
95
108
 
96
109
  # Handle Enum fields (including Optional[Enum])
97
110
  elif _is_enum_type(annotation):
98
- result[field_name] = _parse_enum_field(full_key, form_data, field_info)
111
+ if full_key in form_data: # User sent it
112
+ result[field_name] = _parse_enum_field(full_key, form_data, field_info)
113
+ elif _is_optional_type(annotation): # Optional but omitted
114
+ result[field_name] = None
115
+ # otherwise leave the key out – defaults will be injected later
99
116
 
100
117
  # Handle nested model fields (including Optional[NestedModel])
101
118
  elif (
@@ -112,9 +129,14 @@ def _parse_non_list_fields(
112
129
  # Get the nested model class (unwrap Optional if needed)
113
130
  nested_model_class = _get_underlying_type_if_optional(annotation)
114
131
 
115
- # Parse the nested model - pass the base_prefix
132
+ # Parse the nested model - pass the base_prefix and exclude_fields
116
133
  nested_value = _parse_nested_model_field(
117
- field_name, form_data, nested_model_class, field_info, base_prefix
134
+ field_name,
135
+ form_data,
136
+ nested_model_class,
137
+ field_info,
138
+ base_prefix,
139
+ exclude_fields,
118
140
  )
119
141
 
120
142
  # Only assign if we got a non-None value or the field is not optional
@@ -126,8 +148,13 @@ def _parse_non_list_fields(
126
148
 
127
149
  # Handle simple fields
128
150
  else:
129
- # Use updated _parse_simple_field that handles optionality
130
- result[field_name] = _parse_simple_field(full_key, form_data, field_info)
151
+ if full_key in form_data: # User sent it
152
+ result[field_name] = _parse_simple_field(
153
+ full_key, form_data, field_info
154
+ )
155
+ elif _is_optional_type(annotation): # Optional but omitted
156
+ result[field_name] = None
157
+ # otherwise leave the key out – defaults will be injected later
131
158
 
132
159
  return result
133
160
 
@@ -249,6 +276,7 @@ def _parse_nested_model_field(
249
276
  nested_model_class,
250
277
  field_info,
251
278
  parent_prefix: str = "",
279
+ exclude_fields: Optional[List[str]] = None,
252
280
  ) -> Optional[Dict[str, Any]]:
253
281
  """
254
282
  Parse a nested Pydantic model field from form data.
@@ -282,6 +310,13 @@ def _parse_nested_model_field(
282
310
  sub_key = f"{current_prefix}{sub_field_name}"
283
311
  annotation = getattr(sub_field_info, "annotation", None)
284
312
 
313
+ # Skip SkipJsonSchema fields - they should not be parsed from form data
314
+ if _is_skip_json_schema_field(sub_field_info):
315
+ logger.debug(
316
+ f"Skipping SkipJsonSchema field in nested model during parsing: {sub_field_name}"
317
+ )
318
+ continue
319
+
285
320
  # Handle based on field type, with Optional unwrapping
286
321
  is_optional = _is_optional_type(annotation)
287
322
  base_type = _get_underlying_type_if_optional(annotation)
@@ -323,6 +358,7 @@ def _parse_nested_model_field(
323
358
  form_data,
324
359
  nested_list_defs,
325
360
  current_prefix, # ← prefix for this nested model
361
+ exclude_fields, # Pass through exclude_fields
326
362
  )
327
363
  # Merge without clobbering keys already set in step 1
328
364
  for lf_name, lf_val in list_results.items():
@@ -404,6 +440,7 @@ def _parse_list_fields(
404
440
  form_data: Dict[str, Any],
405
441
  list_field_defs: Dict[str, Dict[str, Any]],
406
442
  base_prefix: str = "",
443
+ exclude_fields: Optional[List[str]] = None,
407
444
  ) -> Dict[str, List[Any]]:
408
445
  """
409
446
  Parse list fields from form data by analyzing keys and reconstructing ordered lists.
@@ -412,10 +449,13 @@ def _parse_list_fields(
412
449
  form_data: Dictionary containing form field data
413
450
  list_field_defs: Dictionary of list field definitions
414
451
  base_prefix: Prefix to use when looking up field names in form_data
452
+ exclude_fields: Optional list of field names to exclude from parsing
415
453
 
416
454
  Returns:
417
455
  Dictionary with parsed list fields
418
456
  """
457
+ exclude_fields = exclude_fields or []
458
+
419
459
  # Skip if no list fields defined
420
460
  if not list_field_defs:
421
461
  return {}
@@ -497,20 +537,18 @@ def _parse_list_fields(
497
537
  if items: # Only add if items were found
498
538
  final_lists[field_name] = items
499
539
 
500
- # For any list field that didn't have form data, use its default
540
+ # Ensure every rendered list field appears in final_lists
501
541
  for field_name, field_def in list_field_defs.items():
502
- if field_name not in final_lists:
503
- field_info = field_def["field_info"]
504
- if hasattr(field_info, "default") and field_info.default is not None:
505
- final_lists[field_name] = field_info.default
506
- elif (
507
- hasattr(field_info, "default_factory")
508
- and field_info.default_factory is not None
509
- ):
510
- try:
511
- final_lists[field_name] = field_info.default_factory()
512
- except Exception:
513
- pass
542
+ # Skip list fields the UI never showed (those in exclude_fields)
543
+ if field_name in exclude_fields:
544
+ continue
545
+
546
+ # When user supplied ≥1 item we already captured it
547
+ if field_name in final_lists:
548
+ continue
549
+
550
+ # User submitted form with zero items → honour intent with empty list
551
+ final_lists[field_name] = []
514
552
 
515
553
  return final_lists
516
554
 
@@ -543,7 +581,9 @@ def _parse_model_list_item(
543
581
  )
544
582
  # 2. Parse inner lists
545
583
  result.update(
546
- _parse_list_fields(form_data, nested_list_defs, base_prefix=item_prefix)
584
+ _parse_list_fields(
585
+ form_data, nested_list_defs, base_prefix=item_prefix, exclude_fields=[]
586
+ )
547
587
  )
548
588
  return result
549
589
 
@@ -17,6 +17,7 @@ import monsterui.all as mui
17
17
  from fastcore.xml import FT
18
18
  from pydantic import BaseModel
19
19
 
20
+ from fh_pydantic_form.constants import _UNSET
20
21
  from fh_pydantic_form.defaults import default_dict_for_model, default_for_annotation
21
22
  from fh_pydantic_form.field_renderers import (
22
23
  BaseFieldRenderer,
@@ -30,7 +31,7 @@ from fh_pydantic_form.form_parser import (
30
31
  )
31
32
  from fh_pydantic_form.list_path import walk_path
32
33
  from fh_pydantic_form.registry import FieldRendererRegistry
33
- from fh_pydantic_form.type_helpers import _UNSET, get_default
34
+ from fh_pydantic_form.type_helpers import _is_skip_json_schema_field, get_default
34
35
  from fh_pydantic_form.ui_style import (
35
36
  SpacingTheme,
36
37
  SpacingValue,
@@ -344,6 +345,11 @@ class PydanticForm(Generic[ModelType]):
344
345
  logger.debug(f"Skipping excluded field: {field_name}")
345
346
  continue
346
347
 
348
+ # Skip SkipJsonSchema fields (they should not be rendered in the form)
349
+ if _is_skip_json_schema_field(field_info):
350
+ logger.debug(f"Skipping SkipJsonSchema field: {field_name}")
351
+ continue
352
+
347
353
  # Only use what was explicitly provided in initial values
348
354
  initial_value = (
349
355
  self.values_dict.get(field_name) if self.values_dict else None
@@ -578,35 +584,30 @@ class PydanticForm(Generic[ModelType]):
578
584
  # Merge list results into the main result
579
585
  result.update(list_results)
580
586
 
581
- # Inject defaults for excluded fields before returning
582
- self._inject_default_values_for_excluded(result)
587
+ # Inject defaults for missing fields before returning
588
+ self._inject_missing_defaults(result)
583
589
 
584
590
  return result
585
591
 
586
- def _inject_default_values_for_excluded(
587
- self, data: Dict[str, Any]
588
- ) -> Dict[str, Any]:
592
+ def _inject_missing_defaults(self, data: Dict[str, Any]) -> Dict[str, Any]:
589
593
  """
590
- Ensures that every field listed in self.exclude_fields is present in data
591
- if the model defines a default or default_factory, or if initial_values were provided.
592
-
593
- Also ensures all model fields have appropriate defaults if missing.
594
+ Ensures all model fields with defaults are present in data if missing.
595
+ Handles excluded fields, SkipJsonSchema fields, and any other fields
596
+ not rendered in the form.
594
597
 
595
598
  Priority order:
596
599
  1. initial_values (if provided during form creation)
597
600
  2. model defaults/default_factory
598
601
 
599
- Operates top-level only (exclude_fields spec is top-level names).
600
-
601
602
  Args:
602
603
  data: Dictionary to modify in-place
603
604
 
604
605
  Returns:
605
606
  The same dictionary instance for method chaining
606
607
  """
607
- # Handle excluded fields first
608
- for field_name in self.exclude_fields:
609
- # Skip if already present (e.g., user provided initial_values)
608
+ # Process ALL model fields, not just excluded ones
609
+ for field_name, field_info in self.model_class.model_fields.items():
610
+ # Skip if already present in parsed data
610
611
  if field_name in data:
611
612
  continue
612
613
 
@@ -617,17 +618,10 @@ class PydanticForm(Generic[ModelType]):
617
618
  if hasattr(initial_val, "model_dump"):
618
619
  initial_val = initial_val.model_dump()
619
620
  data[field_name] = initial_val
620
- logger.debug(
621
- f"Injected initial value for excluded field '{field_name}'"
622
- )
621
+ logger.debug(f"Injected initial value for missing field '{field_name}'")
623
622
  continue
624
623
 
625
624
  # Second priority: use model defaults
626
- field_info = self.model_class.model_fields.get(field_name)
627
- if field_info is None:
628
- logger.warning(f"exclude_fields contains unknown field '{field_name}'")
629
- continue
630
-
631
625
  default_val = get_default(field_info)
632
626
  if default_val is not _UNSET:
633
627
  # If the default is a BaseModel, convert to dict for consistency
@@ -635,27 +629,17 @@ class PydanticForm(Generic[ModelType]):
635
629
  default_val = default_val.model_dump()
636
630
  data[field_name] = default_val
637
631
  logger.debug(
638
- f"Injected model default value for excluded field '{field_name}'"
632
+ f"Injected model default value for missing field '{field_name}'"
639
633
  )
640
634
  else:
641
- # No default leave missing; validation will surface error
642
- logger.debug(f"No default found for excluded field '{field_name}'")
643
-
644
- # Also handle any other missing fields that should have defaults
645
- for field_name, field_info in self.model_class.model_fields.items():
646
- if field_name not in data:
647
- # Try to inject defaults for missing fields
648
- if field_name in self.initial_values_dict:
649
- initial_val = self.initial_values_dict[field_name]
650
- if hasattr(initial_val, "model_dump"):
651
- initial_val = initial_val.model_dump()
652
- data[field_name] = initial_val
635
+ # Check if this is a SkipJsonSchema field
636
+ if _is_skip_json_schema_field(field_info):
637
+ logger.debug(
638
+ f"No default found for SkipJsonSchema field '{field_name}'"
639
+ )
653
640
  else:
654
- default_val = get_default(field_info)
655
- if default_val is not _UNSET:
656
- if hasattr(default_val, "model_dump"):
657
- default_val = default_val.model_dump()
658
- data[field_name] = default_val
641
+ # No default → leave missing; validation will surface error
642
+ logger.debug(f"No default found for field '{field_name}'")
659
643
 
660
644
  return data
661
645
 
@@ -1,12 +1,88 @@
1
+ # Explicit exports for public API
2
+ __all__ = [
3
+ "_is_optional_type",
4
+ "_get_underlying_type_if_optional",
5
+ "_is_literal_type",
6
+ "_is_enum_type",
7
+ "_is_skip_json_schema_field",
8
+ "default_for_annotation",
9
+ ]
10
+
1
11
  import logging
2
12
  from enum import Enum
3
13
  from types import UnionType
4
- from typing import Any, Literal, Union, get_args, get_origin
14
+ from typing import Annotated, Any, Literal, Union, get_args, get_origin
15
+
16
+ from fh_pydantic_form.constants import _UNSET
5
17
 
6
18
  logger = logging.getLogger(__name__)
7
19
 
8
- # Sentinel value to indicate no default is available
9
- _UNSET = object()
20
+
21
+ def _is_skip_json_schema_field(annotation_or_field_info: Any) -> bool:
22
+ """
23
+ Check if a field annotation or field_info indicates it should be skipped in JSON schema.
24
+
25
+ This handles the pattern where SkipJsonSchema is used with typing.Annotated:
26
+ - Annotated[str, SkipJsonSchema()]
27
+ - SkipJsonSchema[str] (which internally uses Annotated)
28
+ - Field metadata containing SkipJsonSchema (Pydantic 2 behavior)
29
+
30
+ Args:
31
+ annotation_or_field_info: The field annotation or field_info to check
32
+
33
+ Returns:
34
+ True if the field should be skipped in JSON schema
35
+ """
36
+ try:
37
+ from pydantic.json_schema import SkipJsonSchema
38
+
39
+ skip_json_schema_cls = SkipJsonSchema
40
+ except ImportError: # very old Pydantic
41
+ skip_json_schema_cls = None
42
+
43
+ if skip_json_schema_cls is None:
44
+ return False
45
+
46
+ # Check if it's a field_info object with metadata
47
+ if hasattr(annotation_or_field_info, "metadata"):
48
+ metadata = getattr(annotation_or_field_info, "metadata", [])
49
+ if metadata:
50
+ for item in metadata:
51
+ if (
52
+ item is skip_json_schema_cls
53
+ or isinstance(item, skip_json_schema_cls)
54
+ or (
55
+ hasattr(item, "__class__")
56
+ and item.__class__.__name__ == "SkipJsonSchema"
57
+ )
58
+ ):
59
+ return True
60
+
61
+ # Fall back to checking annotation (for backward compatibility)
62
+ annotation = annotation_or_field_info
63
+ if hasattr(annotation_or_field_info, "annotation"):
64
+ annotation = getattr(annotation_or_field_info, "annotation")
65
+
66
+ # 1. Direct or generic alias
67
+ if (
68
+ annotation is skip_json_schema_cls
69
+ or getattr(annotation, "__origin__", None) is skip_json_schema_cls
70
+ ):
71
+ return True
72
+
73
+ # 2. Something like Annotated[T, SkipJsonSchema()]
74
+ if get_origin(annotation) is Annotated:
75
+ for meta in get_args(annotation)[1:]:
76
+ meta_class = getattr(meta, "__class__", None)
77
+ if (
78
+ meta is skip_json_schema_cls # plain class
79
+ or isinstance(meta, skip_json_schema_cls) # instance
80
+ or (meta_class is not None and meta_class.__name__ == "SkipJsonSchema")
81
+ ):
82
+ return True
83
+
84
+ # 3. Fallback – cheap but effective
85
+ return "SkipJsonSchema" in repr(annotation)
10
86
 
11
87
 
12
88
  def _is_optional_type(annotation: Any) -> bool:
@@ -27,16 +103,6 @@ def _is_optional_type(annotation: Any) -> bool:
27
103
  return False
28
104
 
29
105
 
30
- # Explicit exports for public API
31
- __all__ = [
32
- "_is_optional_type",
33
- "_get_underlying_type_if_optional",
34
- "_is_literal_type",
35
- "_is_enum_type",
36
- "default_for_annotation",
37
- ]
38
-
39
-
40
106
  def _get_underlying_type_if_optional(annotation: Any) -> Any:
41
107
  """
42
108
  Extract the type T from Optional[T], otherwise return the original annotation.