From e17fc75225e172507bb21eb9420400f27a067509 Mon Sep 17 00:00:00 2001 From: barneygale Date: Fri, 29 Nov 2024 00:52:39 +0000 Subject: [PATCH 1/4] GH-123599: Deprecate duplicate `pathname2url()` implementation Call `urllib.request.pathname2url()` from `pathlib.Path.as_uri()`, and deprecate the duplicate implementation in `PurePath`. --- Doc/library/pathlib.rst | 8 +++-- Doc/whatsnew/3.14.rst | 5 +++ Lib/pathlib/_local.py | 13 +++++-- Lib/test/test_pathlib/test_pathlib.py | 36 +++++++++++-------- ...-11-29-00-53-28.gh-issue-123599.vyUh2S.rst | 2 ++ 5 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-11-29-00-53-28.gh-issue-123599.vyUh2S.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index a42ac1f8bcdf71..27be18a015e091 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -886,9 +886,11 @@ conforming to :rfc:`8089`. >>> p.as_uri() 'file:///c:/Windows' - For historical reasons, this method is also available from - :class:`PurePath` objects. However, its use of :func:`os.fsencode` makes - it strictly impure. + .. deprecated-removed:: 3.14 3.16 + + Calling this method from :class:`PurePath` rather than :class:`Path` is + possible but deprecated. The method's use of :func:`os.fsencode` makes + it strictly impure. Expanding and resolving paths diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 869a47c1261293..14e96e0dc25ec6 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -652,6 +652,11 @@ Deprecated write new code. The :mod:`subprocess` module is recommended instead. (Contributed by Victor Stinner in :gh:`120743`.) +* :mod:`pathlib`: + :meth:`!pathlib.PurePath.as_uri` is deprecated and will be removed in Python + 3.16. Use :meth:`pathlib.Path.as_uri` instead. + (Contributed by Barney Gale in :gh:`123599`.) + * :mod:`symtable`: Deprecate :meth:`symtable.Class.get_methods` due to the lack of interest. (Contributed by Bénédikt Tran in :gh:`119698`.) diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index b27f456d375225..96cf32af3d340e 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -4,6 +4,7 @@ import os import posixpath import sys +import warnings from glob import _StringGlobber from itertools import chain from _collections_abc import Sequence @@ -451,7 +452,6 @@ def is_absolute(self): def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" - import warnings msg = ("pathlib.PurePath.is_reserved() is deprecated and scheduled " "for removal in Python 3.15. Use os.path.isreserved() to " "detect reserved paths on Windows.") @@ -462,6 +462,9 @@ def is_reserved(self): def as_uri(self): """Return the path as a URI.""" + msg = ("pathlib.PurePath.as_uri() is deprecated and scheduled " + "for removal in Python 3.16. Use pathlib.Path.as_uri().") + warnings.warn(msg, DeprecationWarning, stacklevel=2) if not self.is_absolute(): raise ValueError("relative path can't be expressed as a file URI") @@ -524,7 +527,6 @@ class Path(PathBase, PurePath): but cannot instantiate a WindowsPath on a POSIX system or vice versa. """ __slots__ = () - as_uri = PurePath.as_uri @classmethod def _unsupported_msg(cls, attribute): @@ -900,6 +902,13 @@ def expanduser(self): return self + def as_uri(self): + """Return the path as a URI.""" + if not self.is_absolute(): + raise ValueError("relative path can't be expressed as a file URI") + from urllib.request import pathname2url + return 'file:' + pathname2url(str(self)) + @classmethod def from_uri(cls, uri): """Return a new path from the given 'file' URI.""" diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 6a994f890da616..cc104f6a7e8078 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -290,12 +290,18 @@ def assertLess(a, b): with self.assertRaises(TypeError): P() < {} + def make_uri(self, path): + if isinstance(path, pathlib.Path): + return path.as_uri() + with self.assertWarns(DeprecationWarning): + return path.as_uri() + def test_as_uri_common(self): P = self.cls with self.assertRaises(ValueError): - P('a').as_uri() + self.make_uri(P('a')) with self.assertRaises(ValueError): - P().as_uri() + self.make_uri(P()) def test_repr_roundtrips(self): for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): @@ -369,9 +375,9 @@ def test_eq_posix(self): @needs_posix def test_as_uri_posix(self): P = self.cls - self.assertEqual(P('/').as_uri(), 'file:///') - self.assertEqual(P('/a/b.c').as_uri(), 'file:///a/b.c') - self.assertEqual(P('/a/b%#c').as_uri(), 'file:///a/b%25%23c') + self.assertEqual(self.make_uri(P('/')), 'file:///') + self.assertEqual(self.make_uri(P('/a/b.c')), 'file:///a/b.c') + self.assertEqual(self.make_uri(P('/a/b%#c')), 'file:///a/b%25%23c') @needs_posix def test_as_uri_non_ascii(self): @@ -381,7 +387,7 @@ def test_as_uri_non_ascii(self): os.fsencode('\xe9') except UnicodeEncodeError: self.skipTest("\\xe9 cannot be encoded to the filesystem encoding") - self.assertEqual(P('/a/b\xe9').as_uri(), + self.assertEqual(self.make_uri(P('/a/b\xe9')), 'file:///a/b' + quote_from_bytes(os.fsencode('\xe9'))) @needs_posix @@ -475,17 +481,17 @@ def test_eq_windows(self): def test_as_uri_windows(self): P = self.cls with self.assertRaises(ValueError): - P('/a/b').as_uri() + self.make_uri(P('/a/b')) with self.assertRaises(ValueError): - P('c:a/b').as_uri() - self.assertEqual(P('c:/').as_uri(), 'file:///c:/') - self.assertEqual(P('c:/a/b.c').as_uri(), 'file:///c:/a/b.c') - self.assertEqual(P('c:/a/b%#c').as_uri(), 'file:///c:/a/b%25%23c') - self.assertEqual(P('c:/a/b\xe9').as_uri(), 'file:///c:/a/b%C3%A9') - self.assertEqual(P('//some/share/').as_uri(), 'file://some/share/') - self.assertEqual(P('//some/share/a/b.c').as_uri(), + self.make_uri(P('c:a/b')) + self.assertEqual(self.make_uri(P('c:/')), 'file:///c:/') + self.assertEqual(self.make_uri(P('c:/a/b.c')), 'file:///c:/a/b.c') + self.assertEqual(self.make_uri(P('c:/a/b%#c')), 'file:///c:/a/b%25%23c') + self.assertEqual(self.make_uri(P('c:/a/b\xe9')), 'file:///c:/a/b%C3%A9') + self.assertEqual(self.make_uri(P('//some/share/')), 'file://some/share/') + self.assertEqual(self.make_uri(P('//some/share/a/b.c')), 'file://some/share/a/b.c') - self.assertEqual(P('//some/share/a/b%#c\xe9').as_uri(), + self.assertEqual(self.make_uri(P('//some/share/a/b%#c\xe9')), 'file://some/share/a/b%25%23c%C3%A9') @needs_windows diff --git a/Misc/NEWS.d/next/Library/2024-11-29-00-53-28.gh-issue-123599.vyUh2S.rst b/Misc/NEWS.d/next/Library/2024-11-29-00-53-28.gh-issue-123599.vyUh2S.rst new file mode 100644 index 00000000000000..68b63bc085aafb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-29-00-53-28.gh-issue-123599.vyUh2S.rst @@ -0,0 +1,2 @@ +Deprecate :meth:`!pathlib.PurePath.as_uri`; use :meth:`pathlib.Path.as_uri` +instead. From c6e1a8d12d3b691e433c2f383afbda1332240af1 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 18 Mar 2025 20:50:21 +0000 Subject: [PATCH 2/4] Push back removal to 3.19 --- Doc/library/pathlib.rst | 2 +- Doc/whatsnew/3.14.rst | 2 +- Lib/pathlib/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 6c7281c7edced0..708a16e6bc8c94 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -886,7 +886,7 @@ conforming to :rfc:`8089`. >>> p.as_uri() 'file:///c:/Windows' - .. deprecated-removed:: 3.14 3.16 + .. deprecated-removed:: 3.14 3.19 Calling this method from :class:`PurePath` rather than :class:`Path` is possible but deprecated. The method's use of :func:`os.fsencode` makes diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index eb376cc11efb9b..1f3b2bc77ff2ad 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1112,7 +1112,7 @@ Deprecated * :mod:`pathlib`: :meth:`!pathlib.PurePath.as_uri` is deprecated and will be removed in Python - 3.16. Use :meth:`pathlib.Path.as_uri` instead. + 3.19. Use :meth:`pathlib.Path.as_uri` instead. (Contributed by Barney Gale in :gh:`123599`.) * :mod:`pdb`: diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 9304a9e414aa58..b83e9d698a9d91 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -533,7 +533,7 @@ def is_reserved(self): def as_uri(self): """Return the path as a URI.""" msg = ("pathlib.PurePath.as_uri() is deprecated and scheduled " - "for removal in Python 3.16. Use pathlib.Path.as_uri().") + "for removal in Python 3.19. Use pathlib.Path.as_uri().") warnings.warn(msg, DeprecationWarning, stacklevel=2) if not self.is_absolute(): raise ValueError("relative path can't be expressed as a file URI") From 95ec78d751eab861a2676f0027a132e3764abe78 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Thu, 20 Mar 2025 00:08:10 +0000 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Lib/pathlib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index b83e9d698a9d91..1155961cbda5d7 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1272,9 +1272,9 @@ def home(cls): def as_uri(self): """Return the path as a URI.""" if not self.is_absolute(): - raise ValueError("relative path can't be expressed as a file URI") + raise ValueError("relative paths can't be expressed as file URIs") from urllib.request import pathname2url - return 'file:' + pathname2url(str(self)) + return f'file:{pathname2url(str(self))}' @classmethod def from_uri(cls, uri): From 385e6848034d1faa1f051c2abcedeb72b5871399 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 20 Mar 2025 00:08:40 +0000 Subject: [PATCH 4/4] Address review feedback --- Lib/pathlib/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 1155961cbda5d7..24a44a54e0e98b 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -11,7 +11,6 @@ import os import posixpath import sys -import warnings from errno import * from glob import _StringGlobber, _no_recurse_symlinks from itertools import chain @@ -522,19 +521,21 @@ def is_absolute(self): def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" + import warnings msg = ("pathlib.PurePath.is_reserved() is deprecated and scheduled " "for removal in Python 3.15. Use os.path.isreserved() to " "detect reserved paths on Windows.") - warnings.warn(msg, DeprecationWarning, stacklevel=2) + warnings._deprecated("pathlib.PurePath.is_reserved", msg, remove=(3, 15)) if self.parser is ntpath: return self.parser.isreserved(self) return False def as_uri(self): """Return the path as a URI.""" + import warnings msg = ("pathlib.PurePath.as_uri() is deprecated and scheduled " "for removal in Python 3.19. Use pathlib.Path.as_uri().") - warnings.warn(msg, DeprecationWarning, stacklevel=2) + warnings._deprecated("pathlib.PurePath.as_uri", msg, remove=(3, 19)) if not self.is_absolute(): raise ValueError("relative path can't be expressed as a file URI")