FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

ENH: Preserve signature and docs of functions wrapped by `@pipefunc` by basnijholt · Pull Request #960 · pipefunc/pipefunc · GitHub

ENH: Preserve signature and docs of functions wrapped by @pipefunc - #960

Merged
basnijholt merged 3 commits into
mainfrom
pipefunc-paramspec-typing
Jun 10, 2026
Merged

ENH: Preserve signature and docs of functions wrapped by @pipefunc#960
basnijholt merged 3 commits into
mainfrom
pipefunc-paramspec-typing

Conversation

basnijholt commented Jun 9, 2026
edited
Loading

Copy link
Copy Markdown
Collaborator

Summary

Addresses the IDE/introspection pain point from #902 and picks up the non-breaking part of #910 by @LennartGevers (credited as co-author). Unlike #910, this keeps PipeFunc.__call__'s pipeline semantics (renames, defaults, bound, hooks, profiling) fully intact — no behavior changes.

  • PipeFunc is now Generic[P, R] using ParamSpec, and @pipefunc returns PipeFunc[P, R]. Type checkers and IDEs now see the wrapped function's parameter names, types, and return type when calling it, while PipeFunc methods (update_renames, update_scope, ...) stay available and typed:

    @pipefunc(output_name="c")
    def add(a: int, b: float) -> float:
        """Add two numbers."""
        return a + b
    
    reveal_type(add)         # PipeFunc[[a: int, b: float], float]
    reveal_type(add(1, 2.0)) # float
    add("x", "y")            # error: incompatible argument types
  • __doc__ propagation: the wrapped function's docstring is exposed on the instance, so help(f) and Jupyter's f? show the original docs. Class docstrings of callable instances are deliberately not copied.

  • __wrapped__ property following the functools.wraps convention. inspect.signature is unaffected since the existing __signature__ (which reflects renames/defaults) takes precedence.

  • __test__ = False on PipeFunc: pytest follows __wrapped__ during collection, so without this it would collect PipeFunc objects wrapping functions named test* (this bit our own tests/integration/map/test_error_handling.py).

Trade-off

The static signature is the original one, so calls using renamed/scoped parameters (f(a1=3) after renames={"a": "a1"}) are now flagged by type checkers even though they work at runtime — ParamSpec cannot express runtime signature rewrites (see the discussion in #910). Runtime behavior is unchanged; tests covering that dynamic behavior gained targeted # type: ignore[call-arg] comments. Unannotated functions are unaffected (gradual typing).

The remaining #910 question — whether __call__ should bypass pipeline semantics entirely — is left open; this PR is forward-compatible with either answer.

Verification

  • New tests: docstring propagation, fallback to class docstring, __wrapped__, pickle round-trip, inspect.signature precedence, callable-instance docstring exclusion.
  • Full suite: 1400 passed, 11 skipped, 2 xfailed.
  • pre-commit run --all-files (ruff, mypy, etc.) passes.
  • mypy probe confirms P/R bind correctly (revealed types above).

Part of #902; continues #910.

Breaking change (typing only)

PipeFunc was previously Generic[T] with a single type parameter (T bound to Callable). Code that explicitly subscripted it — e.g. x: PipeFunc[Callable[[int], int]] — must update to the new two-parameter form, e.g. PipeFunc[[int], int] or PipeFunc[..., int]. Unsubscripted PipeFunc annotations (the common case, used everywhere internally) are unaffected, and there is no runtime behavior change.

- `PipeFunc` is now `Generic[P, R]` (ParamSpec), so type checkers and
  IDEs see the wrapped function's parameters and return type on
  `__call__`, while `PipeFunc` methods like `update_renames` remain
  fully typed.
- The wrapped function's `__doc__` is exposed on the instance for
  `help()` and Jupyter `?` introspection (skipping class docstrings of
  callable instances).
- Added a `__wrapped__` property following the `functools.wraps`
  convention; `__signature__` (which reflects renames) still takes
  precedence for `inspect.signature`.
- Set `__test__ = False` so pytest does not collect `PipeFunc` objects
  wrapping functions named `test*` (pytest follows `__wrapped__`).

Note: calls using renamed/scoped parameters are now flagged by type
checkers since the static signature is the original one; tests covering
that dynamic behavior gained targeted `type: ignore` comments.

Continues the work from #910 by @LennartGevers; addresses part of #902.

Co-authored-by: Lennart Gevers <lgevers@uni-muenster.de>

codecov Bot commented Jun 9, 2026
edited
Loading

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
pipefunc/_pipefunc.py 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

codspeed-hq Bot commented Jun 9, 2026
edited
Loading

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing pipefunc-paramspec-typing (0de5adc) with main (ce55ab7)

Copy link
Copy Markdown
Contributor

✅ PR Title Formatted Correctly

The title of this PR has been updated to match the correct format. Thank you!

basnijholt merged commit c37501d into main Jun 10, 2026
21 checks passed
basnijholt deleted the pipefunc-paramspec-typing branch June 10, 2026 02:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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.

1 participant


Back | FazBrowse Home | New Git URL