-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] render docstrings in hover #19882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-08-13 14:57:30.986477337 +0000
+++ new-output.txt 2025-08-13 14:57:31.053477542 +0000
@@ -1,5 +1,5 @@
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/918d35d/src/function/execute.rs:215:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(b5023)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/918d35d/src/function/execute.rs:215:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(244b3)): execute: too many cycle iterations`
_directives_deprecated_library.py:15:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
_directives_deprecated_library.py:30:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
_directives_deprecated_library.py:36:41: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__add__` |
|
| x = MyCla<CURSOR>ss(0) | ||
| "#, | ||
| ); | ||
|
|
||
| assert_snapshot!(test.hover(), @r" | ||
| <class 'MyClass'> | ||
| --------------------------------------------- | ||
| This is such a great class!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This result is a disappointment but a pre-existing issue that we don't really distinguish a class from an invocation of the initializer (future work).
| r#" | ||
| def test(a: int): ... | ||
| def test(ab: int): | ||
| """my cool test | ||
| test(a<CURSOR>= 123) | ||
| Args: | ||
| ab: a nice little integer | ||
| """ | ||
| return 0 | ||
| test(a<CURSOR>b= 123) | ||
| "#, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't change the result at all but I changed this test in case one day our hover gets smart enough to see and render these docs.
| def foo(a, b): ... | ||
| def foo(a, b): | ||
| """The foo function""" | ||
| return 0 | ||
| def bar(a, b): ... | ||
| def bar(a, b): | ||
| """The bar function""" | ||
| return 1 | ||
| if random.choice([True, False]): | ||
| a = foo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly this doesn't change anything, but I added docs just in case something got smarter
|
|
||
| test.write_file("lib.py", "a = 10").unwrap(); | ||
| test.write_file( | ||
| "lib.py", | ||
| r" | ||
| ''' | ||
| The cool lib_py module! | ||
| Wow this module rocks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future work, this should render eventually.
| let mut test = cursor_test( | ||
| r#" | ||
| import li<CURSOR>b | ||
| lib | ||
| "#, | ||
| ); | ||
|
|
||
| test.write_file( | ||
| "lib.py", | ||
| r" | ||
| ''' | ||
| The cool lib_py module! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one's even worse and produces no result because apparently hovering an import doesn't yield the module (even though I fixed goto-def on it in a previous PR). I assume this is because hover currently insists it needs the type of the hovered item, not the def/decl.
| ''' | ||
| My cool func | ||
| Args: | ||
| a: hopefully a string, right?! | ||
| ''' | ||
| if a is not None: | ||
| print(a<CURSOR>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another "maybe one day" change
MichaReiser
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is excellent. Thank you for working on this. I only have a few detail comments that are at your own discretion.
| // NumPy-style docstrings | ||
| param_docs.extend(extract_numpy_style_params(docstring)); | ||
| // Google-style docstrings | ||
| param_docs.extend(extract_google_style_params(&self.0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is pre-existing code, so I'm fine deferring it but I wonder if it makes sense to short-circuit if we parsed parameters by at least one style because most code will only use one convention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually have a test checked in for the mixed behaviour, so i guess @UnboundVariable thought it was something that we'll see in the wild?
| /// Normalizes tabs and trims a docstring as specified in PEP-0257 | ||
| /// | ||
| /// See: <https://peps.python.org/pep-0257/#handling-docstring-indentation> | ||
| fn documentation_trim(docs: &str) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yay, our second implementation of this algorithm (it makes sense for them to be different):
ruff/crates/ruff_python_formatter/src/string/docstring.rs
Lines 27 to 114 in 9ae698f
| /// Format a docstring by trimming whitespace and adjusting the indentation. | |
| /// | |
| /// Summary of changes we make: | |
| /// * Normalize the string like all other strings | |
| /// * Ignore docstring that have an escaped newline | |
| /// * Trim all trailing whitespace, except for a chaperone space that avoids quotes or backslashes | |
| /// in the last line. | |
| /// * Trim leading whitespace on the first line, again except for a chaperone space | |
| /// * If there is only content in the first line and after that only whitespace, collapse the | |
| /// docstring into one line | |
| /// * Adjust the indentation (see below) | |
| /// | |
| /// # Docstring indentation | |
| /// | |
| /// Unlike any other string, like black we change the indentation of docstring lines. | |
| /// | |
| /// We want to preserve the indentation inside the docstring relative to the suite statement/block | |
| /// indent that the docstring statement is in, but also want to apply the change of the outer | |
| /// indentation in the docstring, e.g. | |
| /// ```python | |
| /// def sparkle_sky(): | |
| /// """Make a pretty sparkly sky. | |
| /// * * ✨ *. . | |
| /// * * ✨ . | |
| /// . * . ✨ * . . | |
| /// """ | |
| /// ``` | |
| /// should become | |
| /// ```python | |
| /// def sparkle_sky(): | |
| /// """Make a pretty sparkly sky. | |
| /// * * ✨ *. . | |
| /// * * ✨ . | |
| /// . * . ✨ * . . | |
| /// """ | |
| /// ``` | |
| /// We can't compute the full indentation here since we don't know what the block indent of | |
| /// the doc comment will be yet and which we can only have added by formatting each line | |
| /// separately with a hard line break. This means we need to strip shared indentation from | |
| /// docstring while preserving the in-docstring bigger-than-suite-statement indentation. Example: | |
| /// ```python | |
| /// def f(): | |
| /// """first line | |
| /// line a | |
| /// line b | |
| /// """ | |
| /// ``` | |
| /// The docstring indentation is 2, the block indents will change this to 4 (but we can't | |
| /// determine this at this point). The indentation of line a is 2, so we trim ` line a` | |
| /// to `line a`. For line b it's 5, so we trim it to `line b` and pad with 5-2=3 spaces to | |
| /// ` line b`. The closing quotes, being on their own line, are stripped get only the | |
| /// default indentation. Fully formatted: | |
| /// ```python | |
| /// def f(): | |
| /// """first line | |
| /// line a | |
| /// line b | |
| /// """ | |
| /// ``` | |
| /// | |
| /// Tabs are counted by padding them to the next multiple of 8 according to | |
| /// [`str.expandtabs`](https://docs.python.org/3/library/stdtypes.html#str.expandtabs). | |
| /// | |
| /// Additionally, if any line in the docstring has less indentation than the docstring | |
| /// (effectively a negative indentation wrt. to the current level), we pad all lines to the | |
| /// level of the docstring with spaces. | |
| /// ```python | |
| /// def f(): | |
| /// """first line | |
| /// line a | |
| /// line b | |
| /// line c | |
| /// """ | |
| /// ``` | |
| /// Here line a is 3 columns negatively indented, so we pad all lines by an extra 3 spaces: | |
| /// ```python | |
| /// def f(): | |
| /// """first line | |
| /// line a | |
| /// line b | |
| /// line c | |
| /// """ | |
| /// ``` | |
| /// The indentation is rewritten to all-spaces when using [`IndentStyle::Space`]. | |
| /// The formatter preserves tab-indentations when using [`IndentStyle::Tab`], but doesn't convert | |
| /// `indent-width * spaces` to tabs because doing so could break ASCII art and other docstrings | |
| /// that use spaces for alignment. | |
| pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> FormatResult<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In retrospect I really should have checked because of course we do 😓
| ) -> Option<crate::NavigationTargets> { | ||
| match self { | ||
| DefinitionsOrTargets::Definitions(definitions) => { | ||
| definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), definitions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: The stub mapper type still feels a bit overkill to me. It feels like it could easily be replaced by an enum with two variants (map, don't map) and a method (that takes db as an argument) that does the mapping.
Maybe something for a follow up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I have several followup refactors planned here.
* main: [ty] Add precise inference for indexing, slicing and unpacking `NamedTuple` instances (#19560) Add rule code to GitLab description (#19896) [ty] render docstrings in hover (#19882) [ty] simplify return type of place_from_declarations (#19884) [ty] Various minor cleanups to tuple internals (#19891) [ty] Improve `sys.version_info` special casing (#19894)
…aints * dcreager/inferrable: (65 commits) this was right after all mark typevars inferrable as we go fix tests fix inference of constrained typevars [ty] Add precise inference for indexing, slicing and unpacking `NamedTuple` instances (#19560) Add rule code to GitLab description (#19896) [ty] render docstrings in hover (#19882) [ty] simplify return type of place_from_declarations (#19884) [ty] Various minor cleanups to tuple internals (#19891) [ty] Improve `sys.version_info` special casing (#19894) Don't cache files with diagnostics (#19869) [ty] support recursive type aliases (#19805) [ty] Remove unsafe `salsa::Update` implementations in `tuple.rs` (#19880) [ty] Function argument inlay hints (#19269) [ty] Remove Salsa interning for `TypedDictType` (#19879) Fix `lint.future-annotations` link (#19876) [ty] Reduce memory usage of `TupleSpec` and `TupleType` (#19872) [ty] Track heap usage of salsa structs (#19790) Update salsa to pull in tracked struct changes (#19843) [ty] simplify CycleDetector::visit signature (#19873) ...
This PR has several components:
DefinitionsOrTargetstype representing the partial evaluation ofGotoTarget::get_definition_targetsto ideally stop at gettingResolvedDefinitionsdeclaration_targets,definition_targets, anddocstringmethods toDefinitionsOrTargetsfor the 3 usecases we have for this operationdocstringis of course the key addition here, it uses the same basic logic thatsignature_helpwas using: first check the goto-declaration for docstrings, then check the goto-definition for docstrings.signature_helpto use the new APIs instead of implementing it itselfsignature_helpwill erroneously cache docs between functions that have the same type (hover docs don't have this bug)Examples of it working with stdlib, third party, and local definitions:



Notably modules don't work yet (followup work):

Notably we don't show docs for an item if you hover its actual definition (followup work, but also, not the most important):
