Skip to content
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

Inline some types #221

Merged
merged 7 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@
# Be strict about any broken references
nitpicky = True


nitpick_ignore = [
('py:class', '_io.BufferedRandom'),
('py:class', '_io.BufferedReader'),
('py:class', '_io.BufferedWriter'),
('py:class', '_io.FileIO'),
('py:class', '_io.TextIOWrapper'),
('py:class', 'Literal[-1, 1]'),
('py:class', 'OpenBinaryMode'),
('py:class', 'OpenBinaryModeReading'),
('py:class', 'OpenBinaryModeUpdating'),
('py:class', 'OpenBinaryModeWriting'),
('py:class', 'OpenTextMode'),
]

# Include Python intersphinx mapping to prevent failures
# jaraco/skeleton#51
extensions += ['sphinx.ext.intersphinx']
Expand Down
1 change: 1 addition & 0 deletions newsfragments/215.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Inlined some types.
175 changes: 175 additions & 0 deletions path/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
foo_txt = Path("bar") / "foo.txt"
"""

from __future__ import annotations

import builtins
import sys
import warnings
import os
Expand Down Expand Up @@ -49,6 +52,35 @@
with contextlib.suppress(ImportError):
import grp

from io import (
BufferedRandom,
BufferedReader,
BufferedWriter,
FileIO,
TextIOWrapper,
)
from typing import (
Any,
BinaryIO,
Callable,
IO,
Iterator,
Optional,
overload,
)

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from _typeshed import (
OpenBinaryMode,
OpenBinaryModeUpdating,
OpenBinaryModeReading,
OpenBinaryModeWriting,
OpenTextMode,
)
from typing_extensions import Literal

from . import matchers
from . import masks
from . import classes
Expand Down Expand Up @@ -652,6 +684,90 @@ def iglob(self, pattern):
#
# --- Reading or writing an entire file at once.

@overload
def open(
self,
mode: OpenTextMode = ...,
buffering: int = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Optional[Callable[[str, int], int]] = ...,
) -> TextIOWrapper: ...

@overload
def open(
self,
mode: OpenBinaryMode,
buffering: Literal[0],
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Callable[[str, int], int] = ...,
) -> FileIO: ...

@overload
def open(
self,
mode: OpenBinaryModeUpdating,
buffering: Literal[-1, 1] = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Callable[[str, int], int] = ...,
) -> BufferedRandom: ...

@overload
def open(
self,
mode: OpenBinaryModeReading,
buffering: Literal[-1, 1] = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Callable[[str, int], int] = ...,
) -> BufferedReader: ...

@overload
def open(
self,
mode: OpenBinaryModeWriting,
buffering: Literal[-1, 1] = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Callable[[str, int], int] = ...,
) -> BufferedWriter: ...

@overload
def open(
self,
mode: OpenBinaryMode,
buffering: int,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Callable[[str, int], int] = ...,
) -> BinaryIO: ...

@overload
def open(
self,
mode: str,
buffering: int = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Callable[[str, int], int] = ...,
) -> IO[Any]: ...

Comment on lines +687 to +770
Copy link
Contributor

@bswck bswck Apr 10, 2024

Choose a reason for hiding this comment

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

To reduce runtime overhead, you could move all the overloads into an if TYPE_CHECKING block too.
It will

  • not create temporary functions
  • let you move typing.overload import into type checking imports, keeping it away from the runtime variable namespace

while still working out well in type checking. Similarly for other overloads.

Copy link
Owner Author

Choose a reason for hiding this comment

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

You know what would be even nicer is if the type system provided a way to indicate that a function or method is a wrapper around another function/method and takes the exact same or slightly altered arguments, something like:

diff --git a/path/__init__.py b/path/__init__.py
index c803459..d1441d9 100644
--- a/path/__init__.py
+++ b/path/__init__.py
@@ -689,90 +689,7 @@ class Path(str):
     #
     # --- Reading or writing an entire file at once.
 
-    @overload
-    def open(
-        self,
-        mode: OpenTextMode = ...,
-        buffering: int = ...,
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] | None = ...,
-    ) -> TextIOWrapper: ...
-
-    @overload
-    def open(
-        self,
-        mode: OpenBinaryMode,
-        buffering: Literal[0],
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] = ...,
-    ) -> FileIO: ...
-
-    @overload
-    def open(
-        self,
-        mode: OpenBinaryModeUpdating,
-        buffering: Literal[-1, 1] = ...,
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] = ...,
-    ) -> BufferedRandom: ...
-
-    @overload
-    def open(
-        self,
-        mode: OpenBinaryModeReading,
-        buffering: Literal[-1, 1] = ...,
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] = ...,
-    ) -> BufferedReader: ...
-
-    @overload
-    def open(
-        self,
-        mode: OpenBinaryModeWriting,
-        buffering: Literal[-1, 1] = ...,
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] = ...,
-    ) -> BufferedWriter: ...
-
-    @overload
-    def open(
-        self,
-        mode: OpenBinaryMode,
-        buffering: int,
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] = ...,
-    ) -> BinaryIO: ...
-
-    @overload
-    def open(
-        self,
-        mode: str,
-        buffering: int = ...,
-        encoding: str | None = ...,
-        errors: str | None = ...,
-        newline: str | None = ...,
-        closefd: bool = ...,
-        opener: Callable[[str, int], int] = ...,
-    ) -> IO[Any]: ...
-
+    @typing.reflect
     def open(self, *args, **kwargs):
         """Open this file and return a corresponding file object.

Then, a reader of this could wouldn't be accosted by the copy-pasta from the upstream implementation and the type system could readily reflect the overloaded types.

Or even better, make that the default behavior, so that if no type signature is applied, the signature is implied by the way it's used.

Copy link
Owner Author

Choose a reason for hiding this comment

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

To reduce runtime overhead, you could move all the overloads into a TYPE_CHECKING block too.

Proposed in #224. Is that what you meant?

Copy link
Contributor

Choose a reason for hiding this comment

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

There is something like typing.reflect already, but it's not a well-documented feature :) Namely, functools.wraps.

Copy link
Contributor

@bswck bswck Apr 10, 2024

Choose a reason for hiding this comment

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

The reason I think it's a type-checking equivalent of our typing.reflect is that functools.wraps takes a callable of signature P and the wrapper is then typed to be of signature P too.

Copy link
Contributor

Choose a reason for hiding this comment

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

To reduce runtime overhead, you could move all the overloads into a TYPE_CHECKING block too.

Proposed in #224. Is that what you meant?

Yeah, gonna reply there.

Copy link
Owner Author

Choose a reason for hiding this comment

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

There is something like typing.reflect already, but it's not a well-documented feature :) Namely, functools.wraps.

I'm familiar with functools.wraps, but I've not seen it used except for it's documented primary purpose (wrapping a function in a decorator). I've created #225 to explore that possibility.

def open(self, *args, **kwargs):
"""Open this file and return a corresponding file object.

Expand All @@ -665,6 +781,45 @@ def bytes(self):
with self.open('rb') as f:
return f.read()

@overload
def chunks(
self,
size: int,
mode: OpenTextMode = ...,
buffering: int = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Optional[Callable[[str, int], int]] = ...,
) -> Iterator[str]: ...

@overload
def chunks(
self,
size: int,
mode: OpenBinaryMode,
buffering: int = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Optional[Callable[[str, int], int]] = ...,
) -> Iterator[builtins.bytes]: ...

@overload
def chunks(
self,
size: int,
mode: str,
buffering: int = ...,
encoding: Optional[str] = ...,
errors: Optional[str] = ...,
newline: Optional[str] = ...,
closefd: bool = ...,
opener: Optional[Callable[[str, int], int]] = ...,
) -> Iterator[Union[str, builtins.bytes]]: ...

def chunks(self, size, *args, **kwargs):
"""Returns a generator yielding chunks of the file, so it can
be read piece by piece with a simple for loop.
Expand Down Expand Up @@ -718,6 +873,26 @@ def text(self, encoding=None, errors='strict'):
)
return U_NEWLINE.sub('\n', self.read_text(encoding, errors))

@overload
def write_text(
self,
text: str,
encoding: Optional[str] = ...,
errors: str = ...,
linesep: Optional[str] = ...,
append: bool = ...,
) -> None: ...

@overload
def write_text(
self,
text: builtins.bytes,
encoding: None = ...,
errors: str = ...,
linesep: Optional[str] = ...,
append: bool = ...,
) -> None: ...

def write_text(
self, text, encoding=None, errors='strict', linesep=os.linesep, append=False
):
Expand Down
Loading
Loading