<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
requests.)
- Does this pull request include references to any relevant issues?
-->
## Summary
<!-- What's the purpose of the change? What does it do, and why? -->
## Test Plan
<!-- How was it tested? -->
## Summary
Negative subscripts are also indicative of a thing being subcriptable:
```python
class Subscriptable:
def __getitem__(self, key: int) -> int:
return 42
class NotSubscriptable: ...
def _(x: list[Subscriptable | NotSubscriptable]):
if not isinstance(x[-1], NotSubscriptable):
# After narrowing, x[-1] excludes NotSubscriptable, which means subscripting works
reveal_type(x[-1]) # revealed: Subscriptable & ~NotSubscriptable
reveal_type(x[-1][0]) # revealed: int
```
## Summary
Fixes some TODOs introduced in #22672 around cases like the following:
```python
from collections import namedtuple
from dataclasses import dataclass
NT = namedtuple("NT", "x y")
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(NT)
```
On main, `dataclass(NT)` emits `# error: [no-matching-overload]`
instead, which is wrong -- the overload does match! On main, the logic
proceeds as follows:
1. `dataclass` has two overloads:
- `dataclass(cls: type[_T], ...) -> type[_T]`
- `dataclass(cls: None = None, ...) -> Callable[[type[_T]], type[_T]]`
2. When `dataclass(NT)` is called:
- Arity check: Both overloads accept one positional argument, so both
pass.
- Type checking on first overload: `NT` matches `type[_T]`... but then
`invalid_dataclass_target()` runs and adds `InvalidDataclassApplication`
error
- Type checking on second overload: `NT` doesn't match `None`, so we
have a type error.
3. After type checking, both overloads have errors.
4. `matching_overload_index()` filters by
`overload.as_result().is_ok()`, which checks if `errors.is_empty()`.
Since both overloads have errors, neither matches...
5. We emit the "No overload matches arguments" error.
Instead, we now differentiate between non-matching errors, and errors
that occur when we _do_ match, but the call has some other semantic
failure.
## Summary
Consider `x: str | bytes` and then `x.split(" ")`. Because we have a
union, and at least one variant errors (`bytes` expects a `Buffer`, not
a `str`), we call `binding.report_diagnostics` for each variant. For the
`str` variant, it has two overloads that both match arity, but only one
actually matches the signature... So
`matching_overload_before_type_checking` is `None` (because they both
match arity), but we don't actually have an error, and we fall through
to `NO_MATCHING_OVERLOAD`.
If one variant succeeds, we should avoid reporting errors for it, even
if not _all_ variants matched.
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
requests.)
- Does this pull request include references to any relevant issues?
-->
## Summary
This PR is related to https://github.com/apache/airflow/issues/48389.
In Airflow 3, the function signature for `airflow.Dataset`,
`airflow.datasets.Dataset`, and `airflow.sdk.Asset` has changed. Below
is the function signature for `airflow.sdk.Asset`. The second positional
argument is `uri` which is of type `str`. In the older version, the
second positional argument can be a dictionary which is the `extra:
'dict | None' = None` argument. Therefore, we need to flag this in
Airflow 3.
```python
Asset(name: 'str | None' = None, uri: 'str | None' = None, *, group: 'str | None' = None, extra: 'dict | None' = None, watchers: 'list[AssetWatcher | SerializedAssetWatcher] | None' = None) -> 'None'
```
As this is a check on constructor call, we need to create a new method
`check_constructor_arguments`, instead of on method call. The new rule
check whether the index 1 (the second argument) is a dictionary (either
a literal or a `dict()` call).
## Test Plan
The `AIR303.py` test file have been updated with new test cases. The
snapshot has been updated and all other test cases passed.
@Lee-W , could you please review it when you have a chance, and let me
know if you have further feedback. Thanks!
## Summary
https://github.com/astral-sh/ty/issues/111
this pr adds an `invalid-dataclass-override` diagnostic when a custom
`__setattr__` or `__delattr__` is defined on a dataclass where
`frozen=True`
([docs](https://docs.python.org/3/library/dataclasses.html#frozen-instances))
### Runtime exception
```
Traceback (most recent call last):
File "/Users/justinchapman/src/ty-playground/main.py", line 4, in <module>
@dataclass(frozen=True)
~~~~~~~~~^^^^^^^^^^^^^
File "/Users/justinchapman/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/dataclasses.py", line 1295, in wrap
return _process_class(cls, init, repr, eq, order, unsafe_hash,
frozen, match_args, kw_only, slots,
weakref_slot)
File "/Users/justinchapman/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/dataclasses.py", line 1157, in _process_class
func_builder.add_fns_to_class(cls)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
File "/Users/justinchapman/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/dataclasses.py", line 516, in add_fns_to_class
raise TypeError(error_msg)
TypeError: Cannot overwrite attribute __setattr__ in class A
```
### Diagnostic
```
error[invalid-dataclass-override]: Cannot overwrite attribute __setattr__ in class A
--> /Users/justinchapman/src/ty-playground/main.py:6:5
|
4 | @dataclass(frozen=True)
5 | class A:
6 | def __setattr__(self, name: str, value: object) -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info: __setattr__
info: rule `invalid-dataclass-override` is enabled by default
Found 1 diagnostic
```
## Test Plan
- new mdtests
- e2e
- the `attrs` mypy primer diff looks to be a [true
positive](https://github.com/python-attrs/attrs/blob/main/tests/test_setattr.py#L373)
- the other results have been unpredictable and have changed every time
i pushed new code, even if the diagnostic logic didn't change...
---------
Co-authored-by: Carl Meyer <carl@astral.sh>
Hello,
This MR adds a new rule and its fix, `RUF069`,
`DuplicateEntryInDunderAll`. I'm using `RUF069` because we already have
[RUF068](https://github.com/astral-sh/ruff/pull/20585) and
[RUF069](https://github.com/astral-sh/ruff/pull/21079#issuecomment-3493839453)
in the works.
The rule job is to prevent users from accidentally adding duplicate
entries to `__all__`, which, for example, can result from copy-paste
mistakes.
It deals with the following syntaxes:
```python
__all__: list[str] = ["a", "a"]
__all__: typing.Any = ("a", "a")
__all__.extend(["a", "a"])
__all__ += ["a", "a"]
```
But it does not keep track of `__all__` contents, meaning the following
code snippet is a false negative:
```python
class A: ...
__all__ = ["A"]
__all__.extend(["A"])
```
## Violation Example
```console
RUF069 `__all__` contains duplicate entries
--> RUF069.py:2:17
|
1 | __all__ = ["A", "A", "B"]
| ^^^
help: Remove duplicate entries from `__all__`
1 | __all__ = ["A", "B"]
- __all__ = ["A", "A", "B"]
```
## Ecosystem Report
The `ruff-ecosystem` results contain seven violations in four projects,
all of them seem like true positives, with one instance appearing to be
an actual bug.
This [code
snippet](90d855985b/stubs/reportlab/reportlab/lib/rltempfile.pyi (L4))
from `reportlab` contains the same entry twice instead of exporting both
functions.
```python
def get_rl_tempdir(*subdirs: str) -> str: ...
def get_rl_tempfile(fn: str | None = None) -> str: ...
__all__ = ("get_rl_tempdir", "get_rl_tempdir")
```
Closes [#21945](https://github.com/astral-sh/ruff/issues/21945)
---------
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
# Summary
- Report a single combined UnusedNOQA diagnostic when any range
suppression
has one or more unused, disabled, or duplicate lint codes.
- Report a single combined InvalidRuleCode diagnostic for any range
suppression
with one or more invalid lint codes.
# Test Plan
Updated fixtures and snapshots
Fixes#22429, #21873
## Summary
Currently we don't think that namespace packages (e.g. `google` after
you've pip-installed `google-cloud-ndb`) have attributes such as
`__file__`, `__name__`, etc. This PR fixes that.
## Test Plan
Mdtests and snapshots.
- Adds `Tokens::split_at()` to get tokens before/after an offset.
- Updates `Suppressions::load_from_tokens` to take an `Indexer` and use
comment ranges to minimize the need for walking tokens looking for
indent/dedent.
Adapted from
https://github.com/astral-sh/ruff/pull/21441#pullrequestreview-3503773083Fixes#22087
The implementation here is (to me) surprisingly complicated. The main
complications are:
1. Trying to limit the redundant detection to as few of the symbols
we extract as possible. In particular, while I haven't done benchmarking
on this, I perceive the redundancy detection to be somewhat expensive
and auto-import can return lots of symbols. So we're careful to only do
this extra checking on (typically) small groups of symbols that could
possibly be merged.
2. Even by restricting our work, this merging process could still be
called quite a bit. (Thousands of times in my "standard data scienc-y
test environment.") So I went out of my way to amortize allocs.
3. Re-exports can form a chain and we want to find all of them.
4. We (probably) don't want to remove redundant re-exports unless
they share the same top-level module. Otherwise, e.g., a library
that re-exports another library's symbols could have all of its
re-exports dropped.
5. We want to only keep the top-most re-exports, and there may be
multiple such re-exports. So we keep all of them.
6. We can't assume anything about the relationship of re-exports
and the original definition. A re-export could be deeper in the
module hierarchy than the original definition or above it.
The invariants established by the constructors for `ModuleName`
guarantee that this is always available. It's useful for
determining the "top level" module for where a symbol lives.
Previously, we were constructing this at a higher level layer. But this
commit pushes it down a layer of abstraction. This shouldn't result in
constructing the fully qualified name any more frequently than we
previously did. Namely, we're still only doing it for symbols that match
the caller provided search query.
The motivation here is that we want to do some de-duplication based on
module name, and having the fully qualified name of a symbol seems like
the most straight-forward way to go about this. (We'll need one more
piece of information that we add in a subsequent commit.)