Python-checkins
Threads by month
- ----- 2026 -----
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2010 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2009 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2008 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2007 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2006 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2005 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2004 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2003 -----
- December
- November
- October
- September
- August
August 2021
- 1 participants
- 478 discussions
bpo-45041: Increase coverage for sqlite3.Cursor.executescript() (GH-28074)
by pablogsal Aug. 30, 2021
by pablogsal Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/e34bb409197d72711ae2c6197f9d830553…
commit: e34bb409197d72711ae2c6197f9d8305533034d4
branch: main
author: Erlend Egeberg Aasland <erlend.aasland(a)innova.no>
committer: pablogsal <Pablogsal(a)gmail.com>
date: 2021-08-30T22:14:27+01:00
summary:
bpo-45041: Increase coverage for sqlite3.Cursor.executescript() (GH-28074)
files:
M Lib/sqlite3/test/dbapi.py
diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py
index 02e42e8c3751f..89f773daf24a1 100644
--- a/Lib/sqlite3/test/dbapi.py
+++ b/Lib/sqlite3/test/dbapi.py
@@ -880,6 +880,13 @@ def test_cursor_executescript_too_large_script(self, maxsize):
with self.assertRaises(sqlite.DataError):
cur.executescript("create table a(s);".ljust(size))
+ def test_cursor_executescript_tx_control(self):
+ con = sqlite.connect(":memory:")
+ con.execute("begin")
+ self.assertTrue(con.in_transaction)
+ con.executescript("select 1")
+ self.assertFalse(con.in_transaction)
+
def test_connection_execute(self):
con = sqlite.connect(":memory:")
result = con.execute("select 5").fetchone()[0]
1
0
[3.6] bpo-43124: Fix smtplib multiple CRLF injection (GH-25987) (GH-28038)
by ned-deily Aug. 30, 2021
by ned-deily Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/29d97d17fb7adab3b0df9e178b73f70292…
commit: 29d97d17fb7adab3b0df9e178b73f70292d1cf64
branch: 3.6
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: ned-deily <nad(a)python.org>
date: 2021-08-30T15:21:57-04:00
summary:
[3.6] bpo-43124: Fix smtplib multiple CRLF injection (GH-25987) (GH-28038)
Co-authored-by: Miguel Brito <5544985+miguendes(a)users.noreply.github.com>
Co-authored-by: Łukasz Langa <lukasz(a)langa.pl>
(cherry picked from commit 0897253f426068ea6a6fbe0ada01689af9ef1019)
files:
A Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
M Lib/smtplib.py
M Lib/test/test_smtplib.py
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
index 6091c7fb7acad..e3c5b118d9a6e 100755
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -361,10 +361,15 @@ def send(self, s):
def putcmd(self, cmd, args=""):
"""Send a command to the server."""
if args == "":
- str = '%s%s' % (cmd, CRLF)
+ s = cmd
else:
- str = '%s %s%s' % (cmd, args, CRLF)
- self.send(str)
+ s = f'{cmd} {args}'
+ if '\r' in s or '\n' in s:
+ s = s.replace('\n', '\\n').replace('\r', '\\r')
+ raise ValueError(
+ f'command and arguments contain prohibited newline characters: {s}'
+ )
+ self.send(f'{s}{CRLF}')
def getreply(self):
"""Get a reply from the server.
diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
index 87047514e7aad..121bbcc9dbd91 100644
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -286,6 +286,16 @@ def testEXPNNotImplemented(self):
self.assertEqual(smtp.getreply(), expected)
smtp.quit()
+ def test_issue43124_putcmd_escapes_newline(self):
+ # see: https://bugs.python.org/issue43124
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+ self.addCleanup(smtp.close)
+ with self.assertRaises(ValueError) as exc:
+ smtp.putcmd('helo\nX-INJECTED')
+ self.assertIn("prohibited newline characters", str(exc.exception))
+ smtp.quit()
+
def testVRFY(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
expected = (252, b'Cannot VRFY user, but will accept message ' + \
@@ -355,6 +365,51 @@ def testSendNeedingDotQuote(self):
mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
self.assertEqual(self.output.getvalue(), mexpect)
+ def test_issue43124_escape_localhostname(self):
+ # see: https://bugs.python.org/issue43124
+ # connect and send mail
+ m = 'wazzuuup\nlinetwo'
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='hi\nX-INJECTED',
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+ self.addCleanup(smtp.close)
+ with self.assertRaises(ValueError) as exc:
+ smtp.sendmail("hi(a)me.com", "you(a)me.com", m)
+ self.assertIn(
+ "prohibited newline characters: ehlo hi\\nX-INJECTED",
+ str(exc.exception),
+ )
+ # XXX (see comment in testSend)
+ time.sleep(0.01)
+ smtp.quit()
+
+ debugout = smtpd.DEBUGSTREAM.getvalue()
+ self.assertNotIn("X-INJECTED", debugout)
+
+ def test_issue43124_escape_options(self):
+ # see: https://bugs.python.org/issue43124
+ # connect and send mail
+ m = 'wazzuuup\nlinetwo'
+ smtp = smtplib.SMTP(
+ HOST, self.port, local_hostname='localhost',
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+
+ self.addCleanup(smtp.close)
+ smtp.sendmail("hi(a)me.com", "you(a)me.com", m)
+ with self.assertRaises(ValueError) as exc:
+ smtp.mail("hi(a)me.com", ["X-OPTION\nX-INJECTED-1", "X-OPTION2\nX-INJECTED-2"])
+ msg = str(exc.exception)
+ self.assertIn("prohibited newline characters", msg)
+ self.assertIn("X-OPTION\\nX-INJECTED-1 X-OPTION2\\nX-INJECTED-2", msg)
+ # XXX (see comment in testSend)
+ time.sleep(0.01)
+ smtp.quit()
+
+ debugout = smtpd.DEBUGSTREAM.getvalue()
+ self.assertNotIn("X-OPTION", debugout)
+ self.assertNotIn("X-OPTION2", debugout)
+ self.assertNotIn("X-INJECTED-1", debugout)
+ self.assertNotIn("X-INJECTED-2", debugout)
+
def testSendNullSender(self):
m = 'A test message'
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
diff --git a/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
new file mode 100644
index 0000000000000..e897d6cd3641d
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
@@ -0,0 +1,2 @@
+Made the internal ``putcmd`` function in :mod:`smtplib` sanitize input for
+presence of ``\r`` and ``\n`` characters to avoid (unlikely) command injection.
1
0
[3.7] bpo-43124: Fix smtplib multiple CRLF injection (GH-25987) (GH-28037)
by ned-deily Aug. 30, 2021
by ned-deily Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/d2cc04cd3024869101e894f73307944d98…
commit: d2cc04cd3024869101e894f73307944d98d187c8
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: ned-deily <nad(a)python.org>
date: 2021-08-30T15:16:24-04:00
summary:
[3.7] bpo-43124: Fix smtplib multiple CRLF injection (GH-25987) (GH-28037)
Co-authored-by: Miguel Brito <5544985+miguendes(a)users.noreply.github.com>
Co-authored-by: Łukasz Langa <lukasz(a)langa.pl>
(cherry picked from commit 0897253f426068ea6a6fbe0ada01689af9ef1019)
files:
A Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
M Lib/smtplib.py
M Lib/test/test_smtplib.py
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
index 22d5097c697fc9..345f03d5387e39 100755
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -361,10 +361,15 @@ def send(self, s):
def putcmd(self, cmd, args=""):
"""Send a command to the server."""
if args == "":
- str = '%s%s' % (cmd, CRLF)
+ s = cmd
else:
- str = '%s %s%s' % (cmd, args, CRLF)
- self.send(str)
+ s = f'{cmd} {args}'
+ if '\r' in s or '\n' in s:
+ s = s.replace('\n', '\\n').replace('\r', '\\r')
+ raise ValueError(
+ f'command and arguments contain prohibited newline characters: {s}'
+ )
+ self.send(f'{s}{CRLF}')
def getreply(self):
"""Get a reply from the server.
diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
index b4149d3ef007e8..a8c7402e34e76c 100644
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -282,6 +282,16 @@ def testEXPNNotImplemented(self):
self.assertEqual(smtp.getreply(), expected)
smtp.quit()
+ def test_issue43124_putcmd_escapes_newline(self):
+ # see: https://bugs.python.org/issue43124
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+ self.addCleanup(smtp.close)
+ with self.assertRaises(ValueError) as exc:
+ smtp.putcmd('helo\nX-INJECTED')
+ self.assertIn("prohibited newline characters", str(exc.exception))
+ smtp.quit()
+
def testVRFY(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
expected = (252, b'Cannot VRFY user, but will accept message ' + \
@@ -351,6 +361,51 @@ def testSendNeedingDotQuote(self):
mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
self.assertEqual(self.output.getvalue(), mexpect)
+ def test_issue43124_escape_localhostname(self):
+ # see: https://bugs.python.org/issue43124
+ # connect and send mail
+ m = 'wazzuuup\nlinetwo'
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='hi\nX-INJECTED',
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+ self.addCleanup(smtp.close)
+ with self.assertRaises(ValueError) as exc:
+ smtp.sendmail("hi(a)me.com", "you(a)me.com", m)
+ self.assertIn(
+ "prohibited newline characters: ehlo hi\\nX-INJECTED",
+ str(exc.exception),
+ )
+ # XXX (see comment in testSend)
+ time.sleep(0.01)
+ smtp.quit()
+
+ debugout = smtpd.DEBUGSTREAM.getvalue()
+ self.assertNotIn("X-INJECTED", debugout)
+
+ def test_issue43124_escape_options(self):
+ # see: https://bugs.python.org/issue43124
+ # connect and send mail
+ m = 'wazzuuup\nlinetwo'
+ smtp = smtplib.SMTP(
+ HOST, self.port, local_hostname='localhost',
+ timeout=10) # support.LOOPBACK_TIMEOUT in newer Pythons
+
+ self.addCleanup(smtp.close)
+ smtp.sendmail("hi(a)me.com", "you(a)me.com", m)
+ with self.assertRaises(ValueError) as exc:
+ smtp.mail("hi(a)me.com", ["X-OPTION\nX-INJECTED-1", "X-OPTION2\nX-INJECTED-2"])
+ msg = str(exc.exception)
+ self.assertIn("prohibited newline characters", msg)
+ self.assertIn("X-OPTION\\nX-INJECTED-1 X-OPTION2\\nX-INJECTED-2", msg)
+ # XXX (see comment in testSend)
+ time.sleep(0.01)
+ smtp.quit()
+
+ debugout = smtpd.DEBUGSTREAM.getvalue()
+ self.assertNotIn("X-OPTION", debugout)
+ self.assertNotIn("X-OPTION2", debugout)
+ self.assertNotIn("X-INJECTED-1", debugout)
+ self.assertNotIn("X-INJECTED-2", debugout)
+
def testSendNullSender(self):
m = 'A test message'
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
diff --git a/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
new file mode 100644
index 00000000000000..e897d6cd3641d7
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
@@ -0,0 +1,2 @@
+Made the internal ``putcmd`` function in :mod:`smtplib` sanitize input for
+presence of ``\r`` and ``\n`` characters to avoid (unlikely) command injection.
1
0
bpo-45001: Make email date parsing more robust against malformed input (GH-27946) (GH-27976)
by ned-deily Aug. 30, 2021
by ned-deily Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/da9d6c554697414b1d275c8502e00a07c2…
commit: da9d6c554697414b1d275c8502e00a07c2ce06e6
branch: 3.6
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: ned-deily <nad(a)python.org>
date: 2021-08-30T14:57:19-04:00
summary:
bpo-45001: Make email date parsing more robust against malformed input (GH-27946) (GH-27976)
Various date parsing utilities in the email module, such as
email.utils.parsedate(), are supposed to gracefully handle invalid
input, typically by raising an appropriate exception or by returning
None.
The internal email._parseaddr._parsedate_tz() helper used by some of
these date parsing routines tries to be robust against malformed input,
but unfortunately it can still crash ungracefully when a non-empty but
whitespace-only input is passed. This manifests as an unexpected
IndexError.
In practice, this can happen when parsing an email with only a newline
inside a ‘Date:’ header, which unfortunately happens occasionally in the
real world.
Here's a minimal example:
$ python
Python 3.9.6 (default, Jun 30 2021, 10:22:16)
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import email.utils
>>> email.utils.parsedate('foo')
>>> email.utils.parsedate(' ')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/email/_parseaddr.py", line 176, in parsedate
t = parsedate_tz(data)
File "/usr/lib/python3.9/email/_parseaddr.py", line 50, in parsedate_tz
res = _parsedate_tz(data)
File "/usr/lib/python3.9/email/_parseaddr.py", line 72, in _parsedate_tz
if data[0].endswith(',') or data[0].lower() in _daynames:
IndexError: list index out of range
The fix is rather straight-forward: guard against empty lists, after
splitting on whitespace, but before accessing the first element.
(cherry picked from commit 989f6a3800f06b2bd31cfef7c3269a443ad94fac)
Co-authored-by: wouter bolsterlee <wouter(a)bolsterl.ee>
files:
A Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
M Lib/email/_parseaddr.py
M Lib/test/test_email/test_email.py
diff --git a/Lib/email/_parseaddr.py b/Lib/email/_parseaddr.py
index 41ff6f8c000d5..178329fbc6aac 100644
--- a/Lib/email/_parseaddr.py
+++ b/Lib/email/_parseaddr.py
@@ -67,6 +67,8 @@ def _parsedate_tz(data):
if not data:
return
data = data.split()
+ if not data: # This happens for whitespace-only input.
+ return None
# The FWS after the comma after the day-of-week is optional, so search and
# adjust for this.
if data[0].endswith(',') or data[0].lower() in _daynames:
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
index 68d0522799874..e4e40b612feda 100644
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -2961,6 +2961,8 @@ def test_formatdate_usegmt(self):
def test_parsedate_returns_None_for_invalid_strings(self):
self.assertIsNone(utils.parsedate(''))
self.assertIsNone(utils.parsedate_tz(''))
+ self.assertIsNone(utils.parsedate(' '))
+ self.assertIsNone(utils.parsedate_tz(' '))
self.assertIsNone(utils.parsedate('0'))
self.assertIsNone(utils.parsedate_tz('0'))
self.assertIsNone(utils.parsedate('A Complete Waste of Time'))
diff --git a/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst b/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
new file mode 100644
index 0000000000000..55cc409d0da30
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
@@ -0,0 +1,2 @@
+Made email date parsing more robust against malformed input, namely a
+whitespace-only ``Date:`` header. Patch by Wouter Bolsterlee.
1
0
bpo-45001: Make email date parsing more robust against malformed input (GH-27946) (GH-27975)
by ned-deily Aug. 30, 2021
by ned-deily Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/e9b85afd7dc004460f6d914375ab67d617…
commit: e9b85afd7dc004460f6d914375ab67d617a8a7ff
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: ned-deily <nad(a)python.org>
date: 2021-08-30T14:48:04-04:00
summary:
bpo-45001: Make email date parsing more robust against malformed input (GH-27946) (GH-27975)
Various date parsing utilities in the email module, such as
email.utils.parsedate(), are supposed to gracefully handle invalid
input, typically by raising an appropriate exception or by returning
None.
The internal email._parseaddr._parsedate_tz() helper used by some of
these date parsing routines tries to be robust against malformed input,
but unfortunately it can still crash ungracefully when a non-empty but
whitespace-only input is passed. This manifests as an unexpected
IndexError.
In practice, this can happen when parsing an email with only a newline
inside a ‘Date:’ header, which unfortunately happens occasionally in the
real world.
Here's a minimal example:
$ python
Python 3.9.6 (default, Jun 30 2021, 10:22:16)
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import email.utils
>>> email.utils.parsedate('foo')
>>> email.utils.parsedate(' ')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/email/_parseaddr.py", line 176, in parsedate
t = parsedate_tz(data)
File "/usr/lib/python3.9/email/_parseaddr.py", line 50, in parsedate_tz
res = _parsedate_tz(data)
File "/usr/lib/python3.9/email/_parseaddr.py", line 72, in _parsedate_tz
if data[0].endswith(',') or data[0].lower() in _daynames:
IndexError: list index out of range
The fix is rather straight-forward: guard against empty lists, after
splitting on whitespace, but before accessing the first element.
(cherry picked from commit 989f6a3800f06b2bd31cfef7c3269a443ad94fac)
Co-authored-by: wouter bolsterlee <wouter(a)bolsterl.ee>
files:
A Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
M Lib/email/_parseaddr.py
M Lib/test/test_email/test_email.py
diff --git a/Lib/email/_parseaddr.py b/Lib/email/_parseaddr.py
index 41ff6f8c000d57..178329fbc6aacd 100644
--- a/Lib/email/_parseaddr.py
+++ b/Lib/email/_parseaddr.py
@@ -67,6 +67,8 @@ def _parsedate_tz(data):
if not data:
return
data = data.split()
+ if not data: # This happens for whitespace-only input.
+ return None
# The FWS after the comma after the day-of-week is optional, so search and
# adjust for this.
if data[0].endswith(',') or data[0].lower() in _daynames:
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
index 9e5c6adca835d9..64bcdcc47ad0b5 100644
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -2967,6 +2967,8 @@ def test_formatdate_usegmt(self):
def test_parsedate_returns_None_for_invalid_strings(self):
self.assertIsNone(utils.parsedate(''))
self.assertIsNone(utils.parsedate_tz(''))
+ self.assertIsNone(utils.parsedate(' '))
+ self.assertIsNone(utils.parsedate_tz(' '))
self.assertIsNone(utils.parsedate('0'))
self.assertIsNone(utils.parsedate_tz('0'))
self.assertIsNone(utils.parsedate('A Complete Waste of Time'))
diff --git a/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst b/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
new file mode 100644
index 00000000000000..55cc409d0da30f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
@@ -0,0 +1,2 @@
+Made email date parsing more robust against malformed input, namely a
+whitespace-only ``Date:`` header. Patch by Wouter Bolsterlee.
1
0
bpo-16379: expose SQLite error codes and error names in `sqlite3` (GH-27786)
by pablogsal Aug. 30, 2021
by pablogsal Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/86d8b465231473f850cc5e906013ba8581…
commit: 86d8b465231473f850cc5e906013ba8581ddb503
branch: main
author: Erlend Egeberg Aasland <erlend.aasland(a)innova.no>
committer: pablogsal <Pablogsal(a)gmail.com>
date: 2021-08-30T19:32:21+01:00
summary:
bpo-16379: expose SQLite error codes and error names in `sqlite3` (GH-27786)
files:
A Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst
M Doc/includes/sqlite3/complete_statement.py
M Doc/library/sqlite3.rst
M Doc/whatsnew/3.11.rst
M Lib/sqlite3/test/dbapi.py
M Modules/_sqlite/module.c
M Modules/_sqlite/module.h
M Modules/_sqlite/util.c
diff --git a/Doc/includes/sqlite3/complete_statement.py b/Doc/includes/sqlite3/complete_statement.py
index cd38d7305bb69..a5c947969910d 100644
--- a/Doc/includes/sqlite3/complete_statement.py
+++ b/Doc/includes/sqlite3/complete_statement.py
@@ -24,7 +24,10 @@
if buffer.lstrip().upper().startswith("SELECT"):
print(cur.fetchall())
except sqlite3.Error as e:
- print("An error occurred:", e.args[0])
+ err_msg = str(e)
+ err_code = e.sqlite_errorcode
+ err_name = e.sqlite_errorname
+ print(f"{err_name} ({err_code}): {err_msg}")
buffer = ""
con.close()
diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index 6399bed7ed52c..7c60188bc70b5 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -836,6 +836,20 @@ Exceptions
The base class of the other exceptions in this module. It is a subclass
of :exc:`Exception`.
+ .. attribute:: sqlite_errorcode
+
+ The numeric error code from the
+ `SQLite API <https://sqlite.org/rescode.html>`_
+
+ .. versionadded:: 3.11
+
+ .. attribute:: sqlite_errorname
+
+ The symbolic name of the numeric error code
+ from the `SQLite API <https://sqlite.org/rescode.html>`_
+
+ .. versionadded:: 3.11
+
.. exception:: DatabaseError
Exception raised for errors that are related to the database.
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 306385c2a90aa..1b736c71c24fb 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -226,6 +226,12 @@ sqlite3
now raise :exc:`UnicodeEncodeError` instead of :exc:`sqlite3.ProgrammingError`.
(Contributed by Erlend E. Aasland in :issue:`44688`.)
+* :mod:`sqlite3` exceptions now include the SQLite error code as
+ :attr:`~sqlite3.Error.sqlite_errorcode` and the SQLite error name as
+ :attr:`~sqlite3.Error.sqlite_errorname`.
+ (Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in
+ :issue:`16379`.)
+
Removed
=======
diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py
index aadecad32adb2..02e42e8c3751f 100644
--- a/Lib/sqlite3/test/dbapi.py
+++ b/Lib/sqlite3/test/dbapi.py
@@ -28,12 +28,12 @@
import unittest
from test.support import (
+ SHORT_TIMEOUT,
bigmemtest,
check_disallow_instantiation,
threading_helper,
- SHORT_TIMEOUT,
)
-from test.support.os_helper import TESTFN, unlink
+from test.support.os_helper import TESTFN, unlink, temp_dir
# Helper for tests using TESTFN
@@ -102,6 +102,89 @@ def test_not_supported_error(self):
sqlite.DatabaseError),
"NotSupportedError is not a subclass of DatabaseError")
+ def test_module_constants(self):
+ consts = [
+ "SQLITE_ABORT",
+ "SQLITE_ALTER_TABLE",
+ "SQLITE_ANALYZE",
+ "SQLITE_ATTACH",
+ "SQLITE_AUTH",
+ "SQLITE_BUSY",
+ "SQLITE_CANTOPEN",
+ "SQLITE_CONSTRAINT",
+ "SQLITE_CORRUPT",
+ "SQLITE_CREATE_INDEX",
+ "SQLITE_CREATE_TABLE",
+ "SQLITE_CREATE_TEMP_INDEX",
+ "SQLITE_CREATE_TEMP_TABLE",
+ "SQLITE_CREATE_TEMP_TRIGGER",
+ "SQLITE_CREATE_TEMP_VIEW",
+ "SQLITE_CREATE_TRIGGER",
+ "SQLITE_CREATE_VIEW",
+ "SQLITE_CREATE_VTABLE",
+ "SQLITE_DELETE",
+ "SQLITE_DENY",
+ "SQLITE_DETACH",
+ "SQLITE_DONE",
+ "SQLITE_DROP_INDEX",
+ "SQLITE_DROP_TABLE",
+ "SQLITE_DROP_TEMP_INDEX",
+ "SQLITE_DROP_TEMP_TABLE",
+ "SQLITE_DROP_TEMP_TRIGGER",
+ "SQLITE_DROP_TEMP_VIEW",
+ "SQLITE_DROP_TRIGGER",
+ "SQLITE_DROP_VIEW",
+ "SQLITE_DROP_VTABLE",
+ "SQLITE_EMPTY",
+ "SQLITE_ERROR",
+ "SQLITE_FORMAT",
+ "SQLITE_FULL",
+ "SQLITE_FUNCTION",
+ "SQLITE_IGNORE",
+ "SQLITE_INSERT",
+ "SQLITE_INTERNAL",
+ "SQLITE_INTERRUPT",
+ "SQLITE_IOERR",
+ "SQLITE_LOCKED",
+ "SQLITE_MISMATCH",
+ "SQLITE_MISUSE",
+ "SQLITE_NOLFS",
+ "SQLITE_NOMEM",
+ "SQLITE_NOTADB",
+ "SQLITE_NOTFOUND",
+ "SQLITE_OK",
+ "SQLITE_PERM",
+ "SQLITE_PRAGMA",
+ "SQLITE_PROTOCOL",
+ "SQLITE_READ",
+ "SQLITE_READONLY",
+ "SQLITE_REINDEX",
+ "SQLITE_ROW",
+ "SQLITE_SAVEPOINT",
+ "SQLITE_SCHEMA",
+ "SQLITE_SELECT",
+ "SQLITE_TOOBIG",
+ "SQLITE_TRANSACTION",
+ "SQLITE_UPDATE",
+ ]
+ if sqlite.version_info >= (3, 7, 17):
+ consts += ["SQLITE_NOTICE", "SQLITE_WARNING"]
+ if sqlite.version_info >= (3, 8, 3):
+ consts.append("SQLITE_RECURSIVE")
+ consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"]
+ for const in consts:
+ with self.subTest(const=const):
+ self.assertTrue(hasattr(sqlite, const))
+
+ def test_error_code_on_exception(self):
+ err_msg = "unable to open database file"
+ with temp_dir() as db:
+ with self.assertRaisesRegex(sqlite.Error, err_msg) as cm:
+ sqlite.connect(db)
+ e = cm.exception
+ self.assertEqual(e.sqlite_errorcode, sqlite.SQLITE_CANTOPEN)
+ self.assertEqual(e.sqlite_errorname, "SQLITE_CANTOPEN")
+
# sqlite3_enable_shared_cache() is deprecated on macOS and calling it may raise
# OperationalError on some buildbots.
@unittest.skipIf(sys.platform == "darwin", "shared cache is deprecated on macOS")
diff --git a/Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst b/Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst
new file mode 100644
index 0000000000000..874a9cf77d8c0
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-05-08-15-14-32.bpo-16379.rN5JVe.rst
@@ -0,0 +1,2 @@
+Add SQLite error code and name to :mod:`sqlite3` exceptions.
+Patch by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland.
diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c
index 993e572c5cdb3..47b1f7a9d0720 100644
--- a/Modules/_sqlite/module.c
+++ b/Modules/_sqlite/module.c
@@ -282,12 +282,79 @@ static PyMethodDef module_methods[] = {
{NULL, NULL}
};
+/* SQLite API error codes */
+static const struct {
+ const char *name;
+ long value;
+} error_codes[] = {
+#define DECLARE_ERROR_CODE(code) {#code, code}
+ // Primary result code list
+ DECLARE_ERROR_CODE(SQLITE_ABORT),
+ DECLARE_ERROR_CODE(SQLITE_AUTH),
+ DECLARE_ERROR_CODE(SQLITE_BUSY),
+ DECLARE_ERROR_CODE(SQLITE_CANTOPEN),
+ DECLARE_ERROR_CODE(SQLITE_CONSTRAINT),
+ DECLARE_ERROR_CODE(SQLITE_CORRUPT),
+ DECLARE_ERROR_CODE(SQLITE_DONE),
+ DECLARE_ERROR_CODE(SQLITE_EMPTY),
+ DECLARE_ERROR_CODE(SQLITE_ERROR),
+ DECLARE_ERROR_CODE(SQLITE_FORMAT),
+ DECLARE_ERROR_CODE(SQLITE_FULL),
+ DECLARE_ERROR_CODE(SQLITE_INTERNAL),
+ DECLARE_ERROR_CODE(SQLITE_INTERRUPT),
+ DECLARE_ERROR_CODE(SQLITE_IOERR),
+ DECLARE_ERROR_CODE(SQLITE_LOCKED),
+ DECLARE_ERROR_CODE(SQLITE_MISMATCH),
+ DECLARE_ERROR_CODE(SQLITE_MISUSE),
+ DECLARE_ERROR_CODE(SQLITE_NOLFS),
+ DECLARE_ERROR_CODE(SQLITE_NOMEM),
+ DECLARE_ERROR_CODE(SQLITE_NOTADB),
+ DECLARE_ERROR_CODE(SQLITE_NOTFOUND),
+ DECLARE_ERROR_CODE(SQLITE_OK),
+ DECLARE_ERROR_CODE(SQLITE_PERM),
+ DECLARE_ERROR_CODE(SQLITE_PROTOCOL),
+ DECLARE_ERROR_CODE(SQLITE_READONLY),
+ DECLARE_ERROR_CODE(SQLITE_ROW),
+ DECLARE_ERROR_CODE(SQLITE_SCHEMA),
+ DECLARE_ERROR_CODE(SQLITE_TOOBIG),
+#if SQLITE_VERSION_NUMBER >= 3007017
+ DECLARE_ERROR_CODE(SQLITE_NOTICE),
+ DECLARE_ERROR_CODE(SQLITE_WARNING),
+#endif
+#undef DECLARE_ERROR_CODE
+ {NULL, 0},
+};
+
+static int
+add_error_constants(PyObject *module)
+{
+ for (int i = 0; error_codes[i].name != NULL; i++) {
+ const char *name = error_codes[i].name;
+ const long value = error_codes[i].value;
+ if (PyModule_AddIntConstant(module, name, value) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+const char *
+pysqlite_error_name(int rc)
+{
+ for (int i = 0; error_codes[i].name != NULL; i++) {
+ if (error_codes[i].value == rc) {
+ return error_codes[i].name;
+ }
+ }
+ // No error code matched.
+ return NULL;
+}
+
static int add_integer_constants(PyObject *module) {
int ret = 0;
ret += PyModule_AddIntMacro(module, PARSE_DECLTYPES);
ret += PyModule_AddIntMacro(module, PARSE_COLNAMES);
- ret += PyModule_AddIntMacro(module, SQLITE_OK);
ret += PyModule_AddIntMacro(module, SQLITE_DENY);
ret += PyModule_AddIntMacro(module, SQLITE_IGNORE);
ret += PyModule_AddIntMacro(module, SQLITE_CREATE_INDEX);
@@ -325,7 +392,6 @@ static int add_integer_constants(PyObject *module) {
#if SQLITE_VERSION_NUMBER >= 3008003
ret += PyModule_AddIntMacro(module, SQLITE_RECURSIVE);
#endif
- ret += PyModule_AddIntMacro(module, SQLITE_DONE);
return ret;
}
@@ -406,6 +472,11 @@ PyMODINIT_FUNC PyInit__sqlite3(void)
ADD_EXCEPTION(module, state, DataError, state->DatabaseError);
ADD_EXCEPTION(module, state, NotSupportedError, state->DatabaseError);
+ /* Set error constants */
+ if (add_error_constants(module) < 0) {
+ goto error;
+ }
+
/* Set integer constants */
if (add_integer_constants(module) < 0) {
goto error;
diff --git a/Modules/_sqlite/module.h b/Modules/_sqlite/module.h
index a286739579db6..c273c1f9ed9f2 100644
--- a/Modules/_sqlite/module.h
+++ b/Modules/_sqlite/module.h
@@ -81,6 +81,8 @@ pysqlite_get_state_by_type(PyTypeObject *Py_UNUSED(tp))
return &pysqlite_global_state;
}
+extern const char *pysqlite_error_name(int rc);
+
#define PARSE_DECLTYPES 1
#define PARSE_COLNAMES 2
#endif
diff --git a/Modules/_sqlite/util.c b/Modules/_sqlite/util.c
index 24cefc626b66e..cfd189dfc3360 100644
--- a/Modules/_sqlite/util.c
+++ b/Modules/_sqlite/util.c
@@ -36,27 +36,19 @@ pysqlite_step(sqlite3_stmt *statement)
return rc;
}
-/**
- * Checks the SQLite error code and sets the appropriate DB-API exception.
- * Returns the error code (0 means no error occurred).
- */
-int
-_pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
+// Returns non-NULL if a new exception should be raised
+static PyObject *
+get_exception_class(pysqlite_state *state, int errorcode)
{
- int errorcode = sqlite3_errcode(db);
-
- switch (errorcode)
- {
+ switch (errorcode) {
case SQLITE_OK:
PyErr_Clear();
- break;
+ return NULL;
case SQLITE_INTERNAL:
case SQLITE_NOTFOUND:
- PyErr_SetString(state->InternalError, sqlite3_errmsg(db));
- break;
+ return state->InternalError;
case SQLITE_NOMEM:
- (void)PyErr_NoMemory();
- break;
+ return PyErr_NoMemory();
case SQLITE_ERROR:
case SQLITE_PERM:
case SQLITE_ABORT:
@@ -70,26 +62,85 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
case SQLITE_PROTOCOL:
case SQLITE_EMPTY:
case SQLITE_SCHEMA:
- PyErr_SetString(state->OperationalError, sqlite3_errmsg(db));
- break;
+ return state->OperationalError;
case SQLITE_CORRUPT:
- PyErr_SetString(state->DatabaseError, sqlite3_errmsg(db));
- break;
+ return state->DatabaseError;
case SQLITE_TOOBIG:
- PyErr_SetString(state->DataError, sqlite3_errmsg(db));
- break;
+ return state->DataError;
case SQLITE_CONSTRAINT:
case SQLITE_MISMATCH:
- PyErr_SetString(state->IntegrityError, sqlite3_errmsg(db));
- break;
+ return state->IntegrityError;
case SQLITE_MISUSE:
- PyErr_SetString(state->ProgrammingError, sqlite3_errmsg(db));
- break;
+ return state->ProgrammingError;
default:
- PyErr_SetString(state->DatabaseError, sqlite3_errmsg(db));
- break;
+ return state->DatabaseError;
+ }
+}
+
+static void
+raise_exception(PyObject *type, int errcode, const char *errmsg)
+{
+ PyObject *exc = NULL;
+ PyObject *args[] = { PyUnicode_FromString(errmsg), };
+ if (args[0] == NULL) {
+ goto exit;
+ }
+ exc = PyObject_Vectorcall(type, args, 1, NULL);
+ Py_DECREF(args[0]);
+ if (exc == NULL) {
+ goto exit;
+ }
+
+ PyObject *code = PyLong_FromLong(errcode);
+ if (code == NULL) {
+ goto exit;
+ }
+ int rc = PyObject_SetAttrString(exc, "sqlite_errorcode", code);
+ Py_DECREF(code);
+ if (rc < 0) {
+ goto exit;
+ }
+
+ const char *error_name = pysqlite_error_name(errcode);
+ PyObject *name;
+ if (error_name) {
+ name = PyUnicode_FromString(error_name);
+ }
+ else {
+ name = PyUnicode_InternFromString("unknown");
+ }
+ if (name == NULL) {
+ goto exit;
+ }
+ rc = PyObject_SetAttrString(exc, "sqlite_errorname", name);
+ Py_DECREF(name);
+ if (rc < 0) {
+ goto exit;
+ }
+
+ PyErr_SetObject(type, exc);
+
+exit:
+ Py_XDECREF(exc);
+}
+
+/**
+ * Checks the SQLite error code and sets the appropriate DB-API exception.
+ * Returns the error code (0 means no error occurred).
+ */
+int
+_pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
+{
+ int errorcode = sqlite3_errcode(db);
+ PyObject *exc_class = get_exception_class(state, errorcode);
+ if (exc_class == NULL) {
+ // No new exception need be raised; just pass the error code
+ return errorcode;
}
+ /* Create and set the exception. */
+ const char *errmsg = sqlite3_errmsg(db);
+ raise_exception(exc_class, errorcode, errmsg);
return errorcode;
}
1
0
Aug. 30, 2021
https://github.com/python/cpython/commit/f62763d26755260c31c717fb396550e00e…
commit: f62763d26755260c31c717fb396550e00eb6b2a0
branch: main
author: Erlend Egeberg Aasland <erlend.aasland(a)innova.no>
committer: pablogsal <Pablogsal(a)gmail.com>
date: 2021-08-30T18:49:34+01:00
summary:
bpo-43398: Add test for defect connection factories (GH-27966)
files:
M Lib/sqlite3/test/factory.py
diff --git a/Lib/sqlite3/test/factory.py b/Lib/sqlite3/test/factory.py
index 7faa9ac8c1fc2e..9e7a7e28c6ed3a 100644
--- a/Lib/sqlite3/test/factory.py
+++ b/Lib/sqlite3/test/factory.py
@@ -24,9 +24,6 @@
import sqlite3 as sqlite
from collections.abc import Sequence
-class MyConnection(sqlite.Connection):
- def __init__(self, *args, **kwargs):
- sqlite.Connection.__init__(self, *args, **kwargs)
def dict_factory(cursor, row):
d = {}
@@ -40,14 +37,19 @@ def __init__(self, *args, **kwargs):
self.row_factory = dict_factory
class ConnectionFactoryTests(unittest.TestCase):
- def setUp(self):
- self.con = sqlite.connect(":memory:", factory=MyConnection)
-
- def tearDown(self):
- self.con.close()
+ def test_connection_factories(self):
+ class DefectFactory(sqlite.Connection):
+ def __init__(self, *args, **kwargs):
+ return None
+ class OkFactory(sqlite.Connection):
+ def __init__(self, *args, **kwargs):
+ sqlite.Connection.__init__(self, *args, **kwargs)
+
+ for factory in DefectFactory, OkFactory:
+ with self.subTest(factory=factory):
+ con = sqlite.connect(":memory:", factory=factory)
+ self.assertIsInstance(con, factory)
- def test_is_instance(self):
- self.assertIsInstance(self.con, MyConnection)
class CursorFactoryTests(unittest.TestCase):
def setUp(self):
1
0
bpo-43913: Fix bugs in cleaning up classes and modules in unittest. (GH-28006)
by miss-islington Aug. 30, 2021
by miss-islington Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/d65fad04fad1a73b6bb17bcb08ca6f0a24…
commit: d65fad04fad1a73b6bb17bcb08ca6f0a24376952
branch: 3.10
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: miss-islington <31488909+miss-islington(a)users.noreply.github.com>
date: 2021-08-30T10:38:34-07:00
summary:
bpo-43913: Fix bugs in cleaning up classes and modules in unittest. (GH-28006)
* Functions registered with addModuleCleanup() were not called unless
the user defines tearDownModule() in their test module.
* Functions registered with addClassCleanup() were not called if
tearDownClass is set to None.
* Buffering in TestResult did not work with functions registered
with addClassCleanup() and addModuleCleanup().
* Errors in functions registered with addClassCleanup() and
addModuleCleanup() were not handled correctly in buffered and
debug modes.
* Errors in setUpModule() and functions registered with
addModuleCleanup() were reported in wrong order.
* And several lesser bugs.
(cherry picked from commit 08d9e597c8ef5a2b26375ac954fdf224f5d82c3c)
Co-authored-by: Serhiy Storchaka <storchaka(a)gmail.com>
files:
A Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst
M Lib/unittest/suite.py
M Lib/unittest/test/test_result.py
M Lib/unittest/test/test_runner.py
diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py
index 41993f9cf69afc..6f45b6fe5f6039 100644
--- a/Lib/unittest/suite.py
+++ b/Lib/unittest/suite.py
@@ -149,6 +149,7 @@ def _handleClassSetUp(self, test, result):
if getattr(currentClass, "__unittest_skip__", False):
return
+ failed = False
try:
currentClass._classSetupFailed = False
except TypeError:
@@ -157,27 +158,32 @@ def _handleClassSetUp(self, test, result):
pass
setUpClass = getattr(currentClass, 'setUpClass', None)
+ doClassCleanups = getattr(currentClass, 'doClassCleanups', None)
if setUpClass is not None:
_call_if_exists(result, '_setupStdout')
try:
- setUpClass()
- except Exception as e:
- if isinstance(result, _DebugResult):
- raise
- currentClass._classSetupFailed = True
- className = util.strclass(currentClass)
- self._createClassOrModuleLevelException(result, e,
- 'setUpClass',
- className)
+ try:
+ setUpClass()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ failed = True
+ try:
+ currentClass._classSetupFailed = True
+ except TypeError:
+ pass
+ className = util.strclass(currentClass)
+ self._createClassOrModuleLevelException(result, e,
+ 'setUpClass',
+ className)
+ if failed and doClassCleanups is not None:
+ doClassCleanups()
+ for exc_info in currentClass.tearDown_exceptions:
+ self._createClassOrModuleLevelException(
+ result, exc_info[1], 'setUpClass', className,
+ info=exc_info)
finally:
_call_if_exists(result, '_restoreStdout')
- if currentClass._classSetupFailed is True:
- currentClass.doClassCleanups()
- if len(currentClass.tearDown_exceptions) > 0:
- for exc in currentClass.tearDown_exceptions:
- self._createClassOrModuleLevelException(
- result, exc[1], 'setUpClass', className,
- info=exc)
def _get_previous_module(self, result):
previousModule = None
@@ -205,20 +211,22 @@ def _handleModuleFixture(self, test, result):
if setUpModule is not None:
_call_if_exists(result, '_setupStdout')
try:
- setUpModule()
- except Exception as e:
try:
- case.doModuleCleanups()
- except Exception as exc:
- self._createClassOrModuleLevelException(result, exc,
+ setUpModule()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ result._moduleSetUpFailed = True
+ self._createClassOrModuleLevelException(result, e,
'setUpModule',
currentModule)
- if isinstance(result, _DebugResult):
- raise
- result._moduleSetUpFailed = True
- self._createClassOrModuleLevelException(result, e,
- 'setUpModule',
- currentModule)
+ if result._moduleSetUpFailed:
+ try:
+ case.doModuleCleanups()
+ except Exception as e:
+ self._createClassOrModuleLevelException(result, e,
+ 'setUpModule',
+ currentModule)
finally:
_call_if_exists(result, '_restoreStdout')
@@ -251,30 +259,33 @@ def _handleModuleTearDown(self, result):
except KeyError:
return
- tearDownModule = getattr(module, 'tearDownModule', None)
- if tearDownModule is not None:
- _call_if_exists(result, '_setupStdout')
+ _call_if_exists(result, '_setupStdout')
+ try:
+ tearDownModule = getattr(module, 'tearDownModule', None)
+ if tearDownModule is not None:
+ try:
+ tearDownModule()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ self._createClassOrModuleLevelException(result, e,
+ 'tearDownModule',
+ previousModule)
try:
- tearDownModule()
+ case.doModuleCleanups()
except Exception as e:
if isinstance(result, _DebugResult):
raise
self._createClassOrModuleLevelException(result, e,
'tearDownModule',
previousModule)
- finally:
- _call_if_exists(result, '_restoreStdout')
- try:
- case.doModuleCleanups()
- except Exception as e:
- self._createClassOrModuleLevelException(result, e,
- 'tearDownModule',
- previousModule)
+ finally:
+ _call_if_exists(result, '_restoreStdout')
def _tearDownPreviousClass(self, test, result):
previousClass = getattr(result, '_previousTestClass', None)
currentClass = test.__class__
- if currentClass == previousClass:
+ if currentClass == previousClass or previousClass is None:
return
if getattr(previousClass, '_classSetupFailed', False):
return
@@ -284,27 +295,34 @@ def _tearDownPreviousClass(self, test, result):
return
tearDownClass = getattr(previousClass, 'tearDownClass', None)
- if tearDownClass is not None:
- _call_if_exists(result, '_setupStdout')
- try:
- tearDownClass()
- except Exception as e:
- if isinstance(result, _DebugResult):
- raise
- className = util.strclass(previousClass)
- self._createClassOrModuleLevelException(result, e,
- 'tearDownClass',
- className)
- finally:
- _call_if_exists(result, '_restoreStdout')
- previousClass.doClassCleanups()
- if len(previousClass.tearDown_exceptions) > 0:
- for exc in previousClass.tearDown_exceptions:
- className = util.strclass(previousClass)
- self._createClassOrModuleLevelException(result, exc[1],
- 'tearDownClass',
- className,
- info=exc)
+ doClassCleanups = getattr(previousClass, 'doClassCleanups', None)
+ if tearDownClass is None and doClassCleanups is None:
+ return
+
+ _call_if_exists(result, '_setupStdout')
+ try:
+ if tearDownClass is not None:
+ try:
+ tearDownClass()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ className = util.strclass(previousClass)
+ self._createClassOrModuleLevelException(result, e,
+ 'tearDownClass',
+ className)
+ if doClassCleanups is not None:
+ doClassCleanups()
+ for exc_info in previousClass.tearDown_exceptions:
+ if isinstance(result, _DebugResult):
+ raise exc_info[1]
+ className = util.strclass(previousClass)
+ self._createClassOrModuleLevelException(result, exc_info[1],
+ 'tearDownClass',
+ className,
+ info=exc_info)
+ finally:
+ _call_if_exists(result, '_restoreStdout')
class _ErrorHolder(object):
diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py
index a4af67bd8d56de..d6efc7ef0662a4 100644
--- a/Lib/unittest/test/test_result.py
+++ b/Lib/unittest/test/test_result.py
@@ -2,10 +2,11 @@
import sys
import textwrap
-from test.support import warnings_helper
+from test.support import warnings_helper, captured_stdout, captured_stderr
import traceback
import unittest
+from unittest.util import strclass
class MockTraceback(object):
@@ -22,6 +23,16 @@ def restore_traceback():
unittest.result.traceback = traceback
+def bad_cleanup1():
+ print('do cleanup1')
+ raise TypeError('bad cleanup1')
+
+
+def bad_cleanup2():
+ print('do cleanup2')
+ raise ValueError('bad cleanup2')
+
+
class Test_TestResult(unittest.TestCase):
# Note: there are not separate tests for TestResult.wasSuccessful(),
# TestResult.errors, TestResult.failures, TestResult.testsRun or
@@ -633,36 +644,320 @@ def testBufferOutputAddErrorOrFailure(self):
self.assertEqual(result._original_stderr.getvalue(), expectedErrMessage)
self.assertMultiLineEqual(message, expectedFullMessage)
+ def testBufferSetUp(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 1)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDown(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def tearDown(self):
+ print('tear down')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 1)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferDoCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ self.addCleanup(bad_cleanup1)
+ self.addCleanup(bad_cleanup2)
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferSetUp_DoCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ self.addCleanup(bad_cleanup1)
+ self.addCleanup(bad_cleanup2)
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDown_DoCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ self.addCleanup(bad_cleanup1)
+ self.addCleanup(bad_cleanup2)
+ def tearDown(self):
+ print('tear down')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\ntear down\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
def testBufferSetupClass(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@classmethod
def setUpClass(cls):
+ print('set up class')
1/0
def test_foo(self):
pass
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\nset up class\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = f'setUpClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
def testBufferTearDownClass(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@classmethod
def tearDownClass(cls):
+ print('tear down class')
1/0
def test_foo(self):
pass
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\ntear down class\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = f'tearDownClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferDoClassCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ print('set up class')
+ cls.addClassCleanup(bad_cleanup1)
+ cls.addClassCleanup(bad_cleanup2)
+ @classmethod
+ def tearDownClass(cls):
+ print('tear down class')
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down class\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = f'tearDownClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferSetupClass_DoClassCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ print('set up class')
+ cls.addClassCleanup(bad_cleanup1)
+ cls.addClassCleanup(bad_cleanup2)
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up class\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'setUpClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\nset up class\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDownClass_DoClassCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ print('set up class')
+ cls.addClassCleanup(bad_cleanup1)
+ cls.addClassCleanup(bad_cleanup2)
+ @classmethod
+ def tearDownClass(cls):
+ print('tear down class')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down class\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'tearDownClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\ntear down class\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
def testBufferSetUpModule(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@@ -671,6 +966,7 @@ def test_foo(self):
class Module(object):
@staticmethod
def setUpModule():
+ print('set up module')
1/0
Foo.__module__ = 'Module'
@@ -678,10 +974,18 @@ def setUpModule():
self.addCleanup(sys.modules.pop, 'Module')
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\nset up module\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = 'setUpModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
def testBufferTearDownModule(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@@ -690,6 +994,7 @@ def test_foo(self):
class Module(object):
@staticmethod
def tearDownModule():
+ print('tear down module')
1/0
Foo.__module__ = 'Module'
@@ -697,7 +1002,124 @@ def tearDownModule():
self.addCleanup(sys.modules.pop, 'Module')
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\ntear down module\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 1)
+ description = 'tearDownModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferDoModuleCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ print('set up module')
+ unittest.addModuleCleanup(bad_cleanup1)
+ unittest.addModuleCleanup(bad_cleanup2)
+
+ Foo.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ self.addCleanup(sys.modules.pop, 'Module')
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = 'tearDownModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferSetUpModule_DoModuleCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ print('set up module')
+ unittest.addModuleCleanup(bad_cleanup1)
+ unittest.addModuleCleanup(bad_cleanup2)
+ 1/0
+
+ Foo.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ self.addCleanup(sys.modules.pop, 'Module')
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up module\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = 'setUpModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\nset up module\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertIn(expected_out, formatted_exc)
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDownModule_DoModuleCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ print('set up module')
+ unittest.addModuleCleanup(bad_cleanup1)
+ unittest.addModuleCleanup(bad_cleanup2)
+ @staticmethod
+ def tearDownModule():
+ print('tear down module')
+ 1/0
+
+ Foo.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ self.addCleanup(sys.modules.pop, 'Module')
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down module\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = 'tearDownModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\ntear down module\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
if __name__ == '__main__':
diff --git a/Lib/unittest/test/test_runner.py b/Lib/unittest/test/test_runner.py
index dd9a1b6d9aeddf..453e6c3d11cfaa 100644
--- a/Lib/unittest/test/test_runner.py
+++ b/Lib/unittest/test/test_runner.py
@@ -222,14 +222,42 @@ def tearDownClass(cls):
self.assertEqual(ordering,
['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
- def test_debug_executes_classCleanUp(self):
+ def test_run_class_cleanUp_without_tearDownClass(self):
ordering = []
+ blowUp = True
class TestableTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
ordering.append('setUpClass')
cls.addClassCleanup(cleanup, ordering)
+ if blowUp:
+ raise Exception()
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ @property
+ def tearDownClass(cls):
+ raise AttributeError
+
+ runTests(TestableTest)
+ self.assertEqual(ordering, ['setUpClass', 'cleanup_good'])
+
+ ordering = []
+ blowUp = False
+ runTests(TestableTest)
+ self.assertEqual(ordering,
+ ['setUpClass', 'test', 'cleanup_good'])
+
+ def test_debug_executes_classCleanUp(self):
+ ordering = []
+ blowUp = False
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ cls.addClassCleanup(cleanup, ordering, blowUp=blowUp)
def testNothing(self):
ordering.append('test')
@classmethod
@@ -241,6 +269,48 @@ def tearDownClass(cls):
self.assertEqual(ordering,
['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'CleanUpExc')
+ self.assertEqual(ordering,
+ ['setUpClass', 'test', 'tearDownClass', 'cleanup_exc'])
+
+ def test_debug_executes_classCleanUp_when_teardown_exception(self):
+ ordering = []
+ blowUp = False
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ cls.addClassCleanup(cleanup, ordering, blowUp=blowUp)
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ raise Exception('TearDownClassExc')
+
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownClassExc')
+ self.assertEqual(ordering, ['setUpClass', 'test'])
+ self.assertTrue(TestableTest._class_cleanups)
+ TestableTest._class_cleanups.clear()
+
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownClassExc')
+ self.assertEqual(ordering, ['setUpClass', 'test'])
+ self.assertTrue(TestableTest._class_cleanups)
+ TestableTest._class_cleanups.clear()
+
def test_doClassCleanups_with_errors_addClassCleanUp(self):
class TestableTest(unittest.TestCase):
def testNothing(self):
@@ -332,6 +402,7 @@ def tearDownClass(cls):
self.assertEqual(ordering,
['setUpClass', 'setUp', 'test',
'tearDownClass', 'cleanup_exc'])
+
ordering = []
class_blow_up = True
method_blow_up = False
@@ -355,6 +426,26 @@ def tearDownClass(cls):
['setUpClass', 'setUp', 'tearDownClass',
'cleanup_exc'])
+ def test_with_errors_in_tearDownClass(self):
+ ordering = []
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ cls.addClassCleanup(cleanup, ordering)
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+ raise Exception('TearDownExc')
+
+ result = runTests(TestableTest)
+ self.assertEqual(result.errors[0][1].splitlines()[-1],
+ 'Exception: TearDownExc')
+ self.assertEqual(ordering,
+ ['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
+
class TestModuleCleanUp(unittest.TestCase):
def test_add_and_do_ModuleCleanup(self):
@@ -532,13 +623,69 @@ def tearDownClass(cls):
'tearDownModule2', 'cleanup_good'])
self.assertEqual(unittest.case._module_cleanups, [])
- def test_debug_module_executes_cleanUp(self):
+ def test_run_module_cleanUp_without_teardown(self):
ordering = []
class Module(object):
@staticmethod
def setUpModule():
ordering.append('setUpModule')
unittest.addModuleCleanup(cleanup, ordering)
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+
+ TestableTest.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ runTests(TestableTest)
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass', 'cleanup_good'])
+ self.assertEqual(unittest.case._module_cleanups, [])
+
+ def test_run_module_cleanUp_when_teardown_exception(self):
+ ordering = []
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ ordering.append('setUpModule')
+ unittest.addModuleCleanup(cleanup, ordering)
+ @staticmethod
+ def tearDownModule():
+ raise Exception('CleanUpExc')
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+
+ TestableTest.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ result = runTests(TestableTest)
+ self.assertEqual(result.errors[0][1].splitlines()[-1],
+ 'Exception: CleanUpExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass', 'cleanup_good'])
+ self.assertEqual(unittest.case._module_cleanups, [])
+
+ def test_debug_module_executes_cleanUp(self):
+ ordering = []
+ blowUp = False
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ ordering.append('setUpModule')
+ unittest.addModuleCleanup(cleanup, ordering, blowUp=blowUp)
@staticmethod
def tearDownModule():
ordering.append('tearDownModule')
@@ -562,6 +709,60 @@ def tearDownClass(cls):
'tearDownModule', 'cleanup_good'])
self.assertEqual(unittest.case._module_cleanups, [])
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'CleanUpExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass', 'tearDownModule', 'cleanup_exc'])
+ self.assertEqual(unittest.case._module_cleanups, [])
+
+ def test_debug_module_cleanUp_when_teardown_exception(self):
+ ordering = []
+ blowUp = False
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ ordering.append('setUpModule')
+ unittest.addModuleCleanup(cleanup, ordering, blowUp=blowUp)
+ @staticmethod
+ def tearDownModule():
+ raise Exception('TearDownModuleExc')
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+
+ TestableTest.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownModuleExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass'])
+ self.assertTrue(unittest.case._module_cleanups)
+ unittest.case._module_cleanups.clear()
+
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownModuleExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass'])
+ self.assertTrue(unittest.case._module_cleanups)
+ unittest.case._module_cleanups.clear()
+
def test_addClassCleanup_arg_errors(self):
cleanups = []
def cleanup(*args, **kwargs):
@@ -717,9 +918,9 @@ def tearDownClass(cls):
method_blow_up = False
result = runTests(TestableTest)
self.assertEqual(result.errors[0][1].splitlines()[-1],
- 'Exception: CleanUpExc')
- self.assertEqual(result.errors[1][1].splitlines()[-1],
'Exception: ModuleExc')
+ self.assertEqual(result.errors[1][1].splitlines()[-1],
+ 'Exception: CleanUpExc')
self.assertEqual(ordering, ['setUpModule', 'cleanup_exc'])
ordering = []
diff --git a/Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst b/Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst
new file mode 100644
index 00000000000000..cf3d5ee0e456f1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst
@@ -0,0 +1,8 @@
+Fix bugs in cleaning up classes and modules in :mod:`unittest`:
+
+* Functions registered with :func:`~unittest.addModuleCleanup` were not called unless the user defines ``tearDownModule()`` in their test module.
+* Functions registered with :meth:`~unittest.TestCase.addClassCleanup` were not called if ``tearDownClass`` is set to ``None``.
+* Buffering in :class:`~unittest.TestResult` did not work with functions registered with ``addClassCleanup()`` and ``addModuleCleanup()``.
+* Errors in functions registered with ``addClassCleanup()`` and ``addModuleCleanup()`` were not handled correctly in buffered and debug modes.
+* Errors in ``setUpModule()`` and functions registered with ``addModuleCleanup()`` were reported in wrong order.
+* And several lesser bugs.
1
0
[3.9] bpo-43913: Fix bugs in cleaning up classes and modules in unittest. (GH-28006) (GH-28071)
by ambv Aug. 30, 2021
by ambv Aug. 30, 2021
Aug. 30, 2021
https://github.com/python/cpython/commit/9827710a400848c9430ed364ed5d2d54f0…
commit: 9827710a400848c9430ed364ed5d2d54f0926701
branch: 3.9
author: Serhiy Storchaka <storchaka(a)gmail.com>
committer: ambv <lukasz(a)langa.pl>
date: 2021-08-30T19:22:21+02:00
summary:
[3.9] bpo-43913: Fix bugs in cleaning up classes and modules in unittest. (GH-28006) (GH-28071)
* Functions registered with addModuleCleanup() were not called unless
the user defines tearDownModule() in their test module.
* Functions registered with addClassCleanup() were not called if
tearDownClass is set to None.
* Buffering in TestResult did not work with functions registered
with addClassCleanup() and addModuleCleanup().
* Errors in functions registered with addClassCleanup() and
addModuleCleanup() were not handled correctly in buffered and
debug modes.
* Errors in setUpModule() and functions registered with
addModuleCleanup() were reported in wrong order.
* And several lesser bugs..
(cherry picked from commit 08d9e597c8ef5a2b26375ac954fdf224f5d82c3c)
Co-authored-by: Serhiy Storchaka <storchaka(a)gmail.com>
files:
A Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst
M Lib/unittest/suite.py
M Lib/unittest/test/test_result.py
M Lib/unittest/test/test_runner.py
diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py
index 41993f9cf69afc..6f45b6fe5f6039 100644
--- a/Lib/unittest/suite.py
+++ b/Lib/unittest/suite.py
@@ -149,6 +149,7 @@ def _handleClassSetUp(self, test, result):
if getattr(currentClass, "__unittest_skip__", False):
return
+ failed = False
try:
currentClass._classSetupFailed = False
except TypeError:
@@ -157,27 +158,32 @@ def _handleClassSetUp(self, test, result):
pass
setUpClass = getattr(currentClass, 'setUpClass', None)
+ doClassCleanups = getattr(currentClass, 'doClassCleanups', None)
if setUpClass is not None:
_call_if_exists(result, '_setupStdout')
try:
- setUpClass()
- except Exception as e:
- if isinstance(result, _DebugResult):
- raise
- currentClass._classSetupFailed = True
- className = util.strclass(currentClass)
- self._createClassOrModuleLevelException(result, e,
- 'setUpClass',
- className)
+ try:
+ setUpClass()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ failed = True
+ try:
+ currentClass._classSetupFailed = True
+ except TypeError:
+ pass
+ className = util.strclass(currentClass)
+ self._createClassOrModuleLevelException(result, e,
+ 'setUpClass',
+ className)
+ if failed and doClassCleanups is not None:
+ doClassCleanups()
+ for exc_info in currentClass.tearDown_exceptions:
+ self._createClassOrModuleLevelException(
+ result, exc_info[1], 'setUpClass', className,
+ info=exc_info)
finally:
_call_if_exists(result, '_restoreStdout')
- if currentClass._classSetupFailed is True:
- currentClass.doClassCleanups()
- if len(currentClass.tearDown_exceptions) > 0:
- for exc in currentClass.tearDown_exceptions:
- self._createClassOrModuleLevelException(
- result, exc[1], 'setUpClass', className,
- info=exc)
def _get_previous_module(self, result):
previousModule = None
@@ -205,20 +211,22 @@ def _handleModuleFixture(self, test, result):
if setUpModule is not None:
_call_if_exists(result, '_setupStdout')
try:
- setUpModule()
- except Exception as e:
try:
- case.doModuleCleanups()
- except Exception as exc:
- self._createClassOrModuleLevelException(result, exc,
+ setUpModule()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ result._moduleSetUpFailed = True
+ self._createClassOrModuleLevelException(result, e,
'setUpModule',
currentModule)
- if isinstance(result, _DebugResult):
- raise
- result._moduleSetUpFailed = True
- self._createClassOrModuleLevelException(result, e,
- 'setUpModule',
- currentModule)
+ if result._moduleSetUpFailed:
+ try:
+ case.doModuleCleanups()
+ except Exception as e:
+ self._createClassOrModuleLevelException(result, e,
+ 'setUpModule',
+ currentModule)
finally:
_call_if_exists(result, '_restoreStdout')
@@ -251,30 +259,33 @@ def _handleModuleTearDown(self, result):
except KeyError:
return
- tearDownModule = getattr(module, 'tearDownModule', None)
- if tearDownModule is not None:
- _call_if_exists(result, '_setupStdout')
+ _call_if_exists(result, '_setupStdout')
+ try:
+ tearDownModule = getattr(module, 'tearDownModule', None)
+ if tearDownModule is not None:
+ try:
+ tearDownModule()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ self._createClassOrModuleLevelException(result, e,
+ 'tearDownModule',
+ previousModule)
try:
- tearDownModule()
+ case.doModuleCleanups()
except Exception as e:
if isinstance(result, _DebugResult):
raise
self._createClassOrModuleLevelException(result, e,
'tearDownModule',
previousModule)
- finally:
- _call_if_exists(result, '_restoreStdout')
- try:
- case.doModuleCleanups()
- except Exception as e:
- self._createClassOrModuleLevelException(result, e,
- 'tearDownModule',
- previousModule)
+ finally:
+ _call_if_exists(result, '_restoreStdout')
def _tearDownPreviousClass(self, test, result):
previousClass = getattr(result, '_previousTestClass', None)
currentClass = test.__class__
- if currentClass == previousClass:
+ if currentClass == previousClass or previousClass is None:
return
if getattr(previousClass, '_classSetupFailed', False):
return
@@ -284,27 +295,34 @@ def _tearDownPreviousClass(self, test, result):
return
tearDownClass = getattr(previousClass, 'tearDownClass', None)
- if tearDownClass is not None:
- _call_if_exists(result, '_setupStdout')
- try:
- tearDownClass()
- except Exception as e:
- if isinstance(result, _DebugResult):
- raise
- className = util.strclass(previousClass)
- self._createClassOrModuleLevelException(result, e,
- 'tearDownClass',
- className)
- finally:
- _call_if_exists(result, '_restoreStdout')
- previousClass.doClassCleanups()
- if len(previousClass.tearDown_exceptions) > 0:
- for exc in previousClass.tearDown_exceptions:
- className = util.strclass(previousClass)
- self._createClassOrModuleLevelException(result, exc[1],
- 'tearDownClass',
- className,
- info=exc)
+ doClassCleanups = getattr(previousClass, 'doClassCleanups', None)
+ if tearDownClass is None and doClassCleanups is None:
+ return
+
+ _call_if_exists(result, '_setupStdout')
+ try:
+ if tearDownClass is not None:
+ try:
+ tearDownClass()
+ except Exception as e:
+ if isinstance(result, _DebugResult):
+ raise
+ className = util.strclass(previousClass)
+ self._createClassOrModuleLevelException(result, e,
+ 'tearDownClass',
+ className)
+ if doClassCleanups is not None:
+ doClassCleanups()
+ for exc_info in previousClass.tearDown_exceptions:
+ if isinstance(result, _DebugResult):
+ raise exc_info[1]
+ className = util.strclass(previousClass)
+ self._createClassOrModuleLevelException(result, exc_info[1],
+ 'tearDownClass',
+ className,
+ info=exc_info)
+ finally:
+ _call_if_exists(result, '_restoreStdout')
class _ErrorHolder(object):
diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py
index 0ffb87b40256cf..0f7f3b7e371935 100644
--- a/Lib/unittest/test/test_result.py
+++ b/Lib/unittest/test/test_result.py
@@ -3,9 +3,11 @@
import textwrap
from test import support
+from test.support import captured_stdout
import traceback
import unittest
+from unittest.util import strclass
class MockTraceback(object):
@@ -22,6 +24,16 @@ def restore_traceback():
unittest.result.traceback = traceback
+def bad_cleanup1():
+ print('do cleanup1')
+ raise TypeError('bad cleanup1')
+
+
+def bad_cleanup2():
+ print('do cleanup2')
+ raise ValueError('bad cleanup2')
+
+
class Test_TestResult(unittest.TestCase):
# Note: there are not separate tests for TestResult.wasSuccessful(),
# TestResult.errors, TestResult.failures, TestResult.testsRun or
@@ -633,36 +645,320 @@ def testBufferOutputAddErrorOrFailure(self):
self.assertEqual(result._original_stderr.getvalue(), expectedErrMessage)
self.assertMultiLineEqual(message, expectedFullMessage)
+ def testBufferSetUp(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 1)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDown(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def tearDown(self):
+ print('tear down')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 1)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferDoCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ self.addCleanup(bad_cleanup1)
+ self.addCleanup(bad_cleanup2)
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferSetUp_DoCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ self.addCleanup(bad_cleanup1)
+ self.addCleanup(bad_cleanup2)
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDown_DoCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def setUp(self):
+ print('set up')
+ self.addCleanup(bad_cleanup1)
+ self.addCleanup(bad_cleanup2)
+ def tearDown(self):
+ print('tear down')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up\ntear down\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'test_foo ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(str(test_case), description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
def testBufferSetupClass(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@classmethod
def setUpClass(cls):
+ print('set up class')
1/0
def test_foo(self):
pass
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\nset up class\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = f'setUpClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
def testBufferTearDownClass(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@classmethod
def tearDownClass(cls):
+ print('tear down class')
1/0
def test_foo(self):
pass
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\ntear down class\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = f'tearDownClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferDoClassCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ print('set up class')
+ cls.addClassCleanup(bad_cleanup1)
+ cls.addClassCleanup(bad_cleanup2)
+ @classmethod
+ def tearDownClass(cls):
+ print('tear down class')
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down class\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = f'tearDownClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferSetupClass_DoClassCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ print('set up class')
+ cls.addClassCleanup(bad_cleanup1)
+ cls.addClassCleanup(bad_cleanup2)
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up class\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'setUpClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\nset up class\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDownClass_DoClassCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ print('set up class')
+ cls.addClassCleanup(bad_cleanup1)
+ cls.addClassCleanup(bad_cleanup2)
+ @classmethod
+ def tearDownClass(cls):
+ print('tear down class')
+ 1/0
+ def test_foo(self):
+ pass
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down class\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 3)
+ description = f'tearDownClass ({strclass(Foo)})'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\ntear down class\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+ test_case, formatted_exc = result.errors[2]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('TypeError: bad cleanup1', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
def testBufferSetUpModule(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@@ -671,6 +967,7 @@ def test_foo(self):
class Module(object):
@staticmethod
def setUpModule():
+ print('set up module')
1/0
Foo.__module__ = 'Module'
@@ -678,10 +975,18 @@ def setUpModule():
self.addCleanup(sys.modules.pop, 'Module')
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\nset up module\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = 'setUpModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
def testBufferTearDownModule(self):
- result = unittest.TestResult()
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
result.buffer = True
class Foo(unittest.TestCase):
@@ -690,6 +995,7 @@ def test_foo(self):
class Module(object):
@staticmethod
def tearDownModule():
+ print('tear down module')
1/0
Foo.__module__ = 'Module'
@@ -697,7 +1003,124 @@ def tearDownModule():
self.addCleanup(sys.modules.pop, 'Module')
suite = unittest.TestSuite([Foo('test_foo')])
suite(result)
+ expected_out = '\nStdout:\ntear down module\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 1)
+ description = 'tearDownModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferDoModuleCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ print('set up module')
+ unittest.addModuleCleanup(bad_cleanup1)
+ unittest.addModuleCleanup(bad_cleanup2)
+
+ Foo.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ self.addCleanup(sys.modules.pop, 'Module')
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(len(result.errors), 1)
+ description = 'tearDownModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferSetUpModule_DoModuleCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ print('set up module')
+ unittest.addModuleCleanup(bad_cleanup1)
+ unittest.addModuleCleanup(bad_cleanup2)
+ 1/0
+
+ Foo.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ self.addCleanup(sys.modules.pop, 'Module')
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\nset up module\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = 'setUpModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\nset up module\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertIn(expected_out, formatted_exc)
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
+
+ def testBufferTearDownModule_DoModuleCleanups(self):
+ with captured_stdout() as stdout:
+ result = unittest.TestResult()
+ result.buffer = True
+
+ class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ print('set up module')
+ unittest.addModuleCleanup(bad_cleanup1)
+ unittest.addModuleCleanup(bad_cleanup2)
+ @staticmethod
+ def tearDownModule():
+ print('tear down module')
+ 1/0
+
+ Foo.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ self.addCleanup(sys.modules.pop, 'Module')
+ suite = unittest.TestSuite([Foo('test_foo')])
+ suite(result)
+ expected_out = '\nStdout:\ntear down module\ndo cleanup2\ndo cleanup1\n'
+ self.assertEqual(stdout.getvalue(), expected_out)
+ self.assertEqual(len(result.errors), 2)
+ description = 'tearDownModule (Module)'
+ test_case, formatted_exc = result.errors[0]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
+ self.assertNotIn('ValueError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn('\nStdout:\ntear down module\n', formatted_exc)
+ test_case, formatted_exc = result.errors[1]
+ self.assertEqual(test_case.description, description)
+ self.assertIn('ValueError: bad cleanup2', formatted_exc)
+ self.assertNotIn('ZeroDivisionError', formatted_exc)
+ self.assertNotIn('TypeError', formatted_exc)
+ self.assertIn(expected_out, formatted_exc)
if __name__ == '__main__':
diff --git a/Lib/unittest/test/test_runner.py b/Lib/unittest/test/test_runner.py
index dd9a1b6d9aeddf..453e6c3d11cfaa 100644
--- a/Lib/unittest/test/test_runner.py
+++ b/Lib/unittest/test/test_runner.py
@@ -222,14 +222,42 @@ def tearDownClass(cls):
self.assertEqual(ordering,
['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
- def test_debug_executes_classCleanUp(self):
+ def test_run_class_cleanUp_without_tearDownClass(self):
ordering = []
+ blowUp = True
class TestableTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
ordering.append('setUpClass')
cls.addClassCleanup(cleanup, ordering)
+ if blowUp:
+ raise Exception()
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ @property
+ def tearDownClass(cls):
+ raise AttributeError
+
+ runTests(TestableTest)
+ self.assertEqual(ordering, ['setUpClass', 'cleanup_good'])
+
+ ordering = []
+ blowUp = False
+ runTests(TestableTest)
+ self.assertEqual(ordering,
+ ['setUpClass', 'test', 'cleanup_good'])
+
+ def test_debug_executes_classCleanUp(self):
+ ordering = []
+ blowUp = False
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ cls.addClassCleanup(cleanup, ordering, blowUp=blowUp)
def testNothing(self):
ordering.append('test')
@classmethod
@@ -241,6 +269,48 @@ def tearDownClass(cls):
self.assertEqual(ordering,
['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'CleanUpExc')
+ self.assertEqual(ordering,
+ ['setUpClass', 'test', 'tearDownClass', 'cleanup_exc'])
+
+ def test_debug_executes_classCleanUp_when_teardown_exception(self):
+ ordering = []
+ blowUp = False
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ cls.addClassCleanup(cleanup, ordering, blowUp=blowUp)
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ raise Exception('TearDownClassExc')
+
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownClassExc')
+ self.assertEqual(ordering, ['setUpClass', 'test'])
+ self.assertTrue(TestableTest._class_cleanups)
+ TestableTest._class_cleanups.clear()
+
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownClassExc')
+ self.assertEqual(ordering, ['setUpClass', 'test'])
+ self.assertTrue(TestableTest._class_cleanups)
+ TestableTest._class_cleanups.clear()
+
def test_doClassCleanups_with_errors_addClassCleanUp(self):
class TestableTest(unittest.TestCase):
def testNothing(self):
@@ -332,6 +402,7 @@ def tearDownClass(cls):
self.assertEqual(ordering,
['setUpClass', 'setUp', 'test',
'tearDownClass', 'cleanup_exc'])
+
ordering = []
class_blow_up = True
method_blow_up = False
@@ -355,6 +426,26 @@ def tearDownClass(cls):
['setUpClass', 'setUp', 'tearDownClass',
'cleanup_exc'])
+ def test_with_errors_in_tearDownClass(self):
+ ordering = []
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ cls.addClassCleanup(cleanup, ordering)
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+ raise Exception('TearDownExc')
+
+ result = runTests(TestableTest)
+ self.assertEqual(result.errors[0][1].splitlines()[-1],
+ 'Exception: TearDownExc')
+ self.assertEqual(ordering,
+ ['setUpClass', 'test', 'tearDownClass', 'cleanup_good'])
+
class TestModuleCleanUp(unittest.TestCase):
def test_add_and_do_ModuleCleanup(self):
@@ -532,13 +623,69 @@ def tearDownClass(cls):
'tearDownModule2', 'cleanup_good'])
self.assertEqual(unittest.case._module_cleanups, [])
- def test_debug_module_executes_cleanUp(self):
+ def test_run_module_cleanUp_without_teardown(self):
ordering = []
class Module(object):
@staticmethod
def setUpModule():
ordering.append('setUpModule')
unittest.addModuleCleanup(cleanup, ordering)
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+
+ TestableTest.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ runTests(TestableTest)
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass', 'cleanup_good'])
+ self.assertEqual(unittest.case._module_cleanups, [])
+
+ def test_run_module_cleanUp_when_teardown_exception(self):
+ ordering = []
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ ordering.append('setUpModule')
+ unittest.addModuleCleanup(cleanup, ordering)
+ @staticmethod
+ def tearDownModule():
+ raise Exception('CleanUpExc')
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+
+ TestableTest.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ result = runTests(TestableTest)
+ self.assertEqual(result.errors[0][1].splitlines()[-1],
+ 'Exception: CleanUpExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass', 'cleanup_good'])
+ self.assertEqual(unittest.case._module_cleanups, [])
+
+ def test_debug_module_executes_cleanUp(self):
+ ordering = []
+ blowUp = False
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ ordering.append('setUpModule')
+ unittest.addModuleCleanup(cleanup, ordering, blowUp=blowUp)
@staticmethod
def tearDownModule():
ordering.append('tearDownModule')
@@ -562,6 +709,60 @@ def tearDownClass(cls):
'tearDownModule', 'cleanup_good'])
self.assertEqual(unittest.case._module_cleanups, [])
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'CleanUpExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass', 'tearDownModule', 'cleanup_exc'])
+ self.assertEqual(unittest.case._module_cleanups, [])
+
+ def test_debug_module_cleanUp_when_teardown_exception(self):
+ ordering = []
+ blowUp = False
+ class Module(object):
+ @staticmethod
+ def setUpModule():
+ ordering.append('setUpModule')
+ unittest.addModuleCleanup(cleanup, ordering, blowUp=blowUp)
+ @staticmethod
+ def tearDownModule():
+ raise Exception('TearDownModuleExc')
+
+ class TestableTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ ordering.append('setUpClass')
+ def testNothing(self):
+ ordering.append('test')
+ @classmethod
+ def tearDownClass(cls):
+ ordering.append('tearDownClass')
+
+ TestableTest.__module__ = 'Module'
+ sys.modules['Module'] = Module
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownModuleExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass'])
+ self.assertTrue(unittest.case._module_cleanups)
+ unittest.case._module_cleanups.clear()
+
+ ordering = []
+ blowUp = True
+ suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
+ with self.assertRaises(Exception) as cm:
+ suite.debug()
+ self.assertEqual(str(cm.exception), 'TearDownModuleExc')
+ self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
+ 'tearDownClass'])
+ self.assertTrue(unittest.case._module_cleanups)
+ unittest.case._module_cleanups.clear()
+
def test_addClassCleanup_arg_errors(self):
cleanups = []
def cleanup(*args, **kwargs):
@@ -717,9 +918,9 @@ def tearDownClass(cls):
method_blow_up = False
result = runTests(TestableTest)
self.assertEqual(result.errors[0][1].splitlines()[-1],
- 'Exception: CleanUpExc')
- self.assertEqual(result.errors[1][1].splitlines()[-1],
'Exception: ModuleExc')
+ self.assertEqual(result.errors[1][1].splitlines()[-1],
+ 'Exception: CleanUpExc')
self.assertEqual(ordering, ['setUpModule', 'cleanup_exc'])
ordering = []
diff --git a/Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst b/Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst
new file mode 100644
index 00000000000000..cf3d5ee0e456f1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-08-27-23-40-51.bpo-43913.Uo1Gt5.rst
@@ -0,0 +1,8 @@
+Fix bugs in cleaning up classes and modules in :mod:`unittest`:
+
+* Functions registered with :func:`~unittest.addModuleCleanup` were not called unless the user defines ``tearDownModule()`` in their test module.
+* Functions registered with :meth:`~unittest.TestCase.addClassCleanup` were not called if ``tearDownClass`` is set to ``None``.
+* Buffering in :class:`~unittest.TestResult` did not work with functions registered with ``addClassCleanup()`` and ``addModuleCleanup()``.
+* Errors in functions registered with ``addClassCleanup()`` and ``addModuleCleanup()`` were not handled correctly in buffered and debug modes.
+* Errors in ``setUpModule()`` and functions registered with ``addModuleCleanup()`` were reported in wrong order.
+* And several lesser bugs.
1
0
https://github.com/python/cpython/commit/07119dd38c9a6e5da84ca8a0a46acdf8a3…
commit: 07119dd38c9a6e5da84ca8a0a46acdf8a3e60ecf
branch: 3.8
author: Łukasz Langa <lukasz(a)langa.pl>
committer: ambv <lukasz(a)langa.pl>
date: 2021-08-30T16:26:41+02:00
summary:
Python 3.8.12
files:
A Misc/NEWS.d/3.8.12.rst
D Misc/NEWS.d/next/Core and Builtins/2021-08-07-01-26-12.bpo-44856.9rk3li.rst
D Misc/NEWS.d/next/Core and Builtins/2021-08-09-14-29-52.bpo-33930.--5LQ-.rst
D Misc/NEWS.d/next/Core and Builtins/2021-08-09-16-16-03.bpo-44872.OKRlhK.rst
D Misc/NEWS.d/next/Documentation/2021-07-20-21-03-18.bpo-30511.eMFkRi.rst
D Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
D Misc/NEWS.d/next/Security/2021-03-30-16-29-51.bpo-36384.sCAmLs.rst
D Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
D Misc/NEWS.d/next/Security/2021-06-29-02-45-53.bpo-44394.A220N1.rst
D Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst
D Misc/NEWS.d/next/Windows/2021-08-27-23-50-02.bpo-45007.NIBlVG.rst
D Misc/NEWS.d/next/macOS/2021-07-20-22-27-01.bpo-44689.mmT_xH.rst
D Misc/NEWS.d/next/macOS/2021-08-30-00-04-10.bpo-45007.pixqUB.rst
M Include/patchlevel.h
M Lib/pydoc_data/topics.py
M README.rst
diff --git a/Include/patchlevel.h b/Include/patchlevel.h
index cfdfa59b875d7b..de36b5af96719f 100644
--- a/Include/patchlevel.h
+++ b/Include/patchlevel.h
@@ -18,12 +18,12 @@
/*--start constants--*/
#define PY_MAJOR_VERSION 3
#define PY_MINOR_VERSION 8
-#define PY_MICRO_VERSION 11
+#define PY_MICRO_VERSION 12
#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL
#define PY_RELEASE_SERIAL 0
/* Version as a string */
-#define PY_VERSION "3.8.11+"
+#define PY_VERSION "3.8.12"
/*--end constants--*/
/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2.
diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py
index 119e5111bd68c2..7e8d5079077633 100644
--- a/Lib/pydoc_data/topics.py
+++ b/Lib/pydoc_data/topics.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Autogenerated by Sphinx on Mon Jun 28 12:07:21 2021
+# Autogenerated by Sphinx on Mon Aug 30 16:25:18 2021
topics = {'assert': 'The "assert" statement\n'
'**********************\n'
'\n'
@@ -10071,7 +10071,7 @@
'*start* and\n'
' *end* are interpreted as in slice notation.\n'
'\n'
- "str.encode(encoding='utf-8', errors='strict')\n"
+ 'str.encode(encoding="utf-8", errors="strict")\n'
'\n'
' Return an encoded version of the string as a bytes '
'object. Default\n'
@@ -10518,7 +10518,7 @@
'followed by\n'
' the string itself.\n'
'\n'
- 'str.rsplit(sep=None, maxsplit=- 1)\n'
+ 'str.rsplit(sep=None, maxsplit=-1)\n'
'\n'
' Return a list of the words in the string, using *sep* '
'as the\n'
@@ -10549,7 +10549,7 @@
" >>> 'mississippi'.rstrip('ipz')\n"
" 'mississ'\n"
'\n'
- 'str.split(sep=None, maxsplit=- 1)\n'
+ 'str.split(sep=None, maxsplit=-1)\n'
'\n'
' Return a list of the words in the string, using *sep* '
'as the\n'
@@ -11507,7 +11507,7 @@
' points. All the code points in the range "U+0000 - '
'U+10FFFF"\n'
' can be represented in a string. Python doesn’t have a '
- '*char*\n'
+ '"char"\n'
' type; instead, every code point in the string is '
'represented\n'
' as a string object with length "1". The built-in '
@@ -13291,7 +13291,7 @@
'| | "s[i:i] = '
'[x]") | |\n'
'+--------------------------------+----------------------------------+-----------------------+\n'
- '| "s.pop([i])" | retrieves the item at *i* '
+ '| "s.pop()" or "s.pop(i)" | retrieves the item at *i* '
'and | (2) |\n'
'| | also removes it from '
'*s* | |\n'
@@ -13754,7 +13754,7 @@
'| | "s[i:i] = '
'[x]") | |\n'
'+--------------------------------+----------------------------------+-----------------------+\n'
- '| "s.pop([i])" | retrieves the item at '
+ '| "s.pop()" or "s.pop(i)" | retrieves the item at '
'*i* and | (2) |\n'
'| | also removes it from '
'*s* | |\n'
diff --git a/Misc/NEWS.d/3.8.12.rst b/Misc/NEWS.d/3.8.12.rst
new file mode 100644
index 00000000000000..b64613ae2f5ab1
--- /dev/null
+++ b/Misc/NEWS.d/3.8.12.rst
@@ -0,0 +1,125 @@
+.. bpo: 42278
+.. date: 2021-08-29-12-39-44
+.. nonce: jvmQz_
+.. release date: 2021-08-30
+.. section: Security
+
+Replaced usage of :func:`tempfile.mktemp` with
+:class:`~tempfile.TemporaryDirectory` to avoid a potential race condition.
+
+..
+
+.. bpo: 44394
+.. date: 2021-06-29-02-45-53
+.. nonce: A220N1
+.. section: Security
+
+Update the vendored copy of libexpat to 2.4.1 (from 2.2.8) to get the fix
+for the CVE-2013-0340 "Billion Laughs" vulnerability. This copy is most used
+on Windows and macOS.
+
+..
+
+.. bpo: 43124
+.. date: 2021-05-08-11-50-46
+.. nonce: 2CTM6M
+.. section: Security
+
+Made the internal ``putcmd`` function in :mod:`smtplib` sanitize input for
+presence of ``\r`` and ``\n`` characters to avoid (unlikely) command
+injection.
+
+..
+
+.. bpo: 36384
+.. date: 2021-03-30-16-29-51
+.. nonce: sCAmLs
+.. section: Security
+
+:mod:`ipaddress` module no longer accepts any leading zeros in IPv4 address
+strings. Leading zeros are ambiguous and interpreted as octal notation by
+some libraries. For example the legacy function :func:`socket.inet_aton`
+treats leading zeros as octal notation. glibc implementation of modern
+:func:`~socket.inet_pton` does not accept any leading zeros. For a while the
+:mod:`ipaddress` module used to accept ambiguous leading zeros.
+
+..
+
+.. bpo: 44872
+.. date: 2021-08-09-16-16-03
+.. nonce: OKRlhK
+.. section: Core and Builtins
+
+Use new trashcan macros (Py_TRASHCAN_BEGIN/END) in frameobject.c instead of
+the old ones (Py_TRASHCAN_SAFE_BEGIN/END).
+
+..
+
+.. bpo: 33930
+.. date: 2021-08-09-14-29-52
+.. nonce: --5LQ-
+.. section: Core and Builtins
+
+Fix segmentation fault with deep recursion when cleaning method objects.
+Patch by Augusto Goulart and Pablo Galindo.
+
+..
+
+.. bpo: 44856
+.. date: 2021-08-07-01-26-12
+.. nonce: 9rk3li
+.. section: Core and Builtins
+
+Fix reference leaks in the error paths of ``update_bases()`` and
+``__build_class__``. Patch by Pablo Galindo.
+
+..
+
+.. bpo: 45001
+.. date: 2021-08-26-16-25-48
+.. nonce: tn_dKp
+.. section: Library
+
+Made email date parsing more robust against malformed input, namely a
+whitespace-only ``Date:`` header. Patch by Wouter Bolsterlee.
+
+..
+
+.. bpo: 30511
+.. date: 2021-07-20-21-03-18
+.. nonce: eMFkRi
+.. section: Documentation
+
+Clarify that :func:`shutil.make_archive` is not thread-safe due to reliance
+on changing the current working directory.
+
+..
+
+.. bpo: 45007
+.. date: 2021-08-27-23-50-02
+.. nonce: NIBlVG
+.. section: Windows
+
+Update to OpenSSL 1.1.1l in Windows build
+
+..
+
+.. bpo: 45007
+.. date: 2021-08-30-00-04-10
+.. nonce: pixqUB
+.. section: macOS
+
+Update macOS installer builds to use OpenSSL 1.1.1l.
+
+..
+
+.. bpo: 44689
+.. date: 2021-07-20-22-27-01
+.. nonce: mmT_xH
+.. section: macOS
+
+:meth:`ctypes.util.find_library` now works correctly on macOS 11 Big Sur
+even if Python is built on an older version of macOS. Previously, when
+built on older macOS systems, ``find_library`` was not able to find macOS
+system libraries when running on Big Sur due to changes in how system
+libraries are stored.
diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-08-07-01-26-12.bpo-44856.9rk3li.rst b/Misc/NEWS.d/next/Core and Builtins/2021-08-07-01-26-12.bpo-44856.9rk3li.rst
deleted file mode 100644
index 1111d01b726fa2..00000000000000
--- a/Misc/NEWS.d/next/Core and Builtins/2021-08-07-01-26-12.bpo-44856.9rk3li.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fix reference leaks in the error paths of ``update_bases()`` and ``__build_class__``. Patch by Pablo Galindo.
diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-08-09-14-29-52.bpo-33930.--5LQ-.rst b/Misc/NEWS.d/next/Core and Builtins/2021-08-09-14-29-52.bpo-33930.--5LQ-.rst
deleted file mode 100644
index 827dd3f8b65131..00000000000000
--- a/Misc/NEWS.d/next/Core and Builtins/2021-08-09-14-29-52.bpo-33930.--5LQ-.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fix segmentation fault with deep recursion when cleaning method objects.
-Patch by Augusto Goulart and Pablo Galindo.
diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-08-09-16-16-03.bpo-44872.OKRlhK.rst b/Misc/NEWS.d/next/Core and Builtins/2021-08-09-16-16-03.bpo-44872.OKRlhK.rst
deleted file mode 100644
index 9a0d00018b2a7c..00000000000000
--- a/Misc/NEWS.d/next/Core and Builtins/2021-08-09-16-16-03.bpo-44872.OKRlhK.rst
+++ /dev/null
@@ -1 +0,0 @@
-Use new trashcan macros (Py_TRASHCAN_BEGIN/END) in frameobject.c instead of the old ones (Py_TRASHCAN_SAFE_BEGIN/END).
\ No newline at end of file
diff --git a/Misc/NEWS.d/next/Documentation/2021-07-20-21-03-18.bpo-30511.eMFkRi.rst b/Misc/NEWS.d/next/Documentation/2021-07-20-21-03-18.bpo-30511.eMFkRi.rst
deleted file mode 100644
index a358fb9cc2860b..00000000000000
--- a/Misc/NEWS.d/next/Documentation/2021-07-20-21-03-18.bpo-30511.eMFkRi.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Clarify that :func:`shutil.make_archive` is not thread-safe due to
-reliance on changing the current working directory.
diff --git a/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst b/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
deleted file mode 100644
index 55cc409d0da30f..00000000000000
--- a/Misc/NEWS.d/next/Library/2021-08-26-16-25-48.bpo-45001.tn_dKp.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Made email date parsing more robust against malformed input, namely a
-whitespace-only ``Date:`` header. Patch by Wouter Bolsterlee.
diff --git a/Misc/NEWS.d/next/Security/2021-03-30-16-29-51.bpo-36384.sCAmLs.rst b/Misc/NEWS.d/next/Security/2021-03-30-16-29-51.bpo-36384.sCAmLs.rst
deleted file mode 100644
index 6e2926ce1e1746..00000000000000
--- a/Misc/NEWS.d/next/Security/2021-03-30-16-29-51.bpo-36384.sCAmLs.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-:mod:`ipaddress` module no longer accepts any leading zeros in IPv4 address
-strings. Leading zeros are ambiguous and interpreted as octal notation by
-some libraries. For example the legacy function :func:`socket.inet_aton`
-treats leading zeros as octal notation. glibc implementation of modern
-:func:`~socket.inet_pton` does not accept any leading zeros. For a while
-the :mod:`ipaddress` module used to accept ambiguous leading zeros.
diff --git a/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst b/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
deleted file mode 100644
index e897d6cd3641d7..00000000000000
--- a/Misc/NEWS.d/next/Security/2021-05-08-11-50-46.bpo-43124.2CTM6M.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Made the internal ``putcmd`` function in :mod:`smtplib` sanitize input for
-presence of ``\r`` and ``\n`` characters to avoid (unlikely) command injection.
diff --git a/Misc/NEWS.d/next/Security/2021-06-29-02-45-53.bpo-44394.A220N1.rst b/Misc/NEWS.d/next/Security/2021-06-29-02-45-53.bpo-44394.A220N1.rst
deleted file mode 100644
index e32563d2535c7e..00000000000000
--- a/Misc/NEWS.d/next/Security/2021-06-29-02-45-53.bpo-44394.A220N1.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Update the vendored copy of libexpat to 2.4.1 (from 2.2.8) to get the fix
-for the CVE-2013-0340 "Billion Laughs" vulnerability. This copy is most used
-on Windows and macOS.
diff --git a/Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst b/Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst
deleted file mode 100644
index db880cd9026da4..00000000000000
--- a/Misc/NEWS.d/next/Security/2021-08-29-12-39-44.bpo-42278.jvmQz_.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Replaced usage of :func:`tempfile.mktemp` with
-:class:`~tempfile.TemporaryDirectory` to avoid a potential race condition.
diff --git a/Misc/NEWS.d/next/Windows/2021-08-27-23-50-02.bpo-45007.NIBlVG.rst b/Misc/NEWS.d/next/Windows/2021-08-27-23-50-02.bpo-45007.NIBlVG.rst
deleted file mode 100644
index fa076ee4c8b8ad..00000000000000
--- a/Misc/NEWS.d/next/Windows/2021-08-27-23-50-02.bpo-45007.NIBlVG.rst
+++ /dev/null
@@ -1 +0,0 @@
-Update to OpenSSL 1.1.1l in Windows build
diff --git a/Misc/NEWS.d/next/macOS/2021-07-20-22-27-01.bpo-44689.mmT_xH.rst b/Misc/NEWS.d/next/macOS/2021-07-20-22-27-01.bpo-44689.mmT_xH.rst
deleted file mode 100644
index b1e878d1ee44af..00000000000000
--- a/Misc/NEWS.d/next/macOS/2021-07-20-22-27-01.bpo-44689.mmT_xH.rst
+++ /dev/null
@@ -1,5 +0,0 @@
- :meth:`ctypes.util.find_library` now works correctly on macOS 11 Big Sur
- even if Python is built on an older version of macOS. Previously, when
- built on older macOS systems, ``find_library`` was not able to find
- macOS system libraries when running on Big Sur due to changes in
- how system libraries are stored.
diff --git a/Misc/NEWS.d/next/macOS/2021-08-30-00-04-10.bpo-45007.pixqUB.rst b/Misc/NEWS.d/next/macOS/2021-08-30-00-04-10.bpo-45007.pixqUB.rst
deleted file mode 100644
index e4f1ac6de313d4..00000000000000
--- a/Misc/NEWS.d/next/macOS/2021-08-30-00-04-10.bpo-45007.pixqUB.rst
+++ /dev/null
@@ -1 +0,0 @@
-Update macOS installer builds to use OpenSSL 1.1.1l.
diff --git a/README.rst b/README.rst
index 7910dc74746c81..7ef8ea53d0f3c6 100644
--- a/README.rst
+++ b/README.rst
@@ -1,4 +1,4 @@
-This is Python version 3.8.11
+This is Python version 3.8.12
=============================
.. image:: https://travis-ci.org/python/cpython.svg?branch=3.8
1
0