Skip to content

docs: Remove inheritable docstrings#2427

Merged
MarcoGorelli merged 23 commits intomainfrom
remove-inherited-docs
Apr 27, 2025
Merged

docs: Remove inheritable docstrings#2427
MarcoGorelli merged 23 commits intomainfrom
remove-inherited-docs

Conversation

@dangotbanned
Copy link
Copy Markdown
Member

@dangotbanned dangotbanned commented Apr 23, 2025

What type of PR is this? (check all applicable)

  • 💾 Refactor
  • ✨ Feature
  • 🐛 Bug Fix
  • 🔧 Optimization
  • 📝 Documentation
  • ✅ Test
  • 🐳 Other

Related issues

Checklist

  • Code follows style guide (ruff)
  • Tests added
  • Documented the changes

If you have comments or can explain your changes, please do so below

https://docs.python.org/3/library/inspect.html#inspect.getdoc

AFAIK, adding docstrings in the v1 classes/methods is probably doing more harm than good:

  • Best-case, we're duplicating information that we need to keep in sync
  • Worst-case, we override a docstring that had examples with a version without examples

I think we should only be adding docstrings if they are different to what is on main.
Things like deprecations, breaking changes, etc

Examples

Before

def to_dict(
self, *, as_series: bool = True
) -> dict[str, Series[Any]] | dict[str, list[Any]]:
"""Convert DataFrame to a dictionary mapping column name to values.
Arguments:
as_series: If set to true ``True``, then the values are Narwhals Series,
otherwise the values are Any.
Returns:
A mapping from column name to values / Series.
"""

image

After

def to_dict(
self, *, as_series: bool = True
) -> dict[str, Series[Any]] | dict[str, list[Any]]:

image

Tasks

Worse than main, since there are no examples
Using [`inspect.getdoc`](https://docs.python.org/3/library/inspect.html#inspect.getdoc) gives you the inherited doc - which is what users will see
The other way was failing because v1 reuses its parent's docstring now
@dangotbanned dangotbanned added the documentation Improvements or additions to documentation label Apr 23, 2025
@dangotbanned
Copy link
Copy Markdown
Member Author

dangotbanned commented Apr 23, 2025

Ahhh we had a nice streak of no unrelated downstream CI issues going there for a bit 😢

https://github.com/narwhals-dev/narwhals/actions/runs/14629063988/job/41047719471?pr=2427

Update

Resolved in (#2428)

@dangotbanned dangotbanned changed the title docs(DRAFT): Remove inheritable docstrings docs: Remove inheritable docstrings Apr 24, 2025
@FBruzzesi FBruzzesi mentioned this pull request Apr 24, 2025
10 tasks
@dangotbanned
Copy link
Copy Markdown
Member Author

dangotbanned commented Apr 24, 2025

Feedback please!

We have a few options for dealing with the missing class-level docstrings.

Important

Method-level docstrings are fine

1. Revert that part

Simplest option, lowers my code-golf score but no issues beyond that 🙂

2. Getting funky

I can get them to appear statically, but only when the constructor is called:

image

Locally, I've adapted the typing I used in altair.
The usage is simply decorating <subclass>.__init__ with the parent's context

image

Bad news

But that doesn't extend to hovering over the class itself 😞

image

3. Getting funkier

If we did something more dynamic we could get the class-level doc.
However, that would only be displayed in a notebook/kernel/live environment.

It might be possible to combine 2 & 3, but I don't know if they'd interact well across all environments.

Personal note

99% of the time I have kernel completions disabled - since I find them too noisy with pylance at the same time.

So while I'm presenting this as an option - I'd prefer an alternative for my own sake 😄

"jupyter.enableKernelCompletions": false

@dangotbanned dangotbanned marked this pull request as ready for review April 24, 2025 13:32
@MarcoGorelli
Copy link
Copy Markdown
Member

thanks @dangotbanned ! tbh I don't think we need class-level docstrings too much, no objections to removing them

@dangotbanned
Copy link
Copy Markdown
Member Author

thanks @dangotbanned ! tbh I don't think we need class-level docstrings too much, no objections to removing them

Thanks for taking a look @MarcoGorelli

I can't tell if you were saying either:

For the example I gave in (#2427 (comment)) I can understand why they might not seem like a big loss - since we're just missing an admonition.

However, there's also Datetime and Duration which have a bit more utility

Main

image

V1 (currently in this PR)

image

V1 (using 2. Getting funky)

image

image

Also includes an example we didn't have before

image

Possible with minimal changes to Datetime

image

Copy link
Copy Markdown
Member

@MarcoGorelli MarcoGorelli left a comment

Choose a reason for hiding this comment

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

thanks @dangotbanned , changes to docstrings look good

could you summarise the gist of the change in tests/stable_api_test.py please?

Comment on lines +109 to +116
def _iter_api_method_docs(obj: Any, *exclude: str) -> Iterator[tuple[str, str]]:
for name in dir(obj):
if (
not name.startswith("_")
and name not in exclude
and (doc := getdoc(getattr(obj, name)))
):
yield name, doc
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

could you summarise the gist of the change in tests/stable_api_test.py please?

Getting gist-y with it

@MarcoGorelli the changes are centred on this new helper generator function, which mostly is just replacing the repetition in each of:

  • test_dataframe_docstrings
  • test_lazyframe_docstrings
  • test_series_docstrings
Previous

def test_dataframe_docstrings() -> None:
pytest.importorskip("polars")
import polars as pl
stable_df = nw_v1.from_native(pl.DataFrame())
df = nw.from_native(pl.DataFrame())
api = [i for i in df.__dir__() if not i.startswith("_")]
for item in api:
assert remove_docstring_examples(
getattr(stable_df, item).__doc__.replace(
"import narwhals.stable.v1 as nw", "import narwhals as nw"
)
) == remove_docstring_examples(getattr(df, item).__doc__), item
def test_lazyframe_docstrings() -> None:
pytest.importorskip("polars")
import polars as pl
stable_df = nw_v1.from_native(pl.LazyFrame())
df = nw.from_native(pl.LazyFrame())
api = [i for i in df.__dir__() if not i.startswith("_")]
for item in api:
if item in {"schema", "columns"}:
# to avoid performance warning
continue
if item in {"tail", "gather_every"}:
# deprecated
continue
assert remove_docstring_examples(
getattr(stable_df, item).__doc__.replace(
"import narwhals.stable.v1 as nw", "import narwhals as nw"
)
) == remove_docstring_examples(getattr(df, item).__doc__)
def test_series_docstrings() -> None:
pytest.importorskip("polars")
import polars as pl
stable_df = nw_v1.from_native(pl.Series(), series_only=True)
df = nw.from_native(pl.Series(), series_only=True)
api = [i for i in df.__dir__() if not i.startswith("_")]
for item in api:
if getattr(df, item).__doc__ is None:
continue
assert remove_docstring_examples(
getattr(stable_df, item).__doc__.replace(
"import narwhals.stable.v1 as nw", "import narwhals as nw"
)
) == remove_docstring_examples(getattr(df, item).__doc__), item

Notes

  • Used dir(...) instead of __dir__()
  • Collapsed the various if <predicate>: continue statements into one that can be parametrized
    • The groups in test_lazyframe_docstrings with comments became the name of variables passed to *exclude
      • This is intended to make the code self-documenting, but it seems to have not worked 😢
  • Used inspect.getdoc(...) instead of __doc__
    • The latter broke following (0a5c5e7), whereas the next commit (64f6ef9) worked as documented

@dangotbanned
Copy link
Copy Markdown
Member Author

https://github.com/narwhals-dev/narwhals/actions/runs/14693968316/job/41233109700?pr=2427

@MarcoGorelli this seems to have popped up after merging (#2325) 🤔

[XPASS(strict)] 
 =========================== short test summary info ============================
FAILED tests/group_by_test.py::test_group_by_expr[polars[lazy]-0]
= 1 failed, 9881 passed, 147 skipped, 519 xfailed, 1 xpassed in 222.44s (0:03:42) =
Error: Process completed with exit code 1.

Copy link
Copy Markdown
Member

@MarcoGorelli MarcoGorelli left a comment

Choose a reason for hiding this comment

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

there's just been a new polars release, could well be that

thanks @dangotbanned !

@MarcoGorelli MarcoGorelli merged commit 2bab27b into main Apr 27, 2025
27 of 29 checks passed
@MarcoGorelli MarcoGorelli deleted the remove-inherited-docs branch April 27, 2025 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants