Skip to content
Open
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
3 changes: 3 additions & 0 deletions newsfragments/+with-name-validation.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
``Path.with_name`` now rejects empty names, ``'.'``, and names containing
directory or drive components, matching pathlib's replacement-name validation.
``Path.with_stem`` also rejects stems containing path components.
5 changes: 5 additions & 0 deletions path/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,14 @@ def name(self) -> Self:
def with_name(self, name: str) -> Self:
"""Return a new path with the name changed.

Raise :exc:`ValueError` if the name is empty, ``'.'``, or contains
directory or drive components.

>>> Path('/home/guido/python.tar.gz').with_name("foo.zip")
Path('/home/guido/foo.zip')
"""
if not name or name == '.' or self.module.basename(name) != name:
raise ValueError(f"Invalid name {name!r}")
return self._next_class(removesuffix(self, self.name) + name)

def splitpath(self) -> tuple[Self, str]:
Expand Down
42 changes: 42 additions & 0 deletions tests/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,48 @@ def os_choose(**choices):


class TestBasics:
@pytest.mark.parametrize(
('module', 'name'),
[
(posixpath, ''),
(posixpath, '.'),
(posixpath, '../other.txt'),
(posixpath, '/other.txt'),
(posixpath, 'other/'),
(ntpath, ''),
(ntpath, '.'),
(ntpath, r'..\other.txt'),
(ntpath, '../other.txt'),
(ntpath, r'C:\other.txt'),
(ntpath, 'C:other.txt'),
],
)
def test_with_name_rejects_invalid_name(self, module, name):
source = Path.using_module(module)('data/report.txt')
with pytest.raises(ValueError, match='Invalid name'):
source.with_name(name)

@pytest.mark.parametrize('module', [posixpath, ntpath])
def test_with_stem_rejects_path_components(self, module):
source = Path.using_module(module)('data/report.txt')
with pytest.raises(ValueError, match='Invalid name'):
source.with_stem('../other')

@pytest.mark.parametrize(
('module', 'name'),
[
(posixpath, 'other.txt'),
(posixpath, r'other\name.txt'),
(ntpath, 'other.txt'),
],
)
def test_with_name_preserves_parent_and_type(self, module, name):
source = Path.using_module(module)('data/report.txt')
result = source.with_name(name)
assert result.parent == source.parent
assert result.name == name
assert type(result) is type(source)

def test_relpath(self):
root = Path(os_choose(nt='C:\\', posix='/'))
foo = root / 'foo'
Expand Down