Skip to content

Conversation

@marcorudolphflex
Copy link
Contributor

@marcorudolphflex marcorudolphflex commented Feb 5, 2026

This PR overhauls docstring generation for Pydantic v2 and cleans up Sphinx docs to reduce noise and ambiguity.


Changes

Docstring generation refactor + cleanup

  • Extracted docstring formatting utilities into a new module (docstrings.py) and wired Tidy3dBaseModel to use them.
  • Docstrings now:
    • Strip Annotated metadata and verbose typing_extensions.* prefixes.
    • Preserve constrained types (PositiveFloat, NonNegativeInt, etc.).
    • Render ArrayLike[...] with dtype / ndim / shape metadata.
    • Remove noisy defaults like attrs={} and type=... from reprs.
    • Collapse default model args unless non-default values are present.
    • Avoid stale/duplicated “Parameters” sections by caching raw docstrings.
  • Added options to hide attrs by default and toggle default-arg verbosity.

Docstring regression tests

  • Added test_docstrings.py to assert:
    • readable types,
    • absence of Annotated / discriminated_union,
    • correct default formatting.

Docs fixes and cleanup

  • Exclude attrs from autodoc and autosummary (no more attrs subpages).
  • Updated stale autosummary references and invalid API targets.
  • Fixed invalid GDS docs (to_gdspy removal; clarified gdstk only).
  • Corrected plugin autosummary paths (KLayout DRC results, S-matrix data).
  • Disambiguated Sphinx cross-references (e.g., Scene, Geometry, ModeSolverData) by using ~tidy3d.* aliases.
  • Added missing abstract/medium class references and removed nonexistent ones.

See as reference example images (before -> after)

Tests

  • pytest -k docstrings (new docstring regression coverage via test_docstrings.py)

Notes

  • Primary goal: make generated API docs cleaner, more stable, and less noisy under Pydantic v2 while preventing docstring regressions.
image image

Note

Medium Risk
Touches Tidy3dBaseModel docstring generation and __repr__ used across most models, so regressions would be broad (though largely documentation/UI-facing). Sphinx build behavior also changes (autosummary overwrite, skip hooks, xref rewriting), which could affect doc build output and link resolution.

Overview
Refactors model docstring rendering by moving formatting logic into new components/docstrings.py and wiring Tidy3dBaseModel to cache raw class docs, regenerate docstrings on subclass init/rebuild, hide attrs by default, collapse default model reprs, and render cleaner type annotations (strip Annotated noise, preserve constrained types, support ArrayLike metadata, handle autograd traced aliases).

Overhauls Sphinx API generation to reduce noise and broken links: autosummary stubs are now overwritten, templates filter out internal members (attrs, Pydantic internals, private/dunder), and docs/conf.py adds hooks to skip private members, strip default Pydantic model_* doc blocks, and rewrite/validate :class: cross-references against known doc targets (with added intersphinx mappings).

Updates many API docs to fix/modernize autosummary targets and references (new abstract/base entries, moved/renamed symbols, tidy3d.rf.* namespace, removed gdspy mention), adds Gaussian overlap monitor docs, exports FreqDataArray/FreqModeDataArray in tidy3d.__init__, adds docstring regression tests, and removes/adjusts some autograd cache-related test scaffolding.

Written by Cursor Bugbot for commit 14efed3. This will update automatically on new commits. Configure here.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@daquinteroflex
Copy link
Collaborator

This is amazing to address this thanks!

@marcorudolphflex marcorudolphflex force-pushed the FXC-5438-trim-generated-docstrings-for-pydantic-v-2-models branch from 5a85da9 to 14efed3 Compare February 10, 2026 15:39
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@marcorudolphflex
Copy link
Contributor Author

This one got bigger than initially planned, but wanted to get rid of missing links/non-interpretable aliases and some overheads.

@marcorudolphflex marcorudolphflex force-pushed the FXC-5438-trim-generated-docstrings-for-pydantic-v-2-models branch from 21b7c7f to 68ad642 Compare February 10, 2026 16:55
@github-actions
Copy link
Contributor

Diff Coverage

Diff: origin/develop...HEAD, staged and unstaged changes

  • tidy3d/components/base.py (88.1%): Missing lines 285,1688,1711-1712,1721
  • tidy3d/components/docstrings.py (91.9%): Missing lines 35,52,54,60-61,65,68,107,131-132,139,173-174,195-196,206,294
  • tidy3d/constants.py (100%)
  • tidy3d/plugins/expressions/init.py (100%)

Summary

  • Total: 255 lines
  • Missing: 22 lines
  • Coverage: 91%

tidy3d/components/base.py

Lines 281-289

  281             _parent_namespace_depth=_parent_namespace_depth,
  282             _types_namespace=_types_namespace,
  283         )
  284         if _DOCSTRING_RAW_ATTR not in cls.__dict__:
! 285             setattr(cls, _DOCSTRING_RAW_ATTR, cls.__doc__ or "")
  286         cls.__doc__ = cls.generate_docstring()
  287         return rebuilt
  288 
  289     @model_validator(mode="wrap")

Lines 1684-1692

  1684         # keep any pre-existing class description
  1685         original_docstrings = []
  1686         raw_doc = cls.__dict__.get(_DOCSTRING_RAW_ATTR)
  1687         if raw_doc is None:
! 1688             raw_doc = cls.__doc__ or ""
  1689         if raw_doc:
  1690             original_docstrings = raw_doc.split("\n\n")
  1691             doc += original_docstrings.pop(0)
  1692         original_docstrings = "\n\n".join(original_docstrings)

Lines 1707-1716

  1707             # default / default_factory
  1708             if field.default_factory is not None:
  1709                 try:
  1710                     default_val = field.default_factory()
! 1711                 except Exception:
! 1712                     default_val = f"{field.default_factory.__name__}()"
  1713             else:
  1714                 default_val = field.get_default(call_default_factory=False)
  1715 
  1716             if isinstance(default_val, BaseModel):

Lines 1717-1725

  1717                 default_val = _format_model_default(
  1718                     default_val, show_default_args=show_default_args
  1719                 )
  1720             elif "=" in str(default_val) if default_val is not None else False:
! 1721                 default_val = _clean_default_repr(
  1722                     str(f"{default_val.__class__.__name__}({default_val})")
  1723                 )
  1724 
  1725             default_str = "" if field.is_required() else f" = {default_val}"

tidy3d/components/docstrings.py

Lines 31-39

  31     origin = get_origin(ann)
  32     if origin is not None and getattr(origin, "__name__", None) == "Annotated":
  33         args = get_args(ann)
  34         if not args:
! 35             return ann, []
  36         return args[0], list(args[1:])
  37     return ann, []
  38 

Lines 48-58

  48 
  49 def _format_type_name(ann: Any) -> str:
  50     """Return a concise, human-readable name for a type-like object."""
  51     if ann is type(None):
! 52         return "None"
  53     if ann is object:
! 54         return "Any"
  55     try:
  56         from tidy3d.components.base import Tidy3dBaseModel
  57 
  58         if isinstance(ann, type) and issubclass(ann, Tidy3dBaseModel):

Lines 56-72

  56         from tidy3d.components.base import Tidy3dBaseModel
  57 
  58         if isinstance(ann, type) and issubclass(ann, Tidy3dBaseModel):
  59             return f":class:`~{ann.__module__}.{ann.__name__}`"
! 60     except Exception:
! 61         pass
  62     forward_arg = getattr(ann, "__forward_arg__", None)
  63     if forward_arg:
  64         if forward_arg.startswith("tidy3d."):
! 65             return f":class:`~{forward_arg}`"
  66         return forward_arg
  67     if isinstance(ann, str) and ann.startswith("tidy3d."):
! 68         return f":class:`~{ann}`"
  69     if hasattr(ann, "__name__"):
  70         return ann.__name__
  71     return _strip_typing_prefixes(str(ann))

Lines 103-111

  103     def base_repr(m: BaseModel) -> str:
  104         return BaseModel.__repr__(m)
  105 
  106     if show_default_args:
! 107         return _clean_default_repr(" ".join(base_repr(model).split()))
  108 
  109     model_cls = model.__class__
  110     try:
  111         default_model = model_cls()

Lines 127-136

  127     for key in ordered_fields:
  128         if key in diff_keys:
  129             try:
  130                 value = getattr(model, key)
! 131             except Exception:
! 132                 value = current_dump.get(key)
  133             parts.append(
  134                 f"{key}={_format_default_value(value, show_default_args=show_default_args)}"
  135             )

Lines 135-143

  135             )
  136 
  137     for key in diff_keys:
  138         if key not in ordered_fields:
! 139             parts.append(
  140                 f"{key}={_format_default_value(current_dump[key], show_default_args=show_default_args)}"
  141             )
  142 
  143     return _clean_default_repr(f"{model_cls.__name__}({', '.join(parts)})")

Lines 169-178

  169                 "i": "int",
  170                 "u": "int",
  171                 "b": "bool",
  172             }.get(kind, np_dtype.name)
! 173         except Exception:
! 174             dtype_str = str(dtype)
  175 
  176     parts: list[str] = []
  177     if dtype_str is not None:
  178         parts.append(f"dtype={dtype_str}")

Lines 191-200

  191     if isinstance(val, type):
  192         return True
  193     try:
  194         return get_origin(val) is not None or bool(get_args(val))
! 195     except Exception:
! 196         return False
  197 
  198 
  199 def _extract_traced_alias_base(metadata: list[Any]) -> Any | None:
  200     """Extract a base annotation from validator closures used by traced aliases."""

Lines 202-210

  202         func = getattr(meta, "func", None)
  203         if func is None or getattr(func, "__name__", "") != "_validate_box_or_container":
  204             continue
  205         if not func.__closure__:
! 206             continue
  207         for cell in func.__closure__:
  208             val = cell.cell_contents
  209             if isinstance(val, TypeAdapter):
  210                 continue

Lines 290-296

  290 
  291 def _fmt_ann_literal(ann: Any, field_metadata: list[Any] | None = None) -> str:
  292     """Render a concise annotation string for docstrings."""
  293     if ann is None:
! 294         return "Any"
  295     return _format_annotation(ann, field_metadata=field_metadata)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants