Skip to content

Path refactor - #2959

Merged
etianen merged 17 commits into
python-trio:masterfrom
etianen:dh/path-refactor
Mar 11, 2024
Merged

Path refactor#2959
etianen merged 17 commits into
python-trio:masterfrom
etianen:dh/path-refactor

Conversation

@etianen

@etianenetianen commented Feb 17, 2024

Copy link
Copy Markdown
Contributor

This PR implements the proposed refactor in #2944. As well as fixing the Python 3.13 problems, it adds some cool new things! 😎

The big change 💥

trio.Path now subclasses pathlib.PurePath!

This means all the sync method forwarding can be completely removed!

Since it now fits into the pathlib class hierarchy, it can be used interchangeably with other pure paths and methods that take pure paths. This also has the nice effect of making the wrapped async methods just work with regard to input types.

It's also now a non-virtual subclass of os.PathLike[str], which might help some static analysis tools...?

Two concrete subclasses PosixPath and WindowsPath also now exist, matching how pathlib does things. Instantiating a Path actually gets you the appropriate one for the platform, just like pathlib.

The medium change

Async method wrapping is now explicit, fixing Python 3.13. There are three high-level wrapper helpers that work for most cases (_wrap_method, _wrap_method_path and _wrap_method_path_iterable). For methods that don't fit into these, a low-level (_wraps_async) is used.

Other changes

Path is now slotted, to match pathlib. A minor speed boost, with luck!

Fixing Python 3.13

Unlike #2955, this fix actually works! Check it out:

================================================= test session starts ==================================================
platform darwin -- Python 3.13.0a3+, pytest-8.0.1, pluggy-1.4.0
rootdir: /Users/dave.hall/Workspace/trio
configfile: pyproject.toml
collected 33 items src/trio/_tests/test_path.py ................................. [100%]
================================================== 33 passed in 0.05s ==================================================

My battle with pyright --verifytypes 😭

The pyright --verifytypes check does not like this syntax:

stat=_wrap_method(pathlib.Path.stat)

It claims this is an ambiguous type, asking me to annotate it with something that can't be expressed with the Python type system except for a per-method unweildy protocol!

The workaround would be to put in a dummy method and use a decorator syntax, which pyright thinks is absolutely okay despite inferring to exactly the same thing! It's also pretty disgusting to look at, and bloats the implementation with boilerplate.

@_wrap_method(pathlib.Path.stat)defstat(self) ->Any:
raiseAssertionError("unreachable!")

I can't see any way to disable this check for just this file, or just these lines. Any suggestions?! 😭

@codecov

codecovBot commented Feb 17, 2024

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.64%. Comparing base (379d99b) to head (ba5b6c7).

Additional details and impacted files
@@ Coverage Diff @@## master #2959 +/- ##
==========================================
- Coverage 99.64% 99.64% -0.01% 
==========================================
Files 117 117 Lines 17643 17594 -49 Branches 3176 3171 -5 ==========================================
- Hits 17581 17532 -49 
Misses 43 43 Partials 19 19 
FilesCoverage Δ
src/trio/__init__.py100.00% <100.00%> (ø)
src/trio/_path.py100.00% <100.00%> (ø)
src/trio/_tests/test_exports.py99.61% <100.00%> (+<0.01%)⬆️
src/trio/_tests/test_path.py100.00% <100.00%> (ø)


assert not hasattr(MockWrapper, "_private")


Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

All of this can be removed, now we're not using metaclasses.

assert_type(path.drive, str)
assert_type(path.root, str)
assert_type(path.anchor, str)
assert_type(path.parents[3], pathlib.Path)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I count this as a bugfix!

@etianen
etianenforce-pushed the dh/path-refactor branch 4 times, most recently from 5776eaa to 0dbda03CompareFebruary 17, 2024 16:45
Comment threadsrc/trio/_tests/test_exports.py
@etianen
etianenforce-pushed the dh/path-refactor branch 6 times, most recently from cb177d5 to c12689aCompareFebruary 17, 2024 17:32
@etianen
etianen marked this pull request as ready for review February 17, 2024 17:45

@CoolCat467CoolCat467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Questions about a few minor things

Comment threadsrc/trio/_path.py Outdated
Comment threadsrc/trio/_path.py
@jakkdl

Copy link
Copy Markdown
Member

The pyright --verifytypes check does not like this syntax:
stat = _wrap_method(pathlib.Path.stat)
It claims this is an ambiguous type, asking me to annotate it with something that can't be expressed with the Python type system except for a per-method unweildy protocol!
The workaround would be to put in a dummy method and use a decorator syntax, which pyright thinks is absolutely okay despite inferring to exactly the same thing! It's also pretty disgusting to look at, and bloats the implementation with boilerplate.

The error message is specifically about it being ambigious and could be inferred differently by other type checkers, so whether it's being inferred to the same thing by pyright is not really relevant. In most other cases we've resorted to doing the disgusting/bloated boilerplate to get around these errors, but I'll have a closer look later on whether I think it's worth it. We are after all running type checks on both mypy & pyright, so we can probably ignore it.

I can't see any way to disable this check for just this file, or just these lines. Any suggestions?! 😭

Maybe I should resuscitate #2910 and get the output-filtering script finished/merged.

@etianen

Copy link
Copy Markdown
ContributorAuthor

The error message is specifically about it being ambiguous and could be inferred differently by other type checkers

I think I'm disagreeing here with pyright. I can write the same thing using a decorator syntax, which provides no additional typing information, and it's happy. It just seems to be complaining that I'm doing a variable assignment and not explicitly annotating the variable.

This is related to my point about the static visibility linter, really. This check seems to have a very uncompromising idea of what's ambiguous. The use of ParamSpec and Concatenate here has, AFAIK, absolutely only one possible meaning. That's not ambiguous! It's simply not explicit.

I'm a huge fan of static analysis tools in general, and favor very strict typing and linting. But to me, these checks feel overboard. I realize that this exactly the argument people make to me about strict mypy checks, so maybe it's a case of comfort levels? But at the point inheritance and strictly-typed metaprograming become forbidden, it feels too much to me.

I'm left in a bit of a quandry for this PR though. Hopefully you have a solution in mind with #2910.

@etianen

Copy link
Copy Markdown
ContributorAuthor

Also, apologies if the tone of my messages comes over as overly critical! I'm a big fan of how welcoming this project is to new contributors, so this stems from a concern about placing too-high barriers in front of new contributions.

It's hard to express frustration without sounding... frustrated. 🥵

@jakkdl

Copy link
Copy Markdown
Member

disagreeing with pyright seems very fair in this case, and given how complex the Path wrapping is already I'm open to just ignoring what --verifytypes says. I don't know for sure why it forbids assignment here.
Will look at stuff tomorrow :)

@etianen

Copy link
Copy Markdown
ContributorAuthor

Thanks for the feedback! I've implemented the suggested changes. The new docs in particular look very nice! 💪

image

I'll await your findings on the --verifytypes problem. I couldn't find a way to selectively ignore this file, so hopefully you'll have a better time of it than me! 😅

@CoolCat467

Copy link
Copy Markdown
Member

@etianen

Copy link
Copy Markdown
ContributorAuthor

One slightly interesting point https://trio--2959.org.readthedocs.build/en/2959/reference-io.html#trio.Path.link_to

I'm assuming these docs were built not on Python 3.12? The method is indeed gone in 3.12.

@A5rocksA5rocks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I forgot: could you add a newsfragment?

Also a couple comments, ranging from nitpicks to asking for a test or two

Comment threadsrc/trio/_path.py
Comment threadsrc/trio/_path.py
os.PathLike.register(Path)
) -> AsyncIOWrapper[IO[Any]]: ...

@_wraps_async(pathlib.Path.open) # type: ignore[misc] # Overload return mismatch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this is what I think it is (mismatch because we don't have overloads for "r" vs "w" vs whatever), then IMO this should have a TODO. I realize you didn't write this type ignore and just copied it over, but yeah.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is probably actually because we return an AsyncIOWrapper, not the sync file.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's neither! It's Type of decorated function contains type "Any" ("Callable[[Path, str, int, Optional[str], Optional[str], Optional[str]], Coroutine[Any, Any, AsyncIOWrapper[IO[Any]]]]")

For some reason, using any in a decorated function is naughty. It's not naughty if the function isn't decorated though. 🤷

Comment threadsrc/trio/_tests/test_path.py
Comment threadsrc/trio/_tests/type_tests/path.py Outdated
Comment threadsrc/trio/_tests/type_tests/path.py
@A5rocks

Copy link
Copy Markdown
Contributor

The only solution I can see is the solution in #2910. Do we keep this open and wait for that to merge, or do we temporarily disable the check?

I'm sure specifically filtering pyright errors should be a pretty simple thing to do; let's wait a few days and see if that happens. If it doesn't, then temporarily disabling the check sounds good. (I'm hoping we can get a release out in ~a week, so that this and a bunch of changes can go out)

@TeamSpen210

Copy link
Copy Markdown
Contributor

Perhaps we should open an issue for Pyright. Thinking about it, it seems like a bug that verifytypes doesn't pick up on inference being required for the decorator, that's inconsistent.

@etianen

Copy link
Copy Markdown
ContributorAuthor

Hopefully that's all points addressed! Some directly, others laterally.

@A5rocksA5rocks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good, I like that this decreases the total amount of code!

(hopefully someone else can also give this a lookover because my passes might have missed something)

@CoolCat467CoolCat467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks pretty good other than one thing I noticed

Comment threadsrc/trio/_tests/type_tests/path.py Outdated

@CoolCat467CoolCat467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very nice!

@A5rocks

A5rocks commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

pyright changes are merged, you should be able to python src/trio/_tests/check_type_completeness.py --overwrite-file (or something like that) and that should be it! i tried doing that but for some reason github codespaces doesn't recognize that i can push to the branch and i dont want to bother setting this up locally.

@jakkdl

Copy link
Copy Markdown
Member

I added some logic to ignore everything instead of listing all the errors in the json. I feel like has_docstring_at_runtime should've been able to pick up the docstrings, but I haven't bothered figuring out why it doesn't.

@etianen
etianen merged commit f890f8f into python-trio:masterMar 11, 2024
@etianen
etianen deleted the dh/path-refactor branch March 11, 2024 21:35
@etianen

Copy link
Copy Markdown
ContributorAuthor

It's in! Thanks all!

@jakkdl

Copy link
Copy Markdown
Member

Thank you! And sorry for the delay with finishing up #2910 :)

@A5rocks

Copy link
Copy Markdown
Contributor

fyi this went out in 0.25.0!

Sign up for freeto 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.

5 participants

@etianen@jakkdl@CoolCat467@A5rocks@TeamSpen210