bpo-39674: Revert "bpo-37330: open() no longer accept 'U' in file mode (GH-16959)" (GH-18767)

This reverts commit e471e72977c83664f13d041c78549140c86c92de.

The mode will be removed from Python 3.10.
diff --git a/Lib/_pyio.py b/Lib/_pyio.py
index 8eaa114..4804ed2 100644
--- a/Lib/_pyio.py
+++ b/Lib/_pyio.py
@@ -71,6 +71,7 @@
     'b'       binary mode
     't'       text mode (default)
     '+'       open a disk file for updating (reading and writing)
+    'U'       universal newline mode (deprecated)
     ========= ===============================================================
 
     The default mode is 'rt' (open for reading text). For binary random
@@ -86,6 +87,10 @@
     returned as strings, the bytes having been first decoded using a
     platform-dependent encoding or using the specified encoding if given.
 
+    'U' mode is deprecated and will raise an exception in future versions
+    of Python.  It has no effect in Python 3.  Use newline to control
+    universal newlines mode.
+
     buffering is an optional integer used to set the buffering policy.
     Pass 0 to switch buffering off (only allowed in binary mode), 1 to select
     line buffering (only usable in text mode), and an integer > 1 to indicate
@@ -171,7 +176,7 @@
     if errors is not None and not isinstance(errors, str):
         raise TypeError("invalid errors: %r" % errors)
     modes = set(mode)
-    if modes - set("axrwb+t") or len(mode) > len(modes):
+    if modes - set("axrwb+tU") or len(mode) > len(modes):
         raise ValueError("invalid mode: %r" % mode)
     creating = "x" in modes
     reading = "r" in modes
@@ -180,6 +185,13 @@
     updating = "+" in modes
     text = "t" in modes
     binary = "b" in modes
+    if "U" in modes:
+        if creating or writing or appending or updating:
+            raise ValueError("mode U cannot be combined with 'x', 'w', 'a', or '+'")
+        import warnings
+        warnings.warn("'U' mode is deprecated",
+                      DeprecationWarning, 2)
+        reading = True
     if text and binary:
         raise ValueError("can't have text and binary mode at once")
     if creating + reading + writing + appending > 1:
diff --git a/Lib/fileinput.py b/Lib/fileinput.py
index 166c631..c1b0ec9 100644
--- a/Lib/fileinput.py
+++ b/Lib/fileinput.py
@@ -209,10 +209,15 @@
         self._isstdin = False
         self._backupfilename = None
         # restrict mode argument to reading modes
-        if mode not in ('r', 'rb'):
-            raise ValueError("FileInput opening mode must be 'r' or 'rb'")
+        if mode not in ('r', 'rU', 'U', 'rb'):
+            raise ValueError("FileInput opening mode must be one of "
+                             "'r', 'rU', 'U' and 'rb'")
+        if 'U' in mode:
+            import warnings
+            warnings.warn("'U' mode is deprecated",
+                          DeprecationWarning, 2)
         self._mode = mode
-        self._write_mode = mode.replace('r', 'w')
+        self._write_mode = mode.replace('r', 'w') if 'U' not in mode else 'w'
         if openhook:
             if inplace:
                 raise ValueError("FileInput cannot use an opening hook in inplace mode")
diff --git a/Lib/imp.py b/Lib/imp.py
index a6f6fc8..31f8c76 100644
--- a/Lib/imp.py
+++ b/Lib/imp.py
@@ -225,7 +225,7 @@
 
     """
     suffix, mode, type_ = details
-    if mode and (not mode.startswith('r') or '+' in mode):
+    if mode and (not mode.startswith(('r', 'U')) or '+' in mode):
         raise ValueError('invalid file open mode {!r}'.format(mode))
     elif file is None and type_ in {PY_SOURCE, PY_COMPILED}:
         msg = 'file object required for import (type code {})'.format(type_)
diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py
index dcdd574..54a3520 100644
--- a/Lib/test/test_codecs.py
+++ b/Lib/test/test_codecs.py
@@ -712,23 +712,11 @@
         self.addCleanup(support.unlink, support.TESTFN)
         with open(support.TESTFN, 'wb') as fp:
             fp.write(s)
-        with codecs.open(support.TESTFN, 'r',
-                         encoding=self.encoding) as reader:
+        with support.check_warnings(('', DeprecationWarning)):
+            reader = codecs.open(support.TESTFN, 'U', encoding=self.encoding)
+        with reader:
             self.assertEqual(reader.read(), s1)
 
-    def test_invalid_modes(self):
-        for mode in ('U', 'rU', 'r+U'):
-            with self.assertRaises(ValueError) as cm:
-                codecs.open(support.TESTFN, mode, encoding=self.encoding)
-            self.assertIn('invalid mode', str(cm.exception))
-
-        for mode in ('rt', 'wt', 'at', 'r+t'):
-            with self.assertRaises(ValueError) as cm:
-                codecs.open(support.TESTFN, mode, encoding=self.encoding)
-            self.assertIn("can't have text and binary mode at once",
-                          str(cm.exception))
-
-
 class UTF16LETest(ReadTest, unittest.TestCase):
     encoding = "utf-16-le"
     ill_formed_sequence = b"\x80\xdc"
diff --git a/Lib/test/test_fileinput.py b/Lib/test/test_fileinput.py
index 819557d..014f19e 100644
--- a/Lib/test/test_fileinput.py
+++ b/Lib/test/test_fileinput.py
@@ -226,11 +226,19 @@
         self.assertEqual(fi.fileno(), -1)
 
     def test_opening_mode(self):
-        # invalid modes
-        for mode in ('w', 'rU', 'U'):
-            with self.subTest(mode=mode):
-                with self.assertRaises(ValueError):
-                    FileInput(mode=mode)
+        try:
+            # invalid mode, should raise ValueError
+            fi = FileInput(mode="w")
+            self.fail("FileInput should reject invalid mode argument")
+        except ValueError:
+            pass
+        # try opening in universal newline mode
+        t1 = self.writeTmp(b"A\nB\r\nC\rD", mode="wb")
+        with check_warnings(('', DeprecationWarning)):
+            fi = FileInput(files=t1, mode="U")
+        with check_warnings(('', DeprecationWarning)):
+            lines = list(fi)
+        self.assertEqual(lines, ["A\n", "B\n", "C\n", "D"])
 
     def test_stdin_binary_mode(self):
         with mock.patch('sys.stdin') as m_stdin:
@@ -977,6 +985,10 @@
             self.assertEqual(lines, expected_lines)
 
         check('r', ['A\n', 'B\n', 'C\n', 'D\u20ac'])
+        with self.assertWarns(DeprecationWarning):
+            check('rU', ['A\n', 'B\n', 'C\n', 'D\u20ac'])
+        with self.assertWarns(DeprecationWarning):
+            check('U', ['A\n', 'B\n', 'C\n', 'D\u20ac'])
         with self.assertRaises(ValueError):
             check('rb', ['A\n', 'B\r\n', 'C\r', 'D\u20ac'])
 
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index c27dfd9..4a7cbe5 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -3900,6 +3900,16 @@
         self.assertEqual(f.mode, "wb")
         f.close()
 
+        with support.check_warnings(('', DeprecationWarning)):
+            f = self.open(support.TESTFN, "U")
+        self.assertEqual(f.name,            support.TESTFN)
+        self.assertEqual(f.buffer.name,     support.TESTFN)
+        self.assertEqual(f.buffer.raw.name, support.TESTFN)
+        self.assertEqual(f.mode,            "U")
+        self.assertEqual(f.buffer.mode,     "rb")
+        self.assertEqual(f.buffer.raw.mode, "rb")
+        f.close()
+
         f = self.open(support.TESTFN, "w+")
         self.assertEqual(f.mode,            "w+")
         self.assertEqual(f.buffer.mode,     "rb+") # Does it really matter?
@@ -3913,13 +3923,6 @@
         f.close()
         g.close()
 
-    def test_removed_u_mode(self):
-        # "U" mode has been removed in Python 3.9
-        for mode in ("U", "rU", "r+U"):
-            with self.assertRaises(ValueError) as cm:
-                self.open(support.TESTFN, mode)
-            self.assertIn('invalid mode', str(cm.exception))
-
     def test_open_pipe_with_append(self):
         # bpo-27805: Ignore ESPIPE from lseek() in open().
         r, w = os.pipe()