From dc708a6c38ca9657f9c89a3d9ea1b54af46d2c22 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 27 Apr 2025 17:50:03 +0100 Subject: [PATCH 1/2] pathlib ABCs: raise text encoding warnings at correct stack level Ensure that warnings about unspecified text encodings are emitted from `ReadablePath.read_text()`, `WritablePath.write_text()` and `magic_open()` with the correct stack level set. --- Lib/pathlib/_os.py | 6 +++++- Lib/pathlib/types.py | 7 +++++++ Lib/test/test_pathlib/test_read.py | 22 ++++++++++++++++++++++ Lib/test/test_pathlib/test_write.py | 22 ++++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index e3751bbcb62377..597d3175be4ad5 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -172,12 +172,16 @@ def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, Open the file pointed to by this path and return a file object, as the built-in open() function does. """ + text = 'b' not in mode + if text: + # Call io.text_encoding() here to ensure any warning is raised at an + # appropriate stack level. + encoding = io.text_encoding(encoding) try: return io.open(path, mode, buffering, encoding, errors, newline) except TypeError: pass cls = type(path) - text = 'b' not in mode mode = ''.join(sorted(c for c in mode if c not in 'bt')) if text: try: diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d1bb8701b887c8..d8f5c34a1a7513 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -12,6 +12,7 @@ from abc import ABC, abstractmethod from glob import _PathGlobber +from io import text_encoding from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj from pathlib import PurePath, Path from typing import Optional, Protocol, runtime_checkable @@ -262,6 +263,9 @@ def read_text(self, encoding=None, errors=None, newline=None): """ Open the file in text mode, read it, and close the file. """ + # Call io.text_encoding() here to ensure any warning is raised at an + # appropriate stack level. + encoding = text_encoding(encoding) with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f: return f.read() @@ -391,6 +395,9 @@ def write_text(self, data, encoding=None, errors=None, newline=None): """ Open the file in text mode, write to it, and close the file. """ + # Call io.text_encoding() here to ensure any warning is raised at an + # appropriate stack level. + encoding = text_encoding(encoding) if not isinstance(data, str): raise TypeError('data must be str, not %s' % data.__class__.__name__) diff --git a/Lib/test/test_pathlib/test_read.py b/Lib/test/test_pathlib/test_read.py index 753ae5d760aceb..9bb5535a6eb310 100644 --- a/Lib/test/test_pathlib/test_read.py +++ b/Lib/test/test_pathlib/test_read.py @@ -4,6 +4,7 @@ import collections.abc import io +import sys import unittest from .support import is_pypi @@ -35,6 +36,17 @@ def test_open_r(self): self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), 'this is file A\n') + @unittest.skipIf( + not getattr(sys.flags, 'warn_default_encoding', 0), + "Requires warn_default_encoding", + ) + def test_open_r_encoding_warning(self): + p = self.root / 'fileA' + with self.assertWarns(EncodingWarning) as wc: + with magic_open(p, 'r'): + pass + self.assertEqual(wc.filename, __file__) + def test_open_rb(self): p = self.root / 'fileA' with magic_open(p, 'rb') as f: @@ -55,6 +67,16 @@ def test_read_text(self): self.assertEqual(q.read_text(encoding='latin-1'), 'äbcdefg') self.assertEqual(q.read_text(encoding='utf-8', errors='ignore'), 'bcdefg') + @unittest.skipIf( + not getattr(sys.flags, 'warn_default_encoding', 0), + "Requires warn_default_encoding", + ) + def test_read_text_encoding_warning(self): + p = self.root / 'fileA' + with self.assertWarns(EncodingWarning) as wc: + p.read_text() + self.assertEqual(wc.filename, __file__) + def test_read_text_with_newlines(self): p = self.root / 'abc' self.ground.create_file(p, b'abcde\r\nfghlk\n\rmnopq') diff --git a/Lib/test/test_pathlib/test_write.py b/Lib/test/test_pathlib/test_write.py index d302e0a9caa889..2f3c06b433d224 100644 --- a/Lib/test/test_pathlib/test_write.py +++ b/Lib/test/test_pathlib/test_write.py @@ -4,6 +4,7 @@ import io import os +import sys import unittest from .support import is_pypi @@ -35,6 +36,17 @@ def test_open_w(self): f.write('this is file A\n') self.assertEqual(self.ground.readtext(p), 'this is file A\n') + @unittest.skipIf( + not getattr(sys.flags, 'warn_default_encoding', 0), + "Requires warn_default_encoding", + ) + def test_open_w_encoding_warning(self): + p = self.root / 'fileA' + with self.assertWarns(EncodingWarning) as wc: + with magic_open(p, 'w'): + pass + self.assertEqual(wc.filename, __file__) + def test_open_wb(self): p = self.root / 'fileA' with magic_open(p, 'wb') as f: @@ -61,6 +73,16 @@ def test_write_text(self): self.assertRaises(TypeError, p.write_text, b'somebytes') self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg') + @unittest.skipIf( + not getattr(sys.flags, 'warn_default_encoding', 0), + "Requires warn_default_encoding", + ) + def test_write_text_encoding_warning(self): + p = self.root / 'fileA' + with self.assertWarns(EncodingWarning) as wc: + p.write_text('abcdefg') + self.assertEqual(wc.filename, __file__) + def test_write_text_with_newlines(self): # Check that `\n` character change nothing p = self.root / 'fileA' From bae7f8aa329c861f9f053508dc3b3932c3de09c0 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 27 Apr 2025 19:47:00 +0100 Subject: [PATCH 2/2] More consistent imports --- Lib/pathlib/_os.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 597d3175be4ad5..039836941dd456 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -3,8 +3,8 @@ """ from errno import * +from io import TextIOWrapper, text_encoding from stat import S_ISDIR, S_ISREG, S_ISLNK, S_IMODE -import io import os import sys try: @@ -176,9 +176,9 @@ def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, if text: # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. - encoding = io.text_encoding(encoding) + encoding = text_encoding(encoding) try: - return io.open(path, mode, buffering, encoding, errors, newline) + return open(path, mode, buffering, encoding, errors, newline) except TypeError: pass cls = type(path) @@ -204,7 +204,7 @@ def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, else: stream = attr(path, buffering) if text: - stream = io.TextIOWrapper(stream, encoding, errors, newline) + stream = TextIOWrapper(stream, encoding, errors, newline) return stream raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}")