]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-43510: Implement PEP 597 opt-in EncodingWarning. (GH-19481)
authorInada Naoki <songofacandy@gmail.com>
Mon, 29 Mar 2021 03:28:14 +0000 (12:28 +0900)
committerGitHub <noreply@github.com>
Mon, 29 Mar 2021 03:28:14 +0000 (12:28 +0900)
See [PEP 597](https://www.python.org/dev/peps/pep-0597/).

* Add `-X warn_default_encoding` and `PYTHONWARNDEFAULTENCODING`.
* Add EncodingWarning
* Add io.text_encoding()
* open(), TextIOWrapper() emits EncodingWarning when encoding is omitted and warn_default_encoding is enabled.
* _pyio.TextIOWrapper() uses UTF-8 as fallback default encoding used when failed to import locale module. (used during building Python)
* bz2, configparser, gzip, lzma, pathlib, tempfile modules use io.text_encoding().
* What's new entry

32 files changed:
Doc/c-api/init_config.rst
Doc/library/exceptions.rst
Doc/library/io.rst
Doc/using/cmdline.rst
Doc/whatsnew/3.10.rst
Include/cpython/initconfig.h
Include/internal/pycore_initconfig.h
Include/pyerrors.h
Lib/_pyio.py
Lib/bz2.py
Lib/configparser.py
Lib/gzip.py
Lib/io.py
Lib/lzma.py
Lib/pathlib.py
Lib/site.py
Lib/subprocess.py
Lib/tempfile.py
Lib/test/exception_hierarchy.txt
Lib/test/test_embed.py
Lib/test/test_io.py
Lib/test/test_pickle.py
Lib/test/test_sys.py
Misc/NEWS.d/next/Library/2021-03-16-17-20-33.bpo-43510.-BeQH_.rst [new file with mode: 0644]
Modules/_io/_iomodule.c
Modules/_io/clinic/_iomodule.c.h
Modules/_io/textio.c
Objects/exceptions.c
PC/python3dll.c
Python/initconfig.c
Python/preconfig.c
Python/sysmodule.c

index db7c1f43765785faf5846e995cde79eb219b099a..29fbb68195b347cf3b14bc35594bce1753eede66 100644 (file)
@@ -583,6 +583,15 @@ PyConfig
 
       Default: ``0``.
 
+   .. c:member:: int warn_default_encoding
+
+      If non-zero, emit a :exc:`EncodingWarning` warning when :class:`io.TextIOWrapper`
+      uses its default encoding. See :ref:`io-encoding-warning` for details.
+
+      Default: ``0``.
+
+      .. versionadded:: 3.10
+
    .. c:member:: wchar_t* check_hash_pycs_mode
 
       Control the validation behavior of hash-based ``.pyc`` files:
index 1028213699d63727db2a33a91e377d91ce797224..40ccde72d07cc3a1e06cb061d6213cdc4bdfa41f 100644 (file)
@@ -741,6 +741,15 @@ The following exceptions are used as warning categories; see the
    Base class for warnings related to Unicode.
 
 
+.. exception:: EncodingWarning
+
+   Base class for warnings related to encodings.
+
+   See :ref:`io-encoding-warning` for details.
+
+   .. versionadded:: 3.10
+
+
 .. exception:: BytesWarning
 
    Base class for warnings related to :class:`bytes` and :class:`bytearray`.
index 96e02e839ae6537c7811a287ebf58c9937fb46ac..f9ffc19fac489d2d33d44000a8c9d8b09c870c93 100644 (file)
@@ -106,6 +106,56 @@ stream by opening a file in binary mode with buffering disabled::
 The raw stream API is described in detail in the docs of :class:`RawIOBase`.
 
 
+.. _io-text-encoding:
+
+Text Encoding
+-------------
+
+The default encoding of :class:`TextIOWrapper` and :func:`open` is
+locale-specific (:func:`locale.getpreferredencoding(False) <locale.getpreferredencoding>`).
+
+However, many developers forget to specify the encoding when opening text files
+encoded in UTF-8 (e.g. JSON, TOML, Markdown, etc...) since most Unix
+platforms use UTF-8 locale by default. This causes bugs because the locale
+encoding is not UTF-8 for most Windows users. For example::
+
+   # May not work on Windows when non-ASCII characters in the file.
+   with open("README.md") as f:
+       long_description = f.read()
+
+Additionally, while there is no concrete plan as of yet, Python may change
+the default text file encoding to UTF-8 in the future.
+
+Accordingly, it is highly recommended that you specify the encoding
+explicitly when opening text files. If you want to use UTF-8, pass
+``encoding="utf-8"``. To use the current locale encoding,
+``encoding="locale"`` is supported in Python 3.10.
+
+When you need to run existing code on Windows that attempts to opens
+UTF-8 files using the default locale encoding, you can enable the UTF-8
+mode. See :ref:`UTF-8 mode on Windows <win-utf8-mode>`.
+
+.. _io-encoding-warning:
+
+Opt-in EncodingWarning
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 3.10
+   See :pep:`597` for more details.
+
+To find where the default locale encoding is used, you can enable
+the ``-X warn_default_encoding`` command line option or set the
+:envvar:`PYTHONWARNDEFAULTENCODING` environment variable, which will
+emit an :exc:`EncodingWarning` when the default encoding is used.
+
+If you are providing an API that uses :func:`open` or
+:class:`TextIOWrapper` and passes ``encoding=None`` as a parameter, you
+can use :func:`text_encoding` so that callers of the API will emit an
+:exc:`EncodingWarning` if they don't pass an ``encoding``. However,
+please consider using UTF-8 by default (i.e. ``encoding="utf-8"``) for
+new APIs.
+
+
 High-level Module Interface
 ---------------------------
 
@@ -143,6 +193,32 @@ High-level Module Interface
    .. versionadded:: 3.8
 
 
+.. function:: text_encoding(encoding, stacklevel=2)
+
+   This is a helper function for callables that use :func:`open` or
+   :class:`TextIOWrapper` and have an ``encoding=None`` parameter.
+
+   This function returns *encoding* if it is not ``None`` and ``"locale"`` if
+   *encoding* is ``None``.
+
+   This function emits an :class:`EncodingWarning` if
+   :data:`sys.flags.warn_default_encoding <sys.flags>` is true and *encoding*
+   is None. *stacklevel* specifies where the warning is emitted.
+   For example::
+
+      def read_text(path, encoding=None):
+          encoding = io.text_encoding(encoding)  # stacklevel=2
+          with open(path, encoding) as f:
+              return f.read()
+
+   In this example, an :class:`EncodingWarning` is emitted for the caller of
+   ``read_text()``.
+
+   See :ref:`io-text-encoding` for more information.
+
+   .. versionadded:: 3.10
+
+
 .. exception:: BlockingIOError
 
    This is a compatibility alias for the builtin :exc:`BlockingIOError`
@@ -869,6 +945,8 @@ Text I/O
    *encoding* gives the name of the encoding that the stream will be decoded or
    encoded with.  It defaults to
    :func:`locale.getpreferredencoding(False) <locale.getpreferredencoding>`.
+   ``encoding="locale"`` can be used to specify the current locale's encoding
+   explicitly. See :ref:`io-text-encoding` for more information.
 
    *errors* is an optional string that specifies how encoding and decoding
    errors are to be handled.  Pass ``'strict'`` to raise a :exc:`ValueError`
@@ -920,6 +998,9 @@ Text I/O
       locale encoding using :func:`locale.setlocale`, use the current locale
       encoding instead of the user preferred encoding.
 
+   .. versionchanged:: 3.10
+      The *encoding* argument now supports the ``"locale"`` dummy encoding name.
+
    :class:`TextIOWrapper` provides these data attributes and methods in
    addition to those from :class:`TextIOBase` and :class:`IOBase`:
 
index 04e0f3267dbe78f96ce1a6d569b97a5c24a3acc2..1493c7c9017548e0f253f4eea7b8fd23467b8578 100644 (file)
@@ -453,6 +453,9 @@ Miscellaneous options
    * ``-X pycache_prefix=PATH`` enables writing ``.pyc`` files to a parallel
      tree rooted at the given directory instead of to the code tree. See also
      :envvar:`PYTHONPYCACHEPREFIX`.
+   * ``-X warn_default_encoding`` issues a :class:`EncodingWarning` when the
+     locale-specific default encoding is used for opening files.
+     See also :envvar:`PYTHONWARNDEFAULTENCODING`.
 
    It also allows passing arbitrary values and retrieving them through the
    :data:`sys._xoptions` dictionary.
@@ -482,6 +485,9 @@ Miscellaneous options
 
       The ``-X showalloccount`` option has been removed.
 
+   .. versionadded:: 3.10
+      The ``-X warn_default_encoding`` option.
+
    .. deprecated-removed:: 3.9 3.10
       The ``-X oldparser`` option.
 
@@ -907,6 +913,15 @@ conflict.
 
    .. versionadded:: 3.7
 
+.. envvar:: PYTHONWARNDEFAULTENCODING
+
+   If this environment variable is set to a non-empty string, issue a
+   :class:`EncodingWarning` when the locale-specific default encoding is used.
+
+   See :ref:`io-encoding-warning` for details.
+
+   .. versionadded:: 3.10
+
 
 Debug-mode variables
 ~~~~~~~~~~~~~~~~~~~~
index 1c4e5c47fc681e753ab459def85fd45a1efec99b..3a563c10282c86278bfd1151c3cddca35ef8773f 100644 (file)
@@ -454,6 +454,30 @@ For the full specification see :pep:`634`.  Motivation and rationale
 are in :pep:`635`, and a longer tutorial is in :pep:`636`.
 
 
+.. _whatsnew310-pep597:
+
+Optional ``EncodingWarning`` and ``encoding="locale"`` option
+-------------------------------------------------------------
+
+The default encoding of :class:`TextIOWrapper` and :func:`open` is
+platform and locale dependent. Since UTF-8 is used on most Unix
+platforms, omitting ``encoding`` option when opening UTF-8 files
+(e.g. JSON, YAML, TOML, Markdown) is very common bug. For example::
+
+   # BUG: "rb" mode or encoding="utf-8" should be used.
+   with open("data.json") as f:
+       data = json.laod(f)
+
+To find this type of bugs, optional ``EncodingWarning`` is added.
+It is emitted when :data:`sys.flags.warn_default_encoding <sys.flags>`
+is true and locale-specific default encoding is used.
+
+``-X warn_default_encoding`` option and :envvar:`PYTHONWARNDEFAULTENCODING`
+are added to enable the warning.
+
+See :ref:`io-text-encoding` for more information.
+
+
 New Features Related to Type Annotations
 ========================================
 
index 666c1e419ca24d4e6645f5c37dde75dfb33d9592..09f9a2947efef38a01a4a1242477a2f0a26d156e 100644 (file)
@@ -153,6 +153,7 @@ typedef struct PyConfig {
     PyWideStringList warnoptions;
     int site_import;
     int bytes_warning;
+    int warn_default_encoding;
     int inspect;
     int interactive;
     int optimization_level;
index 28cd57030e218125a89aa304a9a9c85d6d9b493b..4b009e816b49279a9406e46472c4c330315d01d4 100644 (file)
@@ -102,6 +102,7 @@ typedef struct {
     int isolated;             /* -I option */
     int use_environment;      /* -E option */
     int dev_mode;             /* -X dev and PYTHONDEVMODE */
+    int warn_default_encoding;     /* -X warn_default_encoding and PYTHONWARNDEFAULTENCODING */
 } _PyPreCmdline;
 
 #define _PyPreCmdline_INIT \
index 14129d3533cbef4ca27537f60a6be032cb1d9290..f5d1c7115771869dcf69c7ff1a3410c817e2ce67 100644 (file)
@@ -146,6 +146,7 @@ PyAPI_DATA(PyObject *) PyExc_FutureWarning;
 PyAPI_DATA(PyObject *) PyExc_ImportWarning;
 PyAPI_DATA(PyObject *) PyExc_UnicodeWarning;
 PyAPI_DATA(PyObject *) PyExc_BytesWarning;
+PyAPI_DATA(PyObject *) PyExc_EncodingWarning;
 PyAPI_DATA(PyObject *) PyExc_ResourceWarning;
 
 
index 4804ed27cd14d628eef56df32cc69722d49d94ea..0f182d42402063e9dac2d8c45007300c98fdc101 100644 (file)
@@ -40,6 +40,29 @@ _IOBASE_EMITS_UNRAISABLE = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mo
 _CHECK_ERRORS = _IOBASE_EMITS_UNRAISABLE
 
 
+def text_encoding(encoding, stacklevel=2):
+    """
+    A helper function to choose the text encoding.
+
+    When encoding is not None, just return it.
+    Otherwise, return the default text encoding (i.e. "locale").
+
+    This function emits an EncodingWarning if *encoding* is None and
+    sys.flags.warn_default_encoding is true.
+
+    This can be used in APIs with an encoding=None parameter
+    that pass it to TextIOWrapper or open.
+    However, please consider using encoding="utf-8" for new APIs.
+    """
+    if encoding is None:
+        encoding = "locale"
+        if sys.flags.warn_default_encoding:
+            import warnings
+            warnings.warn("'encoding' argument not specified.",
+                          EncodingWarning, stacklevel + 1)
+    return encoding
+
+
 def open(file, mode="r", buffering=-1, encoding=None, errors=None,
          newline=None, closefd=True, opener=None):
 
@@ -248,6 +271,7 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
         result = buffer
         if binary:
             return result
+        encoding = text_encoding(encoding)
         text = TextIOWrapper(buffer, encoding, errors, newline, line_buffering)
         result = text
         text.mode = mode
@@ -2004,19 +2028,22 @@ class TextIOWrapper(TextIOBase):
     def __init__(self, buffer, encoding=None, errors=None, newline=None,
                  line_buffering=False, write_through=False):
         self._check_newline(newline)
-        if encoding is None:
+        encoding = text_encoding(encoding)
+
+        if encoding == "locale":
             try:
-                encoding = os.device_encoding(buffer.fileno())
+                encoding = os.device_encoding(buffer.fileno()) or "locale"
             except (AttributeError, UnsupportedOperation):
                 pass
-            if encoding is None:
-                try:
-                    import locale
-                except ImportError:
-                    # Importing locale may fail if Python is being built
-                    encoding = "ascii"
-                else:
-                    encoding = locale.getpreferredencoding(False)
+
+        if encoding == "locale":
+            try:
+                import locale
+            except ImportError:
+                # Importing locale may fail if Python is being built
+                encoding = "utf-8"
+            else:
+                encoding = locale.getpreferredencoding(False)
 
         if not isinstance(encoding, str):
             raise ValueError("invalid encoding: %r" % encoding)
index ce07ebeb142d926aa54b124eca8623b8c483eeb4..1da3ce65c81b7d89f5f2e19cbf49a85c11b11f08 100644 (file)
@@ -311,6 +311,7 @@ def open(filename, mode="rb", compresslevel=9,
     binary_file = BZ2File(filename, bz_mode, compresslevel=compresslevel)
 
     if "t" in mode:
+        encoding = io.text_encoding(encoding)
         return io.TextIOWrapper(binary_file, encoding, errors, newline)
     else:
         return binary_file
index 924cc56a3f150d16f246841512a0876ff83ead6a..3b4cb5e6b2407f7b10989444caf64b1bdc8914de 100644 (file)
@@ -690,6 +690,7 @@ class RawConfigParser(MutableMapping):
         """
         if isinstance(filenames, (str, bytes, os.PathLike)):
             filenames = [filenames]
+        encoding = io.text_encoding(encoding)
         read_ok = []
         for filename in filenames:
             try:
index 136915725ab4f9c8fe491d39fe7c873e1e9eeaa0..0a8993ba354711c93a4ed04aebf944d86be7c498 100644 (file)
@@ -62,6 +62,7 @@ def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_BEST,
         raise TypeError("filename must be a str or bytes object, or a file")
 
     if "t" in mode:
+        encoding = io.text_encoding(encoding)
         return io.TextIOWrapper(binary_file, encoding, errors, newline)
     else:
         return binary_file
index fbce6efc010c07c78bcf7b4458d5f7f22cd6f0ba..01f1df80ded297d0cc6f12b3c93233f34fdea4d2 100644 (file)
--- a/Lib/io.py
+++ b/Lib/io.py
@@ -54,7 +54,7 @@ import abc
 from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation,
                  open, open_code, FileIO, BytesIO, StringIO, BufferedReader,
                  BufferedWriter, BufferedRWPair, BufferedRandom,
-                 IncrementalNewlineDecoder, TextIOWrapper)
+                 IncrementalNewlineDecoder, text_encoding, TextIOWrapper)
 
 OpenWrapper = _io.open # for compatibility with _pyio
 
index 0817b872d2019f2198b17e382cdc9beb3899ac80..c8b197055cddceca5db24cd9790f9f6f94f6d499 100644 (file)
@@ -302,6 +302,7 @@ def open(filename, mode="rb", *,
                            preset=preset, filters=filters)
 
     if "t" in mode:
+        encoding = io.text_encoding(encoding)
         return io.TextIOWrapper(binary_file, encoding, errors, newline)
     else:
         return binary_file
index 531a699a40df4941b16c8886a8169f3bf246d5c1..5c9284b331a3289c3d9640867ed5b97a73c73e8a 100644 (file)
@@ -1241,6 +1241,8 @@ class Path(PurePath):
         Open the file pointed by this path and return a file object, as
         the built-in open() function does.
         """
+        if "b" not in mode:
+            encoding = io.text_encoding(encoding)
         return io.open(self, mode, buffering, encoding, errors, newline,
                        opener=self._opener)
 
@@ -1255,6 +1257,7 @@ class Path(PurePath):
         """
         Open the file in text mode, read it, and close the file.
         """
+        encoding = io.text_encoding(encoding)
         with self.open(mode='r', encoding=encoding, errors=errors) as f:
             return f.read()
 
@@ -1274,6 +1277,7 @@ class Path(PurePath):
         if not isinstance(data, str):
             raise TypeError('data must be str, not %s' %
                             data.__class__.__name__)
+        encoding = io.text_encoding(encoding)
         with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
             return f.write(data)
 
index 5f1b31e73d90ad68330472529e0f415456fd7de2..939893eb5ee93bc8a3c717d02bdf2f4f69636549 100644 (file)
@@ -170,7 +170,9 @@ def addpackage(sitedir, name, known_paths):
     fullname = os.path.join(sitedir, name)
     _trace(f"Processing .pth file: {fullname!r}")
     try:
-        f = io.TextIOWrapper(io.open_code(fullname))
+        # locale encoding is not ideal especially on Windows. But we have used
+        # it for a long time. setuptools uses the locale encoding too.
+        f = io.TextIOWrapper(io.open_code(fullname), encoding="locale")
     except OSError:
         return
     with f:
index 4b011e4ce5579402121405b1a2084e619f6ceb35..2b785496e4f5f318e5bf72d0579146c0eb2cf118 100644 (file)
@@ -693,7 +693,7 @@ def _use_posix_spawn():
 _USE_POSIX_SPAWN = _use_posix_spawn()
 
 
-class Popen(object):
+class Popen:
     """ Execute a child program in a new process.
 
     For a complete description of the arguments see the Python documentation.
@@ -844,6 +844,13 @@ class Popen(object):
 
         self.text_mode = encoding or errors or text or universal_newlines
 
+        # PEP 597: We suppress the EncodingWarning in subprocess module
+        # for now (at Python 3.10), because we focus on files for now.
+        # This will be changed to encoding = io.text_encoding(encoding)
+        # in the future.
+        if self.text_mode and encoding is None:
+            self.encoding = encoding = "locale"
+
         # How long to resume waiting on a child after the first ^C.
         # There is no right value for this.  The purpose is to be polite
         # yet remain good for interactive users trying to exit a tool.
index 4b2547c98f1c71da700c3ee7f74e931e66d34bb8..efcf7a7fb3bbc1968d56a911c752d7b0e3d7fc86 100644 (file)
@@ -543,6 +543,9 @@ def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,
     if _os.name == 'nt' and delete:
         flags |= _os.O_TEMPORARY
 
+    if "b" not in mode:
+        encoding = _io.text_encoding(encoding)
+
     (fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
     try:
         file = _io.open(fd, mode, buffering=buffering,
@@ -583,6 +586,9 @@ else:
         """
         global _O_TMPFILE_WORKS
 
+        if "b" not in mode:
+            encoding = _io.text_encoding(encoding)
+
         prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)
 
         flags = _bin_openflags
@@ -638,6 +644,7 @@ class SpooledTemporaryFile:
         if 'b' in mode:
             self._file = _io.BytesIO()
         else:
+            encoding = _io.text_encoding(encoding)
             self._file = _io.TextIOWrapper(_io.BytesIO(),
                             encoding=encoding, errors=errors,
                             newline=newline)
index 763a6c899b48eb0d458b18da0f648faff87dd7d3..6c5e82139105bf33f2db2f4e3096c687c1affab0 100644 (file)
@@ -61,4 +61,5 @@ BaseException
            +-- ImportWarning
            +-- UnicodeWarning
            +-- BytesWarning
+           +-- EncodingWarning
            +-- ResourceWarning
index 6833b2540d67d76d28f901f1c6af59aace532488..646cd0632edd8c70f65ab8b643b02ab4564fb637 100644 (file)
@@ -389,6 +389,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
 
         'site_import': 1,
         'bytes_warning': 0,
+        'warn_default_encoding': 0,
         'inspect': 0,
         'interactive': 0,
         'optimization_level': 0,
index 3768b625516f41fb13fdfd29f7045fe8979a4fa8..c731302a9f22f6afd12be3f730c130b6791d106b 100644 (file)
@@ -4249,6 +4249,29 @@ class MiscIOTest(unittest.TestCase):
         proc = assert_python_failure('-X', 'dev', '-c', code)
         self.assertEqual(proc.rc, 10, proc)
 
+    def test_check_encoding_warning(self):
+        # PEP 597: Raise warning when encoding is not specified
+        # and sys.flags.warn_default_encoding is set.
+        mod = self.io.__name__
+        filename = __file__
+        code = textwrap.dedent(f'''\
+            import sys
+            from {mod} import open, TextIOWrapper
+            import pathlib
+
+            with open({filename!r}) as f:           # line 5
+                pass
+
+            pathlib.Path({filename!r}).read_text()  # line 8
+        ''')
+        proc = assert_python_ok('-X', 'warn_default_encoding', '-c', code)
+        warnings = proc.err.splitlines()
+        self.assertEqual(len(warnings), 2)
+        self.assertTrue(
+            warnings[0].startswith(b"<string>:5: EncodingWarning: "))
+        self.assertTrue(
+            warnings[1].startswith(b"<string>:8: EncodingWarning: "))
+
 
 class CMiscIOTest(MiscIOTest):
     io = io
index 1f5cb103933e00b08a17e7cfd4648a74e3e1f751..23c7bd261e85cadf45203e1cf993efb774539847 100644 (file)
@@ -483,7 +483,8 @@ class CompatPickleTests(unittest.TestCase):
                 if exc in (BlockingIOError,
                            ResourceWarning,
                            StopAsyncIteration,
-                           RecursionError):
+                           RecursionError,
+                           EncodingWarning):
                     continue
                 if exc is not OSError and issubclass(exc, OSError):
                     self.assertEqual(reverse_mapping('builtins', name),
index fca05e6f88f301ffdf3fded849c1969359bc669d..5b004c2b52da80c4aeb7c1966eab132acae69e7d 100644 (file)
@@ -591,7 +591,8 @@ class SysModuleTest(unittest.TestCase):
                  "inspect", "interactive", "optimize",
                  "dont_write_bytecode", "no_user_site", "no_site",
                  "ignore_environment", "verbose", "bytes_warning", "quiet",
-                 "hash_randomization", "isolated", "dev_mode", "utf8_mode")
+                 "hash_randomization", "isolated", "dev_mode", "utf8_mode",
+                 "warn_default_encoding")
         for attr in attrs:
             self.assertTrue(hasattr(sys.flags, attr), attr)
             attr_type = bool if attr == "dev_mode" else int
diff --git a/Misc/NEWS.d/next/Library/2021-03-16-17-20-33.bpo-43510.-BeQH_.rst b/Misc/NEWS.d/next/Library/2021-03-16-17-20-33.bpo-43510.-BeQH_.rst
new file mode 100644 (file)
index 0000000..b79a49c
--- /dev/null
@@ -0,0 +1,3 @@
+Implement :pep:`597`: Add ``EncodingWarning`` warning, ``-X
+warn_default_encoding`` option, :envvar:`PYTHONWARNDEFAULTENCODING`
+environment variable and ``encoding="locale"`` argument value.
index 9147648b243bed13104a9bf12229a05ee0bda324..652c2ce5b0d61f329d07c9fe7fe3b6c9e55c1088 100644 (file)
@@ -10,6 +10,7 @@
 #define PY_SSIZE_T_CLEAN
 #include "Python.h"
 #include "_iomodule.h"
+#include "pycore_pystate.h"       // _PyInterpreterState_GET()
 
 #ifdef HAVE_SYS_TYPES_H
 #include <sys/types.h>
@@ -33,6 +34,7 @@ PyObject *_PyIO_str_fileno = NULL;
 PyObject *_PyIO_str_flush = NULL;
 PyObject *_PyIO_str_getstate = NULL;
 PyObject *_PyIO_str_isatty = NULL;
+PyObject *_PyIO_str_locale = NULL;
 PyObject *_PyIO_str_newlines = NULL;
 PyObject *_PyIO_str_nl = NULL;
 PyObject *_PyIO_str_peek = NULL;
@@ -504,6 +506,43 @@ _io_open_impl(PyObject *module, PyObject *file, const char *mode,
     return NULL;
 }
 
+
+/*[clinic input]
+_io.text_encoding
+    encoding: object
+    stacklevel: int = 2
+    /
+
+A helper function to choose the text encoding.
+
+When encoding is not None, just return it.
+Otherwise, return the default text encoding (i.e. "locale").
+
+This function emits an EncodingWarning if encoding is None and
+sys.flags.warn_default_encoding is true.
+
+This can be used in APIs with an encoding=None parameter.
+However, please consider using encoding="utf-8" for new APIs.
+[clinic start generated code]*/
+
+static PyObject *
+_io_text_encoding_impl(PyObject *module, PyObject *encoding, int stacklevel)
+/*[clinic end generated code: output=91b2cfea6934cc0c input=bf70231213e2a7b4]*/
+{
+    if (encoding == NULL || encoding == Py_None) {
+        PyInterpreterState *interp = _PyInterpreterState_GET();
+        if (_PyInterpreterState_GetConfig(interp)->warn_default_encoding) {
+            PyErr_WarnEx(PyExc_EncodingWarning,
+                         "'encoding' argument not specified", stacklevel);
+        }
+        Py_INCREF(_PyIO_str_locale);
+        return _PyIO_str_locale;
+    }
+    Py_INCREF(encoding);
+    return encoding;
+}
+
+
 /*[clinic input]
 _io.open_code
 
@@ -629,6 +668,7 @@ iomodule_free(PyObject *mod) {
 
 static PyMethodDef module_methods[] = {
     _IO_OPEN_METHODDEF
+    _IO_TEXT_ENCODING_METHODDEF
     _IO_OPEN_CODE_METHODDEF
     {NULL, NULL}
 };
@@ -747,6 +787,7 @@ PyInit__io(void)
     ADD_INTERNED(flush)
     ADD_INTERNED(getstate)
     ADD_INTERNED(isatty)
+    ADD_INTERNED(locale)
     ADD_INTERNED(newlines)
     ADD_INTERNED(peek)
     ADD_INTERNED(read)
index dc7b5ff243a784d2fb553ebed678b41892845c62..91c55b1816cd82571ed74a17de911ef5c92e9c8d 100644 (file)
@@ -272,6 +272,52 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(_io_text_encoding__doc__,
+"text_encoding($module, encoding, stacklevel=2, /)\n"
+"--\n"
+"\n"
+"A helper function to choose the text encoding.\n"
+"\n"
+"When encoding is not None, just return it.\n"
+"Otherwise, return the default text encoding (i.e. \"locale\").\n"
+"\n"
+"This function emits an EncodingWarning if encoding is None and\n"
+"sys.flags.warn_default_encoding is true.\n"
+"\n"
+"This can be used in APIs with an encoding=None parameter.\n"
+"However, please consider using encoding=\"utf-8\" for new APIs.");
+
+#define _IO_TEXT_ENCODING_METHODDEF    \
+    {"text_encoding", (PyCFunction)(void(*)(void))_io_text_encoding, METH_FASTCALL, _io_text_encoding__doc__},
+
+static PyObject *
+_io_text_encoding_impl(PyObject *module, PyObject *encoding, int stacklevel);
+
+static PyObject *
+_io_text_encoding(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    PyObject *encoding;
+    int stacklevel = 2;
+
+    if (!_PyArg_CheckPositional("text_encoding", nargs, 1, 2)) {
+        goto exit;
+    }
+    encoding = args[0];
+    if (nargs < 2) {
+        goto skip_optional;
+    }
+    stacklevel = _PyLong_AsInt(args[1]);
+    if (stacklevel == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+skip_optional:
+    return_value = _io_text_encoding_impl(module, encoding, stacklevel);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(_io_open_code__doc__,
 "open_code($module, /, path)\n"
 "--\n"
@@ -313,4 +359,4 @@ _io_open_code(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=5c0dd7a262c30ebc input=a9049054013a1b77]*/
+/*[clinic end generated code: output=06e055d1d80b835d input=a9049054013a1b77]*/
index 03001ecb0a5b3bb9a6a5b5cb047fcd1af298bf56..6f89a879c9c2bf233cf85d00b2331d894c43b1af 100644 (file)
@@ -1123,6 +1123,17 @@ _io_TextIOWrapper___init___impl(textio *self, PyObject *buffer,
     self->encodefunc = NULL;
     self->b2cratio = 0.0;
 
+    if (encoding == NULL) {
+        PyInterpreterState *interp = _PyInterpreterState_GET();
+        if (_PyInterpreterState_GetConfig(interp)->warn_default_encoding) {
+            PyErr_WarnEx(PyExc_EncodingWarning,
+                         "'encoding' argument not specified", 1);
+        }
+    }
+    else if (strcmp(encoding, "locale") == 0) {
+        encoding = NULL;
+    }
+
     if (encoding == NULL) {
         /* Try os.device_encoding(fileno) */
         PyObject *fileno;
index 88e2287b14354c313c214fd353b117776f22347a..dfa069e01d9607c1e2ff2aa6fd6cc2bc8eeccbe1 100644 (file)
@@ -2464,6 +2464,13 @@ SimpleExtendsException(PyExc_Warning, BytesWarning,
     "related to conversion from str or comparing to str.");
 
 
+/*
+ *    EncodingWarning extends Warning
+ */
+SimpleExtendsException(PyExc_Warning, EncodingWarning,
+    "Base class for warnings about encodings.");
+
+
 /*
  *    ResourceWarning extends Warning
  */
@@ -2592,6 +2599,7 @@ _PyExc_Init(PyInterpreterState *interp)
     PRE_INIT(BufferError);
     PRE_INIT(Warning);
     PRE_INIT(UserWarning);
+    PRE_INIT(EncodingWarning);
     PRE_INIT(DeprecationWarning);
     PRE_INIT(PendingDeprecationWarning);
     PRE_INIT(SyntaxWarning);
@@ -2731,6 +2739,7 @@ _PyBuiltins_AddExceptions(PyObject *bltinmod)
     POST_INIT(BufferError);
     POST_INIT(Warning);
     POST_INIT(UserWarning);
+    POST_INIT(EncodingWarning);
     POST_INIT(DeprecationWarning);
     POST_INIT(PendingDeprecationWarning);
     POST_INIT(SyntaxWarning);
index ddbd1b1e8e422b0e21c90355bd93ce4f8ab9d0fb..1567ac159168af773d699ecf85cb0aa979ecaa56 100644 (file)
@@ -724,6 +724,7 @@ EXPORT_DATA(PyExc_BlockingIOError)
 EXPORT_DATA(PyExc_BrokenPipeError)
 EXPORT_DATA(PyExc_BufferError)
 EXPORT_DATA(PyExc_BytesWarning)
+EXPORT_DATA(PyExc_EncodingWarning)
 EXPORT_DATA(PyExc_ChildProcessError)
 EXPORT_DATA(PyExc_ConnectionAbortedError)
 EXPORT_DATA(PyExc_ConnectionError)
index 7886d09f7a027a72367b5ca315291d9c0be5b52d..27ae48dd3c97c84d1f2e6b7c3fc8b576d5f4907f 100644 (file)
@@ -94,6 +94,7 @@ static const char usage_3[] = "\
              otherwise activate automatically)\n\
          -X pycache_prefix=PATH: enable writing .pyc files to a parallel tree rooted at the\n\
              given directory instead of to the code tree\n\
+         -X warn_default_encoding: enable opt-in EncodingWarning for 'encoding=None'\n\
 \n\
 --check-hash-based-pycs always|default|never:\n\
     control how Python invalidates hash-based .pyc files\n\
@@ -129,7 +130,8 @@ static const char usage_6[] =
 "PYTHONBREAKPOINT: if this variable is set to 0, it disables the default\n"
 "   debugger. It can be set to the callable of your debugger of choice.\n"
 "PYTHONDEVMODE: enable the development mode.\n"
-"PYTHONPYCACHEPREFIX: root directory for bytecode cache (pyc) files.\n";
+"PYTHONPYCACHEPREFIX: root directory for bytecode cache (pyc) files.\n"
+"PYTHONWARNDEFAULTENCODING: enable opt-in EncodingWarning for 'encoding=None'.\n";
 
 #if defined(MS_WINDOWS)
 #  define PYTHONHOMEHELP "<prefix>\\python{major}{minor}"
@@ -600,6 +602,7 @@ config_check_consistency(const PyConfig *config)
     assert(config->malloc_stats >= 0);
     assert(config->site_import >= 0);
     assert(config->bytes_warning >= 0);
+    assert(config->warn_default_encoding >= 0);
     assert(config->inspect >= 0);
     assert(config->interactive >= 0);
     assert(config->optimization_level >= 0);
@@ -698,6 +701,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
     config->parse_argv = 0;
     config->site_import = -1;
     config->bytes_warning = -1;
+    config->warn_default_encoding = 0;
     config->inspect = -1;
     config->interactive = -1;
     config->optimization_level = -1;
@@ -906,6 +910,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
 
     COPY_ATTR(site_import);
     COPY_ATTR(bytes_warning);
+    COPY_ATTR(warn_default_encoding);
     COPY_ATTR(inspect);
     COPY_ATTR(interactive);
     COPY_ATTR(optimization_level);
@@ -1007,6 +1012,7 @@ _PyConfig_AsDict(const PyConfig *config)
     SET_ITEM_WSTR(platlibdir);
     SET_ITEM_INT(site_import);
     SET_ITEM_INT(bytes_warning);
+    SET_ITEM_INT(warn_default_encoding);
     SET_ITEM_INT(inspect);
     SET_ITEM_INT(interactive);
     SET_ITEM_INT(optimization_level);
@@ -1271,6 +1277,7 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
     GET_WSTRLIST(warnoptions);
     GET_UINT(site_import);
     GET_UINT(bytes_warning);
+    GET_UINT(warn_default_encoding);
     GET_UINT(inspect);
     GET_UINT(interactive);
     GET_UINT(optimization_level);
index b8b0c3a0775ca857d9c68e038b6bdced6f767f5b..ae1cc3f90fca7f18eec1e582c4584b7ef33f71f0 100644 (file)
@@ -169,6 +169,7 @@ _PyPreCmdline_SetConfig(const _PyPreCmdline *cmdline, PyConfig *config)
     COPY_ATTR(isolated);
     COPY_ATTR(use_environment);
     COPY_ATTR(dev_mode);
+    COPY_ATTR(warn_default_encoding);
     return _PyStatus_OK();
 
 #undef COPY_ATTR
@@ -257,9 +258,17 @@ _PyPreCmdline_Read(_PyPreCmdline *cmdline, const PyPreConfig *preconfig)
         cmdline->dev_mode = 0;
     }
 
+    // warn_default_encoding
+    if (_Py_get_xoption(&cmdline->xoptions, L"warn_default_encoding")
+            || _Py_GetEnv(cmdline->use_environment, "PYTHONWARNDEFAULTENCODING"))
+    {
+        cmdline->warn_default_encoding = 1;
+    }
+
     assert(cmdline->use_environment >= 0);
     assert(cmdline->isolated >= 0);
     assert(cmdline->dev_mode >= 0);
+    assert(cmdline->warn_default_encoding >= 0);
 
     return _PyStatus_OK();
 }
index 686b6cae3b29470891c63901f0c99c75a1902e9b..54d70ef05697594d7972f878e1957bce336e82ce 100644 (file)
@@ -2514,6 +2514,7 @@ static PyStructSequence_Field flags_fields[] = {
     {"isolated",                "-I"},
     {"dev_mode",                "-X dev"},
     {"utf8_mode",               "-X utf8"},
+    {"warn_default_encoding",   "-X warn_default_encoding"},
     {0}
 };
 
@@ -2521,7 +2522,7 @@ static PyStructSequence_Desc flags_desc = {
     "sys.flags",        /* name */
     flags__doc__,       /* doc */
     flags_fields,       /* fields */
-    15
+    16
 };
 
 static int
@@ -2560,6 +2561,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
     SetFlag(config->isolated);
     SetFlagObj(PyBool_FromLong(config->dev_mode));
     SetFlag(preconfig->utf8_mode);
+    SetFlag(config->warn_default_encoding);
 #undef SetFlagObj
 #undef SetFlag
     return 0;