Conversation
535561f to
07e539f
Compare
|
3c65586 to
7c267d0
Compare
7c267d0 to
0ecfcc1
Compare
crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md
Outdated
Show resolved
Hide resolved
| from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F | ||
|
|
||
| FINAL_A = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed" | ||
| FINAL_B = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed" | ||
| FINAL_C = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed" | ||
| FINAL_D = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed" | ||
| FINAL_E = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed" | ||
| FINAL_F = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed" |
There was a problem hiding this comment.
relevant discussion on this: https://discuss.python.org/t/imported-final-variable/82429
I sort-of disagree with the resolution there, but don't have the energy to relitigate the question now 😆 and it comes up very rarely. It's good for us to follow the behaviour the spec mandates, anyway (no change requested!).
| Objects qualified with `Final` *can be modified*. `Final` represents a constant reference to an | ||
| object, but that object itself may still be mutable: |
There was a problem hiding this comment.
Yes -- pyre (and possibly pyrefly? Can't remember the exact status) has a feature called ImmutableRef, which they presented on at the typing summit this year. It's similar to Final but it also prevents any mutations to the symbol. It's a far more invasive and more complicated feature to implement; I'm not sure if it's ever going to be proposed for standardisation in the typing system.
| let (declared_ty, is_modifiable) = place_from_declarations(self.db(), declarations) | ||
| .and_then(|place_and_quals| { | ||
| Ok( | ||
| if matches!(place_and_quals.place, Place::Type(_, Boundness::Bound)) { |
There was a problem hiding this comment.
doesn't need to be done in this PR: should we add a Place::is_definitely_bound() method? It feels like we have a lot of these if matches!() conditions around the codebase
There was a problem hiding this comment.
I looked into it, but it seems like such a function would only have this callsite. In most other cases, we also match on the type at the same time.
| if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, node) { | ||
| builder.into_diagnostic("Assignment to `Final` symbol is not allowed"); | ||
| } |
There was a problem hiding this comment.
Could we add a subdiagnostic pointing to the previous definition where it was declared as Final?
There was a problem hiding this comment.
Yes, thanks for the suggestion! It's hard to do this for imported symbols (unless @Gankra adds a vector of Definitions to Place 😄), but I added a subdiagnostic for same-module Finals:
| 3 | MY_CONSTANT: Final[int] = 1 | ||
| | ----------- Original definition | ||
| 4 | | ||
| 5 | # more code | ||
| 6 | | ||
| 7 | MY_CONSTANT = 2 # error: [invalid-assignment] | ||
| | ^^^^^^^^^^^ Reassignment of `Final` symbol |
There was a problem hiding this comment.
even better might be
| 3 | MY_CONSTANT: Final[int] = 1 | |
| | ----------- Original definition | |
| 4 | | |
| 5 | # more code | |
| 6 | | |
| 7 | MY_CONSTANT = 2 # error: [invalid-assignment] | |
| | ^^^^^^^^^^^ Reassignment of `Final` symbol | |
| 3 | MY_CONSTANT: Final[int] = 1 | |
| | ---------- Symbol declared as `Final` here | |
| 4 | | |
| 5 | # more code | |
| 6 | | |
| 7 | MY_CONSTANT = 2 # error: [invalid-assignment] | |
| | ^^^^^^^^^^^^^^^ Symbol later reassigned here |
but probably not worth spending time on that now if that's hard to achieve!
There was a problem hiding this comment.
I tried to highlight the full range for the assignment, but for (annotated) assignments, full_range apparently only includes the left hand side target. I'll try to change that in a follow-up, since it might affect other diagnostics.
Highlighting the annotation for the original definition would also be nice, but is a bit harder because we would need to explicitly match on different definition kinds.
## Summary Implement [this suggestion](#19178 (comment)) by @AlexWaygood. 
| X: Final[int] = 1 | ||
| def inner(): | ||
| nonlocal X | ||
| # TODO: this should be an error |
There was a problem hiding this comment.
This TODO isn't going to last long: rebasing #19112 on top of this PR makes this line fail :)
There was a problem hiding this comment.
I added that just for you 🎁

Summary
Emit a diagnostic when a
Final-qualified symbol is modified. This first iteration only works for name targets. Tests with TODO comments were added for attribute assignments as well.related ticket: astral-sh/ty#158
Ecosystem impact
Correctly identified modification of a
Finalsymbol (behind a# type: ignore):- warning[unused-ignore-comment] sphinx/__init__.py:44:56: Unused blanket `type: ignore` directiveAnd the same here:
- warning[unused-ignore-comment] src/trio/_core/_run.py:128:45: Unused blanket `type: ignore` directiveTest Plan
New Markdown tests