Skip to content

Commit 55be42f

Browse files
add __fspath__ support to os.path (#2053)
Fixes #1997, #2068. This is tricky because we need to get the return values right (see #1960 for prior attempts) and we often run into python/mypy#3644. I found that I could express most signatures correctly using a series of overloads. A few other changes in here: - Added splitunc, which according to https://docs.python.org/3/library/os.path.html should exist in both Unix and Windows. - Made the second argument to os.path.curdir Optional to match the implementation. - Fixed os.path.split, whose previous Path-aware signature triggered python/mypy#3644.
1 parent 5505cb8 commit 55be42f

File tree

2 files changed

+201
-64
lines changed

2 files changed

+201
-64
lines changed

stdlib/2/os/path.pyi

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,106 @@
11
# Stubs for os.path
22
# Ron Murawski <[email protected]>
33

4-
# based on http://docs.python.org/3.2/library/os.path.html
5-
# adapted for 2.7 by Michal Pokorny
4+
from posix import stat_result
65
import sys
76
from typing import (
87
overload, List, Any, AnyStr, Sequence, Tuple, BinaryIO, TextIO,
9-
TypeVar, Union, Text, Callable
8+
TypeVar, Union, Text, Callable, Optional
109
)
1110

1211
_T = TypeVar('_T')
1312

1413
if sys.version_info >= (3, 6):
1514
from builtins import _PathLike
1615
_PathType = Union[bytes, Text, _PathLike]
16+
_StrPath = Union[Text, _PathLike[Text]]
17+
_BytesPath = Union[bytes, _PathLike[bytes]]
1718
else:
1819
_PathType = Union[bytes, Text]
20+
_StrPath = Text
21+
_BytesPath = bytes
1922

2023
# ----- os.path variables -----
21-
supports_unicode_filenames = False
24+
supports_unicode_filenames: bool
2225
# aliases (also in os)
23-
curdir = ... # type: str
24-
pardir = ... # type: str
25-
sep = ... # type: str
26-
altsep = ... # type: str
27-
extsep = ... # type: str
28-
pathsep = ... # type: str
29-
defpath = ... # type: str
30-
devnull = ... # type: str
26+
curdir: str
27+
pardir: str
28+
sep: str
29+
altsep: str
30+
extsep: str
31+
pathsep: str
32+
defpath: str
33+
devnull: str
3134

3235
# ----- os.path function stubs -----
33-
def abspath(path: AnyStr) -> AnyStr: ...
34-
def basename(path: AnyStr) -> AnyStr: ...
36+
if sys.version_info >= (3, 6):
37+
# Overloads are necessary to work around python/mypy#3644.
38+
@overload
39+
def abspath(path: _PathLike[AnyStr]) -> AnyStr: ...
40+
@overload
41+
def abspath(path: AnyStr) -> AnyStr: ...
42+
@overload
43+
def basename(path: _PathLike[AnyStr]) -> AnyStr: ...
44+
@overload
45+
def basename(path: AnyStr) -> AnyStr: ...
46+
@overload
47+
def dirname(path: _PathLike[AnyStr]) -> AnyStr: ...
48+
@overload
49+
def dirname(path: AnyStr) -> AnyStr: ...
50+
@overload
51+
def expanduser(path: _PathLike[AnyStr]) -> AnyStr: ...
52+
@overload
53+
def expanduser(path: AnyStr) -> AnyStr: ...
54+
@overload
55+
def expandvars(path: _PathLike[AnyStr]) -> AnyStr: ...
56+
@overload
57+
def expandvars(path: AnyStr) -> AnyStr: ...
58+
@overload
59+
def normcase(path: _PathLike[AnyStr]) -> AnyStr: ...
60+
@overload
61+
def normcase(path: AnyStr) -> AnyStr: ...
62+
@overload
63+
def normpath(path: _PathLike[AnyStr]) -> AnyStr: ...
64+
@overload
65+
def normpath(path: AnyStr) -> AnyStr: ...
66+
if sys.platform == 'win32':
67+
@overload
68+
def realpath(path: _PathLike[AnyStr]) -> AnyStr: ...
69+
@overload
70+
def realpath(path: AnyStr) -> AnyStr: ...
71+
else:
72+
@overload
73+
def realpath(filename: _PathLike[AnyStr]) -> AnyStr: ...
74+
@overload
75+
def realpath(filename: AnyStr) -> AnyStr: ...
3576

36-
if sys.version_info >= (3, 5):
77+
else:
78+
def abspath(path: AnyStr) -> AnyStr: ...
79+
def basename(path: AnyStr) -> AnyStr: ...
80+
def dirname(path: AnyStr) -> AnyStr: ...
81+
def expanduser(path: AnyStr) -> AnyStr: ...
82+
def expandvars(path: AnyStr) -> AnyStr: ...
83+
def normcase(path: AnyStr) -> AnyStr: ...
84+
def normpath(path: AnyStr) -> AnyStr: ...
85+
if sys.platform == 'win32':
86+
def realpath(path: AnyStr) -> AnyStr: ...
87+
else:
88+
def realpath(filename: AnyStr) -> AnyStr: ...
89+
90+
if sys.version_info >= (3, 6):
91+
# In reality it returns str for sequences of _StrPath and bytes for sequences
92+
# of _BytesPath, but mypy does not accept such a signature.
93+
def commonpath(paths: Sequence[_PathType]) -> Any: ...
94+
elif sys.version_info >= (3, 5):
3795
def commonpath(paths: Sequence[AnyStr]) -> AnyStr: ...
3896

3997
# NOTE: Empty lists results in '' (str) regardless of contained type.
4098
# Also, in Python 2 mixed sequences of Text and bytes results in either Text or bytes
4199
# So, fall back to Any
42-
def commonprefix(list: Sequence[AnyStr]) -> Any: ...
100+
def commonprefix(list: Sequence[_PathType]) -> Any: ...
43101

44-
def dirname(path: AnyStr) -> AnyStr: ...
45102
def exists(path: _PathType) -> bool: ...
46103
def lexists(path: _PathType) -> bool: ...
47-
def expanduser(path: AnyStr) -> AnyStr: ...
48-
def expandvars(path: AnyStr) -> AnyStr: ...
49104

50105
# These return float if os.stat_float_times() == True,
51106
# but int is a subclass of float.
@@ -75,26 +130,41 @@ if sys.version_info < (3, 0):
75130
def join(__p1: bytes, __p2: bytes, __p3: Text, *p: _PathType) -> Text: ...
76131
@overload
77132
def join(__p1: bytes, __p2: bytes, __p3: bytes, __p4: Text, *p: _PathType) -> Text: ...
133+
elif sys.version_info >= (3, 6):
134+
# Mypy complains that the signatures overlap (same for relpath below), but things seem to behave correctly anyway.
135+
@overload
136+
def join(path: _StrPath, *paths: _StrPath) -> Text: ... # type: ignore
137+
@overload
138+
def join(path: _BytesPath, *paths: _BytesPath) -> bytes: ...
78139
else:
79140
def join(path: AnyStr, *paths: AnyStr) -> AnyStr: ...
80141

81-
def normcase(path: AnyStr) -> AnyStr: ...
82-
def normpath(path: AnyStr) -> AnyStr: ...
83-
if sys.platform == 'win32':
84-
def realpath(path: AnyStr) -> AnyStr: ...
85-
else:
86-
def realpath(filename: AnyStr) -> AnyStr: ...
87-
def relpath(path: AnyStr, start: _PathType = ...) -> AnyStr: ...
142+
@overload
143+
def relpath(path: _StrPath, start: Optional[_StrPath] = ...) -> Text: ... # type: ignore
144+
@overload
145+
def relpath(path: _BytesPath, start: _BytesPath) -> bytes: ...
88146

89147
def samefile(path1: _PathType, path2: _PathType) -> bool: ...
90148
def sameopenfile(fp1: int, fp2: int) -> bool: ...
91-
# TODO
92-
# def samestat(stat1: stat_result,
93-
# stat2: stat_result) -> bool: ... # Unix only
149+
def samestat(stat1: stat_result, stat2: stat_result) -> bool: ...
94150

95-
def split(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
96-
def splitdrive(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
97-
def splitext(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
151+
if sys.version_info >= (3, 6):
152+
@overload
153+
def split(path: _PathLike[AnyStr]) -> Tuple[AnyStr, AnyStr]: ...
154+
@overload
155+
def split(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
156+
@overload
157+
def splitdrive(path: _PathLike[AnyStr]) -> Tuple[AnyStr, AnyStr]: ...
158+
@overload
159+
def splitdrive(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
160+
@overload
161+
def splitext(path: _PathLike[AnyStr]) -> Tuple[AnyStr, AnyStr]: ...
162+
@overload
163+
def splitext(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
164+
else:
165+
def split(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
166+
def splitdrive(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
167+
def splitext(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
98168

99169
def splitunc(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ... # Windows only, deprecated
100170

stdlib/3/os/path.pyi

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,106 @@
11
# Stubs for os.path
22
# Ron Murawski <[email protected]>
33

4-
# based on http://docs.python.org/3.2/library/os.path.html
5-
# adapted for 2.7 by Michal Pokorny
4+
from posix import stat_result
65
import sys
76
from typing import (
87
overload, List, Any, AnyStr, Sequence, Tuple, BinaryIO, TextIO,
9-
TypeVar, Union, Text, Callable
8+
TypeVar, Union, Text, Callable, Optional
109
)
1110

1211
_T = TypeVar('_T')
1312

1413
if sys.version_info >= (3, 6):
1514
from builtins import _PathLike
1615
_PathType = Union[bytes, Text, _PathLike]
16+
_StrPath = Union[Text, _PathLike[Text]]
17+
_BytesPath = Union[bytes, _PathLike[bytes]]
1718
else:
1819
_PathType = Union[bytes, Text]
20+
_StrPath = Text
21+
_BytesPath = bytes
1922

2023
# ----- os.path variables -----
21-
supports_unicode_filenames = False
24+
supports_unicode_filenames: bool
2225
# aliases (also in os)
23-
curdir = ... # type: str
24-
pardir = ... # type: str
25-
sep = ... # type: str
26-
altsep = ... # type: str
27-
extsep = ... # type: str
28-
pathsep = ... # type: str
29-
defpath = ... # type: str
30-
devnull = ... # type: str
26+
curdir: str
27+
pardir: str
28+
sep: str
29+
altsep: str
30+
extsep: str
31+
pathsep: str
32+
defpath: str
33+
devnull: str
3134

3235
# ----- os.path function stubs -----
33-
def abspath(path: AnyStr) -> AnyStr: ...
34-
def basename(path: AnyStr) -> AnyStr: ...
36+
if sys.version_info >= (3, 6):
37+
# Overloads are necessary to work around python/mypy#3644.
38+
@overload
39+
def abspath(path: _PathLike[AnyStr]) -> AnyStr: ...
40+
@overload
41+
def abspath(path: AnyStr) -> AnyStr: ...
42+
@overload
43+
def basename(path: _PathLike[AnyStr]) -> AnyStr: ...
44+
@overload
45+
def basename(path: AnyStr) -> AnyStr: ...
46+
@overload
47+
def dirname(path: _PathLike[AnyStr]) -> AnyStr: ...
48+
@overload
49+
def dirname(path: AnyStr) -> AnyStr: ...
50+
@overload
51+
def expanduser(path: _PathLike[AnyStr]) -> AnyStr: ...
52+
@overload
53+
def expanduser(path: AnyStr) -> AnyStr: ...
54+
@overload
55+
def expandvars(path: _PathLike[AnyStr]) -> AnyStr: ...
56+
@overload
57+
def expandvars(path: AnyStr) -> AnyStr: ...
58+
@overload
59+
def normcase(path: _PathLike[AnyStr]) -> AnyStr: ...
60+
@overload
61+
def normcase(path: AnyStr) -> AnyStr: ...
62+
@overload
63+
def normpath(path: _PathLike[AnyStr]) -> AnyStr: ...
64+
@overload
65+
def normpath(path: AnyStr) -> AnyStr: ...
66+
if sys.platform == 'win32':
67+
@overload
68+
def realpath(path: _PathLike[AnyStr]) -> AnyStr: ...
69+
@overload
70+
def realpath(path: AnyStr) -> AnyStr: ...
71+
else:
72+
@overload
73+
def realpath(filename: _PathLike[AnyStr]) -> AnyStr: ...
74+
@overload
75+
def realpath(filename: AnyStr) -> AnyStr: ...
76+
77+
else:
78+
def abspath(path: AnyStr) -> AnyStr: ...
79+
def basename(path: AnyStr) -> AnyStr: ...
80+
def dirname(path: AnyStr) -> AnyStr: ...
81+
def expanduser(path: AnyStr) -> AnyStr: ...
82+
def expandvars(path: AnyStr) -> AnyStr: ...
83+
def normcase(path: AnyStr) -> AnyStr: ...
84+
def normpath(path: AnyStr) -> AnyStr: ...
85+
if sys.platform == 'win32':
86+
def realpath(path: AnyStr) -> AnyStr: ...
87+
else:
88+
def realpath(filename: AnyStr) -> AnyStr: ...
3589

36-
if sys.version_info >= (3, 5):
90+
if sys.version_info >= (3, 6):
91+
# In reality it returns str for sequences of _StrPath and bytes for sequences
92+
# of _BytesPath, but mypy does not accept such a signature.
93+
def commonpath(paths: Sequence[_PathType]) -> Any: ...
94+
elif sys.version_info >= (3, 5):
3795
def commonpath(paths: Sequence[AnyStr]) -> AnyStr: ...
3896

3997
# NOTE: Empty lists results in '' (str) regardless of contained type.
4098
# Also, in Python 2 mixed sequences of Text and bytes results in either Text or bytes
4199
# So, fall back to Any
42-
def commonprefix(list: Sequence[AnyStr]) -> Any: ...
100+
def commonprefix(list: Sequence[_PathType]) -> Any: ...
43101

44-
def dirname(path: AnyStr) -> AnyStr: ...
45102
def exists(path: _PathType) -> bool: ...
46103
def lexists(path: _PathType) -> bool: ...
47-
def expanduser(path: AnyStr) -> AnyStr: ...
48-
def expandvars(path: AnyStr) -> AnyStr: ...
49104

50105
# These return float if os.stat_float_times() == True,
51106
# but int is a subclass of float.
@@ -75,29 +130,41 @@ if sys.version_info < (3, 0):
75130
def join(__p1: bytes, __p2: bytes, __p3: Text, *p: _PathType) -> Text: ...
76131
@overload
77132
def join(__p1: bytes, __p2: bytes, __p3: bytes, __p4: Text, *p: _PathType) -> Text: ...
133+
elif sys.version_info >= (3, 6):
134+
# Mypy complains that the signatures overlap (same for relpath below), but things seem to behave correctly anyway.
135+
@overload
136+
def join(path: _StrPath, *paths: _StrPath) -> Text: ... # type: ignore
137+
@overload
138+
def join(path: _BytesPath, *paths: _BytesPath) -> bytes: ...
78139
else:
79140
def join(path: AnyStr, *paths: AnyStr) -> AnyStr: ...
80141

81-
def normcase(path: AnyStr) -> AnyStr: ...
82-
def normpath(path: AnyStr) -> AnyStr: ...
83-
if sys.platform == 'win32':
84-
def realpath(path: AnyStr) -> AnyStr: ...
85-
else:
86-
def realpath(filename: AnyStr) -> AnyStr: ...
87-
def relpath(path: AnyStr, start: _PathType = ...) -> AnyStr: ...
142+
@overload
143+
def relpath(path: _StrPath, start: Optional[_StrPath] = ...) -> Text: ... # type: ignore
144+
@overload
145+
def relpath(path: _BytesPath, start: _BytesPath) -> bytes: ...
88146

89147
def samefile(path1: _PathType, path2: _PathType) -> bool: ...
90148
def sameopenfile(fp1: int, fp2: int) -> bool: ...
91-
# TODO
92-
# def samestat(stat1: stat_result,
93-
# stat2: stat_result) -> bool: ... # Unix only
149+
def samestat(stat1: stat_result, stat2: stat_result) -> bool: ...
94150

95151
if sys.version_info >= (3, 6):
96-
def split(path: Union[AnyStr, _PathLike[AnyStr]]) -> Tuple[AnyStr, AnyStr]: ...
152+
@overload
153+
def split(path: _PathLike[AnyStr]) -> Tuple[AnyStr, AnyStr]: ...
154+
@overload
155+
def split(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
156+
@overload
157+
def splitdrive(path: _PathLike[AnyStr]) -> Tuple[AnyStr, AnyStr]: ...
158+
@overload
159+
def splitdrive(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
160+
@overload
161+
def splitext(path: _PathLike[AnyStr]) -> Tuple[AnyStr, AnyStr]: ...
162+
@overload
163+
def splitext(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
97164
else:
98165
def split(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
99-
def splitdrive(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
100-
def splitext(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
166+
def splitdrive(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
167+
def splitext(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ...
101168

102169
def splitunc(path: AnyStr) -> Tuple[AnyStr, AnyStr]: ... # Windows only, deprecated
103170

0 commit comments

Comments
 (0)