diff --git a/newsfragments/+with-name-validation.bugfix.rst b/newsfragments/+with-name-validation.bugfix.rst new file mode 100644 index 0000000..c7fe3fa --- /dev/null +++ b/newsfragments/+with-name-validation.bugfix.rst @@ -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. diff --git a/path/__init__.py b/path/__init__.py index 731e88e..7322d41 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -381,9 +381,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(self.removesuffix(self.name) + name) def splitpath(self) -> tuple[Self, str]: diff --git a/tests/test_path.py b/tests/test_path.py index 55d733c..26b61d6 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -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'