Skip to content

[ty] add tests for workspaces#21741

Merged
Gankra merged 2 commits intomainfrom
gankra/add-tests
Dec 2, 2025
Merged

[ty] add tests for workspaces#21741
Gankra merged 2 commits intomainfrom
gankra/add-tests

Conversation

@Gankra
Copy link
Contributor

@Gankra Gankra commented Dec 1, 2025

Here are a bunch of (variously failing and passing) mdtests that reflect the kinds of issues people encounter when running ty over an entire workspace without sufficient hand-holding (especially because in the IDE it is unclear how to provide that hand-holding).

@Gankra Gankra added the internal An internal refactor or improvement label Dec 1, 2025
@Gankra Gankra requested a review from MichaReiser as a code owner December 1, 2025 21:07
@Gankra Gankra added the ty Multi-file analysis & type inference label Dec 1, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 1, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 1, 2025

mypy_primer results

Changes were detected when running on open source projects
pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ pandas-stubs/_typing.pyi:1207:16: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 5814 diagnostics
+ Found 5815 diagnostics

rotki (https://github.com/rotki/rotki)
- rotkehlchen/accounting/structures/processed_event.py:85:75: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- rotkehlchen/api/rest.py:1039:73: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 2040 diagnostics
+ Found 2038 diagnostics

No memory usage changes detected ✅

@Gankra
Copy link
Contributor Author

Gankra commented Dec 1, 2025

Note that some real-world examples are combinations of these. For instance "Tests Directory With Overlapping Names" actually works perfectly, but it fails in pyx because it's actually technically hitting "Current Directory Is Invalid Module Name" (because we try to resolve relative imports like .setup to aproj.tests.setup, which in pyx's case ends up being a-proj.tests.setup, which is rejected).

Some approaches to workspaces may dodge the invalid-module-name situation, some may not. I tried to get a good coverage of interesting cases that different solutions would show differences on.

@AlexWaygood AlexWaygood removed their request for review December 1, 2025 22:09
Comment on lines +11 to +13
While you can't syntactically refer to a module with an invalid name (i.e. one with a `-`, or that
has the same name as a keyword) there are plenty of situations where a module with an invalid name
can be run. For instance `python my-script.py` and `python my-proj/main.py` both work, even though
Copy link
Member

Choose a reason for hiding this comment

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

Yes, the way I think about it is that you can run a python script with an (mostly?) arbitrary name but whether you can import it depends on the segments relative to the search roots.

Comment on lines +16 to +17
Also, a sufficiently motivated programmer can technically use `importlib.import_module` which takes
strings and does in fact allow syntactically invalid module names.
Copy link
Member

Choose a reason for hiding this comment

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

okay, I didn't know that

z: int = 2
```

## Multiple Projects
Copy link
Member

Choose a reason for hiding this comment

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

Long term, I think we want a different solution to correctly support cases where the projects have different configurations (e.g. enabled / disabled some rules). But that shouldn't prevent us from doing better when there's no member-level configuration.

Comment on lines +171 to +172
[environment]
# This is similar to what we would compute for installed editables
Copy link
Member

Choose a reason for hiding this comment

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

Those tests work because we also apply our default environment.root paths in mdtests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe so

@sharkdp sharkdp removed their request for review December 2, 2025 08:19
@Gankra Gankra merged commit 015ab9e into main Dec 2, 2025
41 checks passed
@Gankra Gankra deleted the gankra/add-tests branch December 2, 2025 11:43
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Thanks for adding these tests!

Comment on lines +28 to +32
# error: [unresolved-import]
from .mod1 import x

# error: [unresolved-import]
from . import mod2
Copy link
Contributor

Choose a reason for hiding this comment

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

Both of these imports fail at runtime with ImportError: attempted relative import with no known parent package if you execute python my-main.py.

This is true (both at runtime and in ty today) even if we rename my-main.py to just main.py -- that is, it's unrelated to the use of an invalid module name. It's because you cannot relative-import top-level modules.

The only way these two imports can work is if all of these modules are imported as part of the same top-level parent package (that is, the directory containing these files is a Python package, and its parent directory is on the search path). (With the name my-main.py, the only way that can happen is via importlib.import_module.)

In other words, as this test is currently written, I think the above TODO is wrong, and these imports should error. If we add an outer containing directory and place it on the search path, then I think these imports should work (and already would with a valid module name). With that change, I think the TODO to make them also work with an invalid module name would be correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great callout -- I've put everything but mod3 under a tests/ directory and renamed my-main.py to my-mod.py, and made a clarifying comment that in this case we're assuming importlib shenanigans.

(We in fact have had one user complain that they had a random def.py that they just importlib.import_module but we wouldn't resolve relative imports from it)


# error: [unresolved-import]
from . import mod2
import mod3
Copy link
Contributor

Choose a reason for hiding this comment

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

The only scenario in which this import can work, and the two imports above also work, is overlapping search paths. The two relative imports can only work if this is all part of a containing package (as described above); this import can only work if these are all top-level modules. Overlapping search paths is the only way those two things can both be true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Fix mentioned in previous comment also handles this)


## Multiple Projects

It's common for a monorepo to define many separate projects that may or may not depend on eachother
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "eachother" is not a word, it's two words. (I assumed this was a typo but then saw multiple other occurrences below. Not worth fixing alone but if we're changing this file anyway we may as well.)

Suggested change
It's common for a monorepo to define many separate projects that may or may not depend on eachother
It's common for a monorepo to define many separate projects that may or may not depend on each other

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's Canadian English

Comment on lines +172 to +173
# This is similar to what we would compute for installed editables
extra-paths = ["aproj/src/", "bproj/src/"]
Copy link
Contributor

@carljm carljm Dec 2, 2025

Choose a reason for hiding this comment

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

The use of extra-paths here is significantly different from an installed editable, in that extra-paths are higher priority than environment.roots, while editable installs are lower priority. This seems like it could make a noticeable difference in the behavior of these tests, particularly those that test the interaction between same-named "outer" and "inner" packages.

It would be a bit more boilerplate, but I think these tests could better reflect a realistic scenario if they instead used python = config and then created .pth files in the fake site-packages that pointed to aproj/src, bproj/src -- that would actually get those paths added as editable installs. import/site_packages_discovery.md demonstrates this, minus the .pth file part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also a great point, I misremembered the order of extra and first-party.

I think per discussion of "we should be auto-sorting nested search-paths" I probably want to have at least one test that uses editables and one that uses extra-paths (I imagine some users will find extra-paths, do this kind of thing manually, and it will behoove us to untangle it).

```py
# TODO: there should be no errors in this file.

# error: [unresolved-import]
Copy link
Contributor

Choose a reason for hiding this comment

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

So I guess what happens here is we decide the package we should import is a.tests.setup, but then (because ./a/src/ is a higher priority search path than .) we go looking for a/src/a/tests/setup.py and don't find it.

This behavior seems correct (i.e. matches runtime)? Except that as discussed above, a more realistic test would not use extra-paths, which would make . higher priority than ./a/src/ and thus totally change the behavior of this test.

Comment on lines +332 to +333
The same situation as the previous test but `tests/__init__.py` is also defined, in case that
complicates the situation.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it can make a difference to this test. In theory testing various cases makes sense, but there are an infinite number of possible scenarios to test, and I'm not sure it scales to sort of randomly test little tweaks "in case", even if we haven't ever seen evidence (i.e. a failure in real code) that it makes a difference. So I'm kind of not convinced we should have both this test and the previous one, unless there's an observed behavior difference.

Copy link
Contributor Author

@Gankra Gankra Dec 3, 2025

Choose a reason for hiding this comment

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

This particular case was because I was perplexed by seeing this random variation in pyx's codebase and got a bit paranoid about whether it mattered if tests was a namespace package or a normal package. I think the __init__.py version is "strictly harder" for us to handle, so I should probably only have that one.


### Tests Package Absolute Importing `main.py`

The same as the previous case but `tests/__init__.py` exists in case that causes different issues.
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above.

@carljm

This comment was marked as off-topic.

@carljm

This comment was marked as off-topic.

dcreager added a commit that referenced this pull request Dec 2, 2025
* origin/main: (67 commits)
  Move `Token`, `TokenKind` and `Tokens` to `ruff-python-ast` (#21760)
  [ty] Don't confuse multiple occurrences of `typing.Self` when binding bound methods (#21754)
  Use our org-wide Renovate preset (#21759)
  Delete `my-script.py` (#21751)
  [ty] Move `all_members`, and related types/routines, out of `ide_support.rs` (#21695)
  [ty] Fix find-references for import aliases (#21736)
  [ty] add tests for workspaces (#21741)
  [ty] Stop testing the (brittle) constraint set display implementation (#21743)
  [ty] Use generator over list comprehension to avoid cast (#21748)
  [ty] Add a diagnostic for prohibited `NamedTuple` attribute overrides (#21717)
  [ty] Fix subtyping with `type[T]` and unions (#21740)
  Use `npm ci --ignore-scripts` everywhere (#21742)
  [`flake8-simplify`] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (`SIM222`, `SIM223`) (#21479)
  [`flake8-use-pathlib`] Mark fixes unsafe for return type changes (`PTH104`, `PTH105`, `PTH109`, `PTH115`) (#21440)
  [ty] Fix auto-import code action to handle pre-existing import
  Enable PEP 740 attestations when publishing to PyPI (#21735)
  [ty] Fix find references for type defined in stub (#21732)
  Use OIDC instead of codspeed token (#21719)
  [ty] Exclude `typing_extensions` from completions unless it's really available
  [ty] Fix false positives for `class F(Generic[*Ts]): ...` (#21723)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal An internal refactor or improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants