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

[WIP] GH-65238: Preserve trailing slash in pathlib #112363

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a1326c8
GH-65238: Preserve trailing slash in pathlib
barneygale Nov 24, 2023
df1e115
Fix absolute() on bare DOS drive
barneygale Nov 24, 2023
61be2e3
Simplify diff
barneygale Nov 24, 2023
445364a
Reduce diff
barneygale Nov 24, 2023
6395815
Speed up _make_child_relpath()
barneygale Nov 24, 2023
435be1b
Ignore empty initialiser arguments for backwards compat
barneygale Nov 25, 2023
e577a67
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Nov 25, 2023
d766d06
Simplify implementation
barneygale Dec 1, 2023
12cfb1a
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 2, 2023
2593ddb
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 3, 2023
b275078
Add tests
barneygale Dec 3, 2023
5d87cf4
Docstrings
barneygale Dec 4, 2023
af12c24
Docs
barneygale Dec 4, 2023
cae3774
Ensure has_trailing_sep is boolean
barneygale Dec 4, 2023
d4c87c6
Add tests for new methods/properties
barneygale Dec 4, 2023
1a77c8a
Fix WindowsPath('C:').absolute()
barneygale Dec 4, 2023
120beed
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 4, 2023
df79c49
Don't access `.parts` from `._glob()`
barneygale Dec 7, 2023
0f75804
Fix pickle roundtripping
barneygale Dec 7, 2023
eeb35ff
Undo changes to `parts`
barneygale Dec 7, 2023
153c4af
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 7, 2023
b852339
Simplify `_glob()` slightly
barneygale Dec 7, 2023
2b77a61
Fix possible scoping issue in `_glob()`
barneygale Dec 7, 2023
7b6766f
Formatting
barneygale Dec 8, 2023
f5c2265
Add test cases for preserving slash in absolute() and expanduser()
barneygale Dec 8, 2023
1126117
Add notes to `match()` and `glob()` docs
barneygale Dec 8, 2023
9c9b5ea
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 8, 2023
9539c60
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 11, 2023
14a9635
Merge branch 'main' into keep-trailing-slash-wip3
barneygale Dec 17, 2023
305381b
Undo pickling change
barneygale Dec 17, 2023
dfe7aae
Undo pickling changes.
barneygale Dec 18, 2023
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
74 changes: 71 additions & 3 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ we also call *flavours*:
>>> PureWindowsPath('c:/Windows', '/Program Files')
PureWindowsPath('c:/Program Files')

At most one trailing slash is kept::

>>> PurePath('foo//')
PurePosixPath('foo/')

.. versionchanged:: 3.13
A trailing slash is now retained, as it is meaningful to path
resolution.

Spurious slashes and single dots are collapsed, but double dots (``'..'``)
and leading double slashes (``'//'``) are not, since this would change the
meaning of a path for various reasons (e.g. symbolic links, UNC paths)::
Expand All @@ -153,8 +162,8 @@ we also call *flavours*:
PurePosixPath('foo/bar')
>>> PurePath('//foo/bar')
PurePosixPath('//foo/bar')
>>> PurePath('foo/./bar')
PurePosixPath('foo/bar')
>>> PurePath('foo/./bar/.')
PurePosixPath('foo/bar/')
>>> PurePath('foo/../bar')
PurePosixPath('foo/../bar')

Expand Down Expand Up @@ -184,7 +193,7 @@ we also call *flavours*:
filesystem paths, including `UNC paths`_::

>>> PureWindowsPath('c:/Program Files/')
PureWindowsPath('c:/Program Files')
PureWindowsPath('c:/Program Files/')
>>> PureWindowsPath('//server/share/file')
PureWindowsPath('//server/share/file')

Expand Down Expand Up @@ -376,6 +385,21 @@ Pure paths provide the following methods and properties:
'\\\\host\\share\\'


.. attribute:: PurePath.has_trailing_sep

Whether the path has a trailing slash after its :attr:`name`::

>>> PurePosixPath('foo/bar/').has_trailing_sep
True

If the path has no name, this property is false::

>>> PureWindowsPath('c:/').has_trailing_sep
False

.. versionadded:: 3.13


.. attribute:: PurePath.parents

An immutable sequence providing access to the logical ancestors of
Expand Down Expand Up @@ -614,6 +638,10 @@ Pure paths provide the following methods and properties:
Support for the recursive wildcard "``**``" was added. In previous
versions, it acted like the non-recursive wildcard "``*``".

.. versionchanged:: 3.13
Matching now considers whether the path and *pattern* end with path
separators.


.. method:: PurePath.relative_to(other, walk_up=False)

Expand Down Expand Up @@ -719,6 +747,41 @@ Pure paths provide the following methods and properties:
PureWindowsPath('README')


.. method:: PurePath.with_trailing_sep()

Return a new path with a trailing slash after its :attr:`name`. If the
original path doesn't have a name, :exc:`ValueError` is raised::

>>> p = PureWindowsPath('c:/windows')
>>> p.with_trailing_sep()
PureWindowsPath('c:/windows/')
>>> p = PureWindowsPath('c:/')
>>> p.with_trailing_sep()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
p.with_trailing_sep()
~~~~~~~~~~~~~~~~~~~^^
File "/home/barney/projects/cpython/Lib/pathlib.py", line 459, in with_trailing_sep
raise ValueError(f"{self!r} has an empty name")
ValueError: PureWindowsPath('c:/') has an empty name

.. versionadded:: 3.13


.. method:: PurePath.without_trailing_sep()

Return a new path without a slash after its :attr:`name`, if any::

>>> p = PureWindowsPath('c:/windows/')
>>> p.without_trailing_sep()
PureWindowsPath('c:/windows')
>>> p = PureWindowsPath('c:/')
>>> p.without_trailing_sep()
PureWindowsPath('c:/')

.. versionadded:: 3.13


.. method:: PurePath.with_segments(*pathsegments)

Create a new path object of the same type by combining the given
Expand Down Expand Up @@ -1020,6 +1083,11 @@ call fails (for example because the path doesn't exist).
future Python release, patterns with this ending will match both files
and directories. Add a trailing slash to match only directories.

.. versionchanged:: 3.13
Returns paths with trailing path separators if *pattern* also ends with
a pathname components separator (:data:`~os.sep` or :data:`~os.altsep`).


.. method:: Path.group(*, follow_symlinks=True)

Return the name of the group owning the file. :exc:`KeyError` is raised
Expand Down
15 changes: 9 additions & 6 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ def __init__(self, *args):
"argument should be a str or an os.PathLike "
"object where __fspath__ returns a str, "
f"not {type(path).__name__!r}")
paths.append(path)
if path:
paths.append(path)
# Avoid calling super().__init__, as an optimisation
self._raw_paths = paths
self._resolving = False
Expand Down Expand Up @@ -287,7 +288,7 @@ def absolute(self):
return self
if self.root:
drive = os.path.splitroot(os.getcwd())[0]
return self._from_parsed_parts(drive, self.root, self._tail)
return self._from_parsed_parts(drive, self.root, self._tail, self.has_trailing_sep)
if self.drive:
# There is a CWD on each drive-letter drive.
cwd = os.path.abspath(self.drive)
Expand All @@ -304,10 +305,10 @@ def absolute(self):
return result
drive, root, rel = os.path.splitroot(cwd)
if not rel:
return self._from_parsed_parts(drive, root, self._tail)
return self._from_parsed_parts(drive, root, self._tail, self.has_trailing_sep)
tail = rel.split(self.pathmod.sep)
tail.extend(self._tail)
return self._from_parsed_parts(drive, root, tail)
return self._from_parsed_parts(drive, root, tail, self.has_trailing_sep)

def resolve(self, strict=False):
"""
Expand Down Expand Up @@ -454,8 +455,10 @@ def expanduser(self):
homedir = os.path.expanduser(self._tail[0])
if homedir[:1] == "~":
raise RuntimeError("Could not determine home directory.")
drv, root, tail = self._parse_path(homedir)
return self._from_parsed_parts(drv, root, tail + self._tail[1:])
drv, root, tail, _ = self._parse_path(homedir)
tail.extend(self._tail[1:])
has_trailing_sep = self.has_trailing_sep and bool(tail)
return self._from_parsed_parts(drv, root, tail, has_trailing_sep)

return self

Expand Down
Loading
Loading