From c74cba16a3e49c7ffc4a7d5a73334cb07cf673ef Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Mon, 4 May 2026 14:02:50 -0400 Subject: [PATCH 01/10] gh-145917: Add MIME types for TTC and Haptics formats (#145918) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Benedikt Johannes Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> --- Doc/whatsnew/3.15.rst | 2 +- Lib/mimetypes.py | 4 ++++ .../Library/2026-03-13-14-23-33.gh-issue-145917.TooGKx.rst | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-13-14-23-33.gh-issue-145917.TooGKx.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 3baae534041446..b215c56408503a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1045,7 +1045,7 @@ mimetypes (Contributed by Benedikt Johannes, Charlie Lin, Foolbar, Gil Forcada and John Franey in :gh:`144217`, :gh:`145720`, :gh:`140937`, :gh:`139959`, :gh:`145698`, - :gh:`145718` and :gh:`144213`.) + :gh:`145718`, :gh:`145918`, and :gh:`144213`.) * Rename ``application/x-texinfo`` to ``application/texinfo``. (Contributed by Charlie Lin in :gh:`140165`.) * Changed the MIME type for ``.ai`` files to ``application/pdf``. diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index a834826114614d..ad18db09f6b340 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -599,10 +599,14 @@ def _default_mime_types(): '.ra' : 'audio/x-pn-realaudio', '.wav' : 'audio/vnd.wave', '.weba' : 'audio/webm', + '.ttc' : 'font/collection', '.otf' : 'font/otf', '.ttf' : 'font/ttf', '.woff' : 'font/woff', '.woff2' : 'font/woff2', + '.hjif' : 'haptics/hjif', + '.hmpg' : 'haptics/hmpg', + '.ivs' : 'haptics/ivs', '.avif' : 'image/avif', '.bmp' : 'image/bmp', '.emf' : 'image/emf', diff --git a/Misc/NEWS.d/next/Library/2026-03-13-14-23-33.gh-issue-145917.TooGKx.rst b/Misc/NEWS.d/next/Library/2026-03-13-14-23-33.gh-issue-145917.TooGKx.rst new file mode 100644 index 00000000000000..23933a633f2391 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-13-14-23-33.gh-issue-145917.TooGKx.rst @@ -0,0 +1,2 @@ +Add MIME types for TTC and Haptics formats to :mod:`mimetypes`. +(Contributed by Charlie Lin in :gh:`145918`.) From bc285e583286c739e553e49c19fd946cb63432c7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 4 May 2026 21:03:11 +0300 Subject: [PATCH 02/10] gh-138907: Support RFC 9309 in robotparser (GH-138908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * empty lines are always ignored instead of separating groups * the "user-agent" line after a rule starts a new group * groups matching the same user agent are now merged * the rule with the longest match wins instead of the first matching rule * in case of equal matches, the “Allow” rule wins over “Disallow” * special characters “$” and “*” are now supported in rules * prefer full match for user agent --- Doc/library/urllib.robotparser.rst | 2 +- Lib/test/test_robotparser.py | 340 ++++++++++++++++-- Lib/urllib/robotparser.py | 209 +++++++---- ...-04-25-14-11-24.gh-issue-138907.u21Wnh.rst | 1 + 4 files changed, 441 insertions(+), 111 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-25-14-11-24.gh-issue-138907.u21Wnh.rst diff --git a/Doc/library/urllib.robotparser.rst b/Doc/library/urllib.robotparser.rst index 492c65ae209d92..1fa7fc13baa539 100644 --- a/Doc/library/urllib.robotparser.rst +++ b/Doc/library/urllib.robotparser.rst @@ -18,7 +18,7 @@ This module provides a single class, :class:`RobotFileParser`, which answers questions about whether or not a particular user agent can fetch a URL on the website that published the :file:`robots.txt` file. For more details on the -structure of :file:`robots.txt` files, see http://www.robotstxt.org/orig.html. +structure of :file:`robots.txt` files, see :rfc:`9309`. .. class:: RobotFileParser(url='') diff --git a/Lib/test/test_robotparser.py b/Lib/test/test_robotparser.py index e33723cc70c877..3ea0ec66fbfbe9 100644 --- a/Lib/test/test_robotparser.py +++ b/Lib/test/test_robotparser.py @@ -15,14 +15,18 @@ class BaseRobotTest: good = [] bad = [] site_maps = None + expected_output = None def __init_subclass__(cls): super().__init_subclass__() # Remove tests that do nothing. - if not cls.good: - cls.test_good_urls = None - if not cls.bad: - cls.test_bad_urls = None + if issubclass(cls, unittest.TestCase): + if not cls.good: + cls.test_good_urls = None + if not cls.bad: + cls.test_bad_urls = None + if cls.expected_output is None: + cls.test_string_formatting = None def setUp(self): lines = io.StringIO(self.robots_txt).readlines() @@ -50,6 +54,8 @@ def test_bad_urls(self): def test_site_maps(self): self.assertEqual(self.parser.site_maps(), self.site_maps) + def test_string_formatting(self): + self.assertEqual(str(self.parser), self.expected_output) class UserAgentWildcardTest(BaseRobotTest, unittest.TestCase): robots_txt = """\ @@ -61,6 +67,56 @@ class UserAgentWildcardTest(BaseRobotTest, unittest.TestCase): good = ['/', '/test.html'] bad = ['/cyberworld/map/index.html', '/tmp/xxx', '/foo.html'] +class SimpleExampleTest(BaseRobotTest, unittest.TestCase): + # Example from RFC 9309, section 5.1. + robots_txt = """\ +User-Agent: * +Disallow: *.gif$ +Disallow: /example/ +Allow: /publications/ + +User-Agent: foobot +Disallow:/ +Allow:/example/page.html +Allow:/example/allowed.gif + +User-Agent: barbot +User-Agent: bazbot +Disallow: /example/page.html + +User-Agent: quxbot + """ + good = [ + '/', '/publications/', + ('foobot', '/example/page.html'), ('foobot', '/example/allowed.gif'), + ('barbot', '/'), ('barbot', '/example/'), + ('barbot', '/example/allowed.gif'), + ('barbot', '/example/disallowed.gif'), + ('barbot', '/publications/'), + ('barbot', '/publications/allowed.gif'), + ('bazbot', '/'), ('bazbot', '/example/'), + ('bazbot', '/example/allowed.gif'), + ('bazbot', '/example/disallowed.gif'), + ('bazbot', '/publications/'), + ('bazbot', '/publications/allowed.gif'), + ('quxbot', '/'), ('quxbot', '/example/'), + ('quxbot', '/example/page.html'), ('quxbot', '/example/allowed.gif'), + ('quxbot', '/example/disallowed.gif'), + ('quxbot', '/publications/'), + ('quxbot', '/publications/allowed.gif'), + ] + bad = [ + '/example/', '/example/page.html', '/example/allowed.gif', + '/example/disallowed.gif', + '/publications/allowed.gif', + ('foobot', '/'), ('foobot', '/example/'), + ('foobot', '/example/disallowed.gif'), + ('foobot', '/publications/'), + ('foobot', '/publications/allowed.gif'), + ('barbot', '/example/page.html'), + ('bazbot', '/example/page.html'), + ] + class CrawlDelayAndCustomAgentTest(BaseRobotTest, unittest.TestCase): robots_txt = """\ @@ -102,7 +158,7 @@ class RejectAllRobotsTest(BaseRobotTest, unittest.TestCase): User-agent: * Disallow: / """ - good = [] + good = ['/robots.txt'] bad = ['/cyberworld/map/index.html', '/', '/tmp/'] @@ -137,6 +193,7 @@ def test_request_rate(self): class EmptyFileTest(BaseRequestRateTest, unittest.TestCase): robots_txt = '' good = ['/foo'] + expected_output = '' class CrawlDelayAndRequestRateTest(BaseRequestRateTest, unittest.TestCase): @@ -203,35 +260,209 @@ class AnotherInvalidRequestRateTest(BaseRobotTest, unittest.TestCase): class UserAgentOrderingTest(BaseRobotTest, unittest.TestCase): - # the order of User-agent should be correct. note - # that this file is incorrect because "Googlebot" is a - # substring of "Googlebot-Mobile" + # the order of User-agent should not matter robots_txt = """\ User-agent: Googlebot Disallow: / +Allow: /folder1/ User-agent: Googlebot-Mobile Allow: / +Disallow: /folder1/ """ agent = 'Googlebot' bad = ['/something.jpg'] + good = ['/folder1/myfile.html'] class UserAgentGoogleMobileTest(UserAgentOrderingTest): - agent = 'Googlebot-Mobile' + agent = 'Googlebot-mobile' + bad = ['/folder1/myfile.html'] + good = ['/something.jpg'] -class GoogleURLOrderingTest(BaseRobotTest, unittest.TestCase): - # Google also got the order wrong. You need - # to specify the URLs from more specific to more general +class LongestMatchTest(BaseRobotTest, unittest.TestCase): + # Based on example from RFC 9309, section 5.2. robots_txt = """\ -User-agent: Googlebot -Allow: /folder1/myfile.html -Disallow: /folder1/ +User-agent: * +Allow: /example/page/ +Disallow: /example/page/disallowed.gif +Allow: /example/ """ - agent = 'googlebot' - good = ['/folder1/myfile.html'] - bad = ['/folder1/anotherfile.html'] + good = ['/example/', '/example/page/'] + bad = ['/example/page/disallowed.gif'] + + +class LongestMatchWildcardTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: * +Allow: /example/page/ +Disallow: *.gif +Allow: /example/ + """ + good = ['/example/', '/example/page/'] + bad = ['/example/page/disallowed.gif', '/x.gif'] + + +class AllowWinsEqualMatchTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: * +Disallow: /spam +Allow: /spam +Disallow: /spam + """ + good = ['/spam', '/spam/'] + + +class AllowWinsEqualFullMatchTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: * +Disallow: /spam +Allow: /spam$ +Disallow: /spam +Disallow: /eggs$ +Allow: /eggs +Disallow: /eggs$ + """ + good = ['/spam', '/eggs', '/eggs/'] + bad = ['/spam/'] + + +class AllowWinsEqualMatchWildcardTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: * +Disallow: /spam +Allow: *am +Disallow: /spam +Disallow: *gs +Allow: /eggs +Disallow: *gs + """ + good = ['/spam', '/eggs', '/spam/', '/eggs/'] + + +class MergeGroupsTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: spambot +Disallow: /some/path + +User-agent: spambot +Disallow: /another/path + """ + agent = 'spambot' + bad = ['/some/path', '/another/path'] + + +class UserAgentStartsGroupTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: spambot +Disallow: /some/path +User-agent: eggsbot +Disallow: /another/path + """ + good = [('spambot', '/'), ('spambot', '/another/path'), + ('eggsbot', '/'), ('eggsbot', '/some/path')] + bad = [('spambot', '/some/path'), ('eggsbot', '/another/path')] + expected_output = """\ +User-agent: spambot +Disallow: /some/path + +User-agent: eggsbot +Disallow: /another/path\ +""" + +class IgnoreEmptyLinesTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: spambot + +User-agent: eggsbot +Disallow: /some/path + +Disallow: /another/path + """ + good = [('spambot', '/'), ('eggsbot', '/')] + bad = [ + ('spambot', '/some/path'), ('spambot', '/another/path'), + ('eggsbot', '/some/path'), ('eggsbot', '/another/path'), + ] + expected_output = """\ +User-agent: spambot +User-agent: eggsbot +Disallow: /some/path +Disallow: /another/path\ +""" + + +class IgnoreRulesWithoutUserAgentTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +Disallow: /some/path + +User-agent: * +Disallow: /another/path + """ + good = ['/', '/some/path'] + bad = ['/another/path'] + expected_output = """\ +User-agent: * +Disallow: /another/path\ +""" + + +class EmptyGroupTest(BaseRobotTest, unittest.TestCase): + robots_txt = """\ +User-agent: * +Disallow: /some/path + +User-agent: spambot + """ + agent = 'spambot' + good = ['/', '/some/path'] + expected_output = """\ +User-agent: * +Disallow: /some/path + +User-agent: spambot +Allow:\ +""" + + +class WeirdPathTest(BaseRobotTest, unittest.TestCase): + robots_txt = f"""\ +User-agent: * +Disallow: /a$$$ +Disallow: /b$z +Disallow: /c*** +Disallow: /d***z +Disallow: /e*$**$$ +Disallow: /f*$**$$z +Disallow: /g$*$$** +Disallow: /h$*$$**z + """ + good = ['/ax', '/a$$', '/b', '/bz', '/b$z', '/d', '/f', '/fz', + '/f$$$z', '/fx$y$$z', '/gx', '/g$$$', '/g$x$$y', '/h', '/hz', + '/h$$$z', '/h$x$$yz'] + bad = ['/a', '/c', '/cxy', '/dz', '/dxyz', '/dxzy', '/e', '/exy', + '/e$$', '/ex$y$', '/g'] + expected_output = """\ +User-agent: * +Disallow: /a$ +Disallow: /c* +Disallow: /d*z +Disallow: /e*$ +Disallow: /g$\ +""" + + +class PathWithManyWildcardsTest(BaseRobotTest, unittest.TestCase): + # This test would take many years if use naive translation to regular + # expression (* -> .*). + N = 50 + robots_txt = f"""\ +User-agent: * +Disallow: /{'*a'*N}*b + """ + good = ['/' + 'a'*N + 'a'] + bad = ['/' + 'a'*N + 'b'] class DisallowQueryStringTest(BaseRobotTest, unittest.TestCase): @@ -245,25 +476,13 @@ class DisallowQueryStringTest(BaseRobotTest, unittest.TestCase): good = ['/some/path', '/some/path?', '/some/path%3Fname=value', '/some/path?name%3Dvalue', '/another/path', '/another/path%3F', - '/yet/one/path?name=value%26more'] + '/yet/one/path?name=value%26more', + '/some/pathxname=value'] bad = ['/some/path?name=value' '/another/path?', '/another/path?name=value', '/yet/one/path?name=value&more'] -class UseFirstUserAgentWildcardTest(BaseRobotTest, unittest.TestCase): - # obey first * entry (#4108) - robots_txt = """\ -User-agent: * -Disallow: /some/path - -User-agent: * -Disallow: /another/path - """ - good = ['/another/path'] - bad = ['/some/path'] - - class PercentEncodingTest(BaseRobotTest, unittest.TestCase): robots_txt = """\ User-agent: * @@ -365,17 +584,60 @@ class StringFormattingTest(BaseRobotTest, unittest.TestCase): """ expected_output = """\ -User-agent: cybermapper -Disallow: /some/path - User-agent: * Crawl-delay: 1 Request-rate: 3/15 -Disallow: /cyberworld/map/\ +Disallow: /cyberworld/map/ + +User-agent: cybermapper +Disallow: /some/path\ """ - def test_string_formatting(self): - self.assertEqual(str(self.parser), self.expected_output) + +class ConstructedStringFormattingTest(unittest.TestCase): + def test_empty(self): + parser = urllib.robotparser.RobotFileParser() + self.assertEqual(str(parser), '') + + def test_group_without_rules(self): + parser = urllib.robotparser.RobotFileParser() + entry = urllib.robotparser.Entry() + entry.useragents = ['spambot'] + parser._add_entry(entry) + entry = urllib.robotparser.Entry() + entry.useragents = ['hambot'] + entry.rulelines = [urllib.robotparser.RuleLine('/ham', False)] + parser._add_entry(entry) + entry = urllib.robotparser.Entry() + entry.useragents = ['eggsbot'] + parser._add_entry(entry) + self.assertEqual(str(parser), """\ +User-agent: spambot +Allow: + +User-agent: hambot +Disallow: /ham + +User-agent: eggsbot +Allow:\ +""") + + def test_group_without_user_agent(self): + parser = urllib.robotparser.RobotFileParser() + entry = urllib.robotparser.Entry() + entry.rulelines = [urllib.robotparser.RuleLine('/ham', False)] + parser._add_entry(entry) + entry = urllib.robotparser.Entry() + entry.useragents = ['spambot'] + entry.rulelines = [urllib.robotparser.RuleLine('/spam', False)] + parser._add_entry(entry) + entry = urllib.robotparser.Entry() + entry.rulelines = [urllib.robotparser.RuleLine('/eggs', False)] + parser._add_entry(entry) + self.assertEqual(str(parser), """\ +User-agent: spambot +Disallow: /spam\ +""") @unittest.skipUnless( @@ -495,7 +757,7 @@ def test_basic(self): def test_can_fetch(self): self.assertTrue(self.parser.can_fetch('*', self.url('elsewhere'))) self.assertFalse(self.parser.can_fetch('Nutch', self.base_url)) - self.assertFalse(self.parser.can_fetch('Nutch', self.url('brian'))) + self.assertTrue(self.parser.can_fetch('Nutch', self.url('brian'))) self.assertFalse(self.parser.can_fetch('Nutch', self.url('webstats'))) self.assertFalse(self.parser.can_fetch('*', self.url('webstats'))) self.assertTrue(self.parser.can_fetch('*', self.base_url)) diff --git a/Lib/urllib/robotparser.py b/Lib/urllib/robotparser.py index 4009fd6b58f594..e70eae80036784 100644 --- a/Lib/urllib/robotparser.py +++ b/Lib/urllib/robotparser.py @@ -7,7 +7,7 @@ 2) PSF license for Python 2.2 The robots.txt Exclusion Protocol is implemented as specified in - http://www.robotstxt.org/norobots-rfc.txt + RFC 9309 """ import collections @@ -21,19 +21,6 @@ RequestRate = collections.namedtuple("RequestRate", "requests seconds") -def normalize(path): - unquoted = urllib.parse.unquote(path, errors='surrogateescape') - return urllib.parse.quote(unquoted, errors='surrogateescape') - -def normalize_path(path): - path, sep, query = path.partition('?') - path = normalize(path) - if sep: - query = re.sub(r'[^=&]+', lambda m: normalize(m[0]), query) - path += '?' + query - return path - - class RobotFileParser: """ This class provides a set of methods to read, parse and answer questions about a single robots.txt file. @@ -42,6 +29,7 @@ class RobotFileParser: def __init__(self, url=''): self.entries = [] + self.groups = {} self.sitemaps = [] self.default_entry = None self.disallow_all = False @@ -86,13 +74,13 @@ def read(self): self.parse(raw.decode("utf-8", "surrogateescape").splitlines()) def _add_entry(self, entry): - if "*" in entry.useragents: - # the default entry is considered last - if self.default_entry is None: - # the first default entry wins - self.default_entry = entry - else: - self.entries.append(entry) + self.entries.append(entry) + for agent in entry.useragents: + agent = agent.lower() + if agent not in self.groups: + self.groups[agent] = entry + else: + self.groups[agent] = merge_entries(self.groups[agent], entry) def parse(self, lines): """Parse the input lines from a robots.txt file. @@ -100,6 +88,7 @@ def parse(self, lines): We allow that a user-agent: line is not preceded by one or more blank lines. """ + entries = [] # states: # 0: start state # 1: saw user-agent line @@ -109,14 +98,6 @@ def parse(self, lines): self.modified() for line in lines: - if not line: - if state == 1: - entry = Entry() - state = 0 - elif state == 2: - self._add_entry(entry) - entry = Entry() - state = 0 # remove optional comment and strip line i = line.find('#') if i >= 0: @@ -132,16 +113,23 @@ def parse(self, lines): if state == 2: self._add_entry(entry) entry = Entry() - entry.useragents.append(line[1]) + product_token = line[1] + entry.useragents.append(product_token) state = 1 elif line[0] == "disallow": if state != 0: - entry.rulelines.append(RuleLine(line[1], False)) state = 2 + try: + entry.rulelines.append(RuleLine(line[1], False)) + except ValueError: + pass elif line[0] == "allow": if state != 0: - entry.rulelines.append(RuleLine(line[1], True)) state = 2 + try: + entry.rulelines.append(RuleLine(line[1], True)) + except ValueError: + pass elif line[0] == "crawl-delay": if state != 0: # before trying to convert to int we need to make @@ -164,9 +152,18 @@ def parse(self, lines): # so it doesn't matter where you place it in your file." # Therefore we do not change the state of the parser. self.sitemaps.append(line[1]) - if state == 2: + if state != 0: self._add_entry(entry) + def _find_entry(self, useragent): + entry = self.groups.get(useragent.lower()) + if entry is not None: + return entry + for entry in self.groups.values(): + if entry.applies_to(useragent): + return entry + return self.groups.get('*') + def can_fetch(self, useragent, url): """using the parsed robots.txt decide if useragent can fetch url""" if self.disallow_all: @@ -179,43 +176,36 @@ def can_fetch(self, useragent, url): # calls can_fetch() before calling read(). if not self.last_checked: return False - # search for given user agent matches - # the first match counts # TODO: The private API is used in order to preserve an empty query. # This is temporary until the public API starts supporting this feature. parsed_url = urllib.parse._urlsplit(url, '') url = urllib.parse._urlunsplit(None, None, *parsed_url[2:]) - url = normalize_path(url) + url = normalize_uri(url) if not url: url = "/" - for entry in self.entries: - if entry.applies_to(useragent): - return entry.allowance(url) - # try the default entry last - if self.default_entry: - return self.default_entry.allowance(url) - # agent not found ==> access granted - return True + if url == '/robots.txt': + # The /robots.txt URI is implicitly allowed. + return True + entry = self._find_entry(useragent) + if entry is None: + return True + return entry.allowance(url) def crawl_delay(self, useragent): if not self.mtime(): return None - for entry in self.entries: - if entry.applies_to(useragent): - return entry.delay - if self.default_entry: - return self.default_entry.delay - return None + entry = self._find_entry(useragent) + if entry is None: + return None + return entry.delay def request_rate(self, useragent): if not self.mtime(): return None - for entry in self.entries: - if entry.applies_to(useragent): - return entry.req_rate - if self.default_entry: - return self.default_entry.req_rate - return None + entry = self._find_entry(useragent) + if entry is None: + return None + return entry.req_rate def site_maps(self): if not self.sitemaps: @@ -226,7 +216,7 @@ def __str__(self): entries = self.entries if self.default_entry is not None: entries = entries + [self.default_entry] - return '\n\n'.join(map(str, entries)) + return '\n\n'.join(filter(None, map(str, entries))) class RuleLine: """A rule line is a single "Allow:" (allowance==True) or "Disallow:" @@ -235,14 +225,42 @@ def __init__(self, path, allowance): if path == '' and not allowance: # an empty value means allow all allowance = True - self.path = normalize_path(path) + path = re.sub(r'[*]{2,}', '*', path) + path = re.sub(r'[$][$*]+', '$', path) + path = normalize_pattern(path) + self.fullmatch = path.endswith('$') + path = path.rstrip('$') + if '$' in path: + raise ValueError('$ not at the end of path') + self.matcher = None + if '*' in path: + pattern = re.compile(translate_pattern(path), re.DOTALL) + if self.fullmatch: + self.matcher = pattern.fullmatch + else: + self.matcher = pattern.match + self.path = path self.allowance = allowance def applies_to(self, filename): - return self.path == "*" or filename.startswith(self.path) + # If the filename matches the rule, return the matching length plus 1. + # If it does not match, return 0. + if self.matcher is not None: + m = self.matcher(filename) + if m: + return m.end() + 1 + else: + if self.fullmatch: + if filename == self.path: + return len(self.path) + 1 + else: + if filename.startswith(self.path): + return len(self.path) + 1 + return 0 def __str__(self): - return ("Allow" if self.allowance else "Disallow") + ": " + self.path + return (("Allow" if self.allowance else "Disallow") + ": " + self.path + + ('$' if self.fullmatch else '')) class Entry: @@ -254,6 +272,8 @@ def __init__(self): self.req_rate = None def __str__(self): + if not self.useragents: + return '' ret = [] for agent in self.useragents: ret.append(f"User-agent: {agent}") @@ -262,27 +282,74 @@ def __str__(self): if self.req_rate is not None: rate = self.req_rate ret.append(f"Request-rate: {rate.requests}/{rate.seconds}") - ret.extend(map(str, self.rulelines)) + if self.rulelines: + ret.extend(map(str, self.rulelines)) + else: + ret.append("Allow:") return '\n'.join(ret) def applies_to(self, useragent): """check if this entry applies to the specified agent""" + if useragent is None: + return '*' in self.useragents # split the name token and make it lower case useragent = useragent.split("/")[0].lower() for agent in self.useragents: - if agent == '*': - # we have the catch-all agent - return True - agent = agent.lower() - if agent in useragent: - return True + if agent != '*': + agent = agent.lower() + if agent in useragent: + return True return False def allowance(self, filename): """Preconditions: - our agent applies to this entry - - filename is URL encoded""" + - filename is URL encoded + """ + best_match = -1 + allowance = True for line in self.rulelines: - if line.applies_to(filename): - return line.allowance - return True + m = line.applies_to(filename) + if m: + if m > best_match: + best_match = m + allowance = line.allowance + elif m == best_match and not allowance: + allowance = line.allowance + return allowance + + +def normalize(path): + unquoted = urllib.parse.unquote(path, errors='surrogateescape') + return urllib.parse.quote(unquoted, errors='surrogateescape') + +def normalize_uri(path): + path, sep, query = path.partition('?') + path = normalize(path) + if sep: + query = re.sub(r'[^=&]+', lambda m: normalize(m[0]), query) + path += '?' + query + return path + +def normalize_pattern(path): + path, sep, query = path.partition('?') + path = re.sub(r'[^*$]+', lambda m: normalize(m[0]), path) + if sep: + query = re.sub(r'[^=&*$]+', lambda m: normalize(m[0]), query) + path += '?' + query + return path + +def translate_pattern(path): + parts = list(map(re.escape, path.split('*'))) + for i in range(1, len(parts)-1): + parts[i] = f'(?>.*?{parts[i]})' + parts[-1] = f'.*{parts[-1]}' + return ''.join(parts) + +def merge_entries(e1, e2): + entry = Entry() + entry.useragents = list(filter(set(e2.useragents).__contains__, e1.useragents)) + entry.rulelines = e1.rulelines + e2.rulelines + entry.delay = e1.delay if e2.delay is None else e2.delay + entry.req_rate = e1.req_rate if e2.req_rate is None else e2.req_rate + return entry diff --git a/Misc/NEWS.d/next/Library/2026-04-25-14-11-24.gh-issue-138907.u21Wnh.rst b/Misc/NEWS.d/next/Library/2026-04-25-14-11-24.gh-issue-138907.u21Wnh.rst new file mode 100644 index 00000000000000..cc996a85f1c167 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-25-14-11-24.gh-issue-138907.u21Wnh.rst @@ -0,0 +1 @@ +Support :rfc:`9309` in :mod:`urllib.robotparser`. From 12a20daee7295b35eab570dd1c16a976d4f7c3a9 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 4 May 2026 19:03:21 +0100 Subject: [PATCH 03/10] gh-149321: Fix stdlib imports with lazy imports disabled (#149338) --- Lib/ast.py | 4 +- Lib/dataclasses.py | 3 +- Lib/test/test_lazy_import/__init__.py | 44 +++++++++++++++++++ ...-05-03-12-00-00.gh-issue-149321.fUaxrz.rst | 2 + 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-03-12-00-00.gh-issue-149321.fUaxrz.rst diff --git a/Lib/ast.py b/Lib/ast.py index ba4ee0197b85d2..a7997c4b740635 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -21,7 +21,6 @@ :license: Python License. """ from _ast import * -lazy from _colorize import can_colorize, get_theme def parse(source, filename='', mode='exec', *, @@ -142,6 +141,8 @@ def dump( If show_empty is False, then empty lists and fields that are None will be omitted from the output for better readability. """ + from _colorize import get_theme + t = get_theme(force_color=color, force_no_color=not color).ast def _format(node, level=0): @@ -708,6 +709,7 @@ def main(args=None): tree = parse(source, name, args.mode, type_comments=args.no_type_comments, feature_version=feature_version, optimize=args.optimize) + from _colorize import can_colorize print(dump(tree, include_attributes=args.include_attributes, color=can_colorize(file=sys.stdout), indent=args.indent, show_empty=args.show_empty)) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index d67cc4dd1b19ab..dbfabded2e47aa 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -6,7 +6,6 @@ import abc from reprlib import recursive_repr lazy import copy -lazy import inspect lazy import re @@ -981,6 +980,7 @@ def __get__(self, _obj, cls): try: # In some cases fetching a signature is not possible. # But, we surely should not fail in this case. + import inspect text_sig = str(inspect.signature( cls, annotation_format=annotationlib.Format.FORWARDREF, @@ -1391,6 +1391,7 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # If this is a wrapped function, unwrap it. if not isinstance(member, type) and hasattr(member, '__wrapped__'): + import inspect member = inspect.unwrap(member) if isinstance(member, types.FunctionType): diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index a9a8cd143e0d75..1d1d2e00bd733f 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1034,6 +1034,50 @@ def test_cli_lazy_imports_none_forces_all_imports_eager(self): self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}") self.assertIn("EAGER", result.stdout) + @support.requires_resource("cpu") + def test_cli_lazy_imports_modes_import_stdlib_modules(self): + """-X lazy_imports modes should import available stdlib modules.""" + # Do not smoke-test modules with intentional import-time effects. + import_side_effect_modules = {"antigravity", "this"} + importable = [] + + for module in sorted(sys.stdlib_module_names): + if module in import_side_effect_modules: + continue + + with self.subTest(module=module): + code = f"import {module}; print({module})" + baseline = subprocess.run( + [sys.executable, "-I", "-c", code], + capture_output=True, + text=True, + timeout=60, + ) + if baseline.returncode: + # sys.stdlib_module_names includes modules for other + # platforms and optional extension modules not built here. + continue + importable.append(module) + + for mode in ("normal", "none"): + with self.subTest(module=module, mode=mode): + result = subprocess.run( + [ + sys.executable, + "-I", + "-X", + f"lazy_imports={mode}", + "-c", + code, + ], + capture_output=True, + text=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + self.assertGreater(len(importable), 100) + def test_cli_lazy_imports_normal_respects_lazy_keyword_only(self): """-X lazy_imports=normal should respect lazy keyword only.""" # Note: Use test modules instead of stdlib modules to avoid diff --git a/Misc/NEWS.d/next/Library/2026-05-03-12-00-00.gh-issue-149321.fUaxrz.rst b/Misc/NEWS.d/next/Library/2026-05-03-12-00-00.gh-issue-149321.fUaxrz.rst new file mode 100644 index 00000000000000..8fd4bf60cf32a7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-03-12-00-00.gh-issue-149321.fUaxrz.rst @@ -0,0 +1,2 @@ +Fix import cycles exposed by running standard library modules with +``-X lazy_imports=none``. From de66149f66a365625a7a5fc194935b0f6d6862b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 4 May 2026 21:58:28 +0200 Subject: [PATCH 04/10] gh-149377: update bundled pip to 26.1.1 (#149378) Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> --- Lib/ensurepip/__init__.py | 2 +- ...ne-any.whl => pip-26.1.1-py3-none-any.whl} | Bin 1812804 -> 1812777 bytes ...-05-04-19-28-48.gh-issue-149377.WNlc8Y.rst | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename Lib/ensurepip/_bundled/{pip-26.1-py3-none-any.whl => pip-26.1.1-py3-none-any.whl} (94%) create mode 100644 Misc/NEWS.d/next/Library/2026-05-04-19-28-48.gh-issue-149377.WNlc8Y.rst diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 5a55525d6bd235..9077e84e958cce 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -10,7 +10,7 @@ __all__ = ["version", "bootstrap"] -_PIP_VERSION = "26.1" +_PIP_VERSION = "26.1.1" # Directory of system wheel packages. Some Linux distribution packaging # policies recommend against bundling dependencies. For example, Fedora diff --git a/Lib/ensurepip/_bundled/pip-26.1-py3-none-any.whl b/Lib/ensurepip/_bundled/pip-26.1.1-py3-none-any.whl similarity index 94% rename from Lib/ensurepip/_bundled/pip-26.1-py3-none-any.whl rename to Lib/ensurepip/_bundled/pip-26.1.1-py3-none-any.whl index b51afa14f7c0ad88a192f4bf606775653df23681..ab0307c7716212a514231bfc5149437cfb9df70a 100644 GIT binary patch delta 50667 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 diff --git a/Misc/NEWS.d/next/Library/2026-05-04-19-28-48.gh-issue-149377.WNlc8Y.rst b/Misc/NEWS.d/next/Library/2026-05-04-19-28-48.gh-issue-149377.WNlc8Y.rst new file mode 100644 index 00000000000000..7bab1c049e67ff --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-04-19-28-48.gh-issue-149377.WNlc8Y.rst @@ -0,0 +1 @@ +Update bundled pip to 26.1.1 From e89568f0cbcfd055419cdde5ac1248cb48055f90 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 5 May 2026 02:14:45 +0500 Subject: [PATCH 05/10] GH-148726: Add heap_size to generational GC (#149195) --- Include/internal/pycore_gc.h | 6 +++++- Include/internal/pycore_interp_structs.h | 11 +++++++++-- Lib/test/test_gc.py | 9 +++++++++ Lib/test/test_gc_stats.py | 2 +- Modules/_remote_debugging/clinic/module.c.h | 3 ++- Modules/_remote_debugging/gc_stats.c | 1 + Modules/_remote_debugging/module.c | 6 ++++-- Modules/_testinternalcapi.c | 3 +-- Python/gc.c | 5 ++++- 9 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Include/internal/pycore_gc.h b/Include/internal/pycore_gc.h index e105677cd2e674..bfe52f42f1141c 100644 --- a/Include/internal/pycore_gc.h +++ b/Include/internal/pycore_gc.h @@ -223,12 +223,14 @@ static inline void _PyObject_GC_TRACK( "object is in generation which is garbage collected", filename, lineno, __func__); - PyGC_Head *generation0 = _PyInterpreterState_GET()->gc.generation0; + struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc; + PyGC_Head *generation0 = gcstate->generation0; PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev); _PyGCHead_SET_NEXT(last, gc); _PyGCHead_SET_PREV(gc, last); _PyGCHead_SET_NEXT(gc, generation0); generation0->_gc_prev = (uintptr_t)gc; + gcstate->heap_size++; #endif } @@ -263,6 +265,8 @@ static inline void _PyObject_GC_UNTRACK( _PyGCHead_SET_PREV(next, prev); gc->_gc_next = 0; gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED; + struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc; + gcstate->heap_size--; #endif } diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 86f018e328656e..2d04c173e85abe 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -191,6 +191,8 @@ struct gc_generation_stats { Py_ssize_t candidates; // Total duration of the collection in seconds: double duration; + /* heap_size on the start of the collection */ + Py_ssize_t heap_size; }; #ifdef Py_GIL_DISABLED @@ -226,7 +228,6 @@ struct _gc_runtime_state { /* linked lists of container objects */ #ifndef Py_GIL_DISABLED struct gc_generation generations[NUM_GENERATIONS]; - PyGC_Head *generation0; #else struct gc_generation young; struct gc_generation old[2]; @@ -244,6 +245,9 @@ struct _gc_runtime_state { /* a list of callbacks to be invoked when collection is performed */ PyObject *callbacks; + /* The number of live objects. */ + Py_ssize_t heap_size; + /* This is the number of objects that survived the last full collection. It approximates the number of long lived objects tracked by the GC. @@ -269,6 +273,8 @@ struct _gc_runtime_state { /* Mutex held for gc_should_collect_mem_usage(). */ PyMutex mutex; +#else + PyGC_Head *generation0; #endif }; @@ -278,7 +284,8 @@ struct _gc_runtime_state { { .threshold = 2000, }, \ { .threshold = 10, }, \ { .threshold = 10, }, \ - }, + }, \ + .heap_size = 0, #else #define GC_GENERATION_INIT \ .young = { .threshold = 2000, }, \ diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 88d265cbc21709..3fc084ea6e9c6e 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1288,6 +1288,15 @@ def test_tuple_untrack_counts(self): # Use n // 2 just in case some other objects were collected. self.assertTrue(new_count - count > (n // 2)) + @requires_gil_enabled('need generational GC') + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") + def test_heap_size(self): + count = _testinternalcapi.get_tracked_heap_size() + l = [] + self.assertEqual(count + 1, _testinternalcapi.get_tracked_heap_size()) + del l + self.assertEqual(count, _testinternalcapi.get_tracked_heap_size()) + class GCCallbackTests(unittest.TestCase): def setUp(self): diff --git a/Lib/test/test_gc_stats.py b/Lib/test/test_gc_stats.py index 59365ad45b32c9..bd75924397e76e 100644 --- a/Lib/test/test_gc_stats.py +++ b/Lib/test/test_gc_stats.py @@ -22,7 +22,7 @@ GC_STATS_FIELDS = ( "gen", "iid", "ts_start", "ts_stop", "collections", "collected", - "uncollectable", "candidates", "duration") + "uncollectable", "candidates", "heap_size", "duration") def get_interpreter_identifiers(gc_stats) -> tuple[int,...]: diff --git a/Modules/_remote_debugging/clinic/module.c.h b/Modules/_remote_debugging/clinic/module.c.h index 179a7b97dd4e2f..1133db808efaec 100644 --- a/Modules/_remote_debugging/clinic/module.c.h +++ b/Modules/_remote_debugging/clinic/module.c.h @@ -601,6 +601,7 @@ PyDoc_STRVAR(_remote_debugging_GCMonitor_get_gc_stats__doc__, " - collected: Total number of collected objects.\n" " - uncollectable: Total number of uncollectable objects.\n" " - candidates: Total objects considered and traversed.\n" +" - heap_size: number of live objects.\n" " - duration: Total collection time, in seconds.\n" "\n" "Raises:\n" @@ -1563,4 +1564,4 @@ _remote_debugging_get_gc_stats(PyObject *module, PyObject *const *args, Py_ssize exit: return return_value; } -/*[clinic end generated code: output=1151e58683dab9f4 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=36674f4cb8a653f3 input=a9049054013a1b77]*/ diff --git a/Modules/_remote_debugging/gc_stats.c b/Modules/_remote_debugging/gc_stats.c index 852dc866153192..d5d05edb8ecf5e 100644 --- a/Modules/_remote_debugging/gc_stats.c +++ b/Modules/_remote_debugging/gc_stats.c @@ -53,6 +53,7 @@ read_gc_stats(struct gc_stats *stats, int64_t iid, PyObject *result, SET_FIELD(PyLong_FromSsize_t, items->collected); SET_FIELD(PyLong_FromSsize_t, items->uncollectable); SET_FIELD(PyLong_FromSsize_t, items->candidates); + SET_FIELD(PyLong_FromSsize_t, items->heap_size); SET_FIELD(PyFloat_FromDouble, items->duration); diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index c840c59971c478..c694e587e7cccb 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -143,6 +143,7 @@ static PyStructSequence_Field GCStatsInfo_fields[] = { {"collected", "Total number of collected objects"}, {"uncollectable", "Total number of uncollectable objects"}, {"candidates", "Total objects considered and traversed"}, + {"heap_size", "Number of live objects"}, {"duration", "Total collection time, in seconds"}, {NULL} }; @@ -151,7 +152,7 @@ PyStructSequence_Desc GCStatsInfo_desc = { "_remote_debugging.GCStatsInfo", "Information about a garbage collector stats sample", GCStatsInfo_fields, - 9 + 10 }; /* ============================================================================ @@ -1225,6 +1226,7 @@ Returns a list of GCStatsInfo objects with GC statistics data. - collected: Total number of collected objects. - uncollectable: Total number of uncollectable objects. - candidates: Total objects considered and traversed. + - heap_size: number of live objects. - duration: Total collection time, in seconds. Raises: @@ -1235,7 +1237,7 @@ Returns a list of GCStatsInfo objects with GC statistics data. static PyObject * _remote_debugging_GCMonitor_get_gc_stats_impl(GCMonitorObject *self, int all_interpreters) -/*[clinic end generated code: output=f73f365725224f7a input=09e647719c65f9e4]*/ +/*[clinic end generated code: output=f73f365725224f7a input=12f7c1a288cf2741]*/ { RemoteDebuggingState *st = RemoteDebugging_GetStateFromType(Py_TYPE(self)); return get_gc_stats(&self->offsets, all_interpreters, st->GCStatsInfo_Type); diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index a07675bb66d8cc..d85b9eb5f7da89 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2731,8 +2731,7 @@ has_deferred_refcount(PyObject *self, PyObject *op) static PyObject * get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored)) { - // Generational GC doesn't track heap_size, return -1. - return PyLong_FromInt64(-1); + return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size); } static PyObject * diff --git a/Python/gc.c b/Python/gc.c index 134da107e1b61d..54ac1b089e503d 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -1405,13 +1405,13 @@ add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats) memcpy(cur_stats, prev_stats, sizeof(struct gc_generation_stats)); cur_stats->ts_start = stats->ts_start; - cur_stats->collections += 1; cur_stats->collected += stats->collected; cur_stats->uncollectable += stats->uncollectable; cur_stats->candidates += stats->candidates; cur_stats->duration += stats->duration; + cur_stats->heap_size = stats->heap_size; /* Publish ts_stop last so remote readers do not select a partially updated stats record as the latest collection. */ cur_stats->ts_stop = stats->ts_stop; @@ -1471,6 +1471,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) invoke_gc_callback(tstate, "start", generation, &stats); } + stats.heap_size = gcstate->heap_size; // ignore error: don't interrupt the GC if reading the clock fails (void)PyTime_PerfCounterRaw(&stats.ts_start); if (gcstate->debug & _PyGC_DEBUG_STATS) { @@ -2097,6 +2098,8 @@ PyObject_GC_Del(void *op) PyGC_Head *g = AS_GC(op); if (_PyObject_GC_IS_TRACKED(op)) { gc_list_remove(g); + GCState *gcstate = get_gc_state(); + gcstate->heap_size--; #ifdef Py_DEBUG PyObject *exc = PyErr_GetRaisedException(); if (PyErr_WarnExplicitFormat(PyExc_ResourceWarning, "gc", 0, From 7acee984e8e2a88bcfb7a83e9c472902e340e5be Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Mon, 4 May 2026 14:38:07 -0700 Subject: [PATCH 06/10] gh-146406: Add cross-language method suggestions for builtin AttributeError (#146407) When Levenshtein-based suggestions find no match for an AttributeError on list, str, or dict, check a static table of common method names from JavaScript, Java, C#, and Ruby. For example, [].push() now suggests .append(), "".toUpperCase() suggests .upper(), and {}.keySet() suggests .keys(). The list.add() case suggests using a set instead of suggesting .append(), since .add() is a set method and the user may have passed a list where a set was expected (per discussion with Serhiy Storchaka, Terry Reedy, and Paul Moore). Design: flat (type, attr) -> suggestion text table, no runtime introspection. Only exact builtin types are matched to avoid false positives on subclasses. Discussion: https://discuss.python.org/t/106632 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Victor Stinner Co-authored-by: Claude Opus 4.6 (1M context) --- Doc/whatsnew/3.15.rst | 41 ++++++++ Lib/test/test_traceback.py | 89 ++++++++++++++++++ Lib/traceback.py | 94 +++++++++++++++++-- ...-03-25-07-17-41.gh-issue-146406.ydsmqe.rst | 6 ++ 4 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b215c56408503a..9409b41f574222 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -492,6 +492,47 @@ Improved error messages ^^^^^^^^^^^^^^ AttributeError: 'Container' object has no attribute 'area'. Did you mean '.inner.area' instead of '.area'? +* When an :exc:`AttributeError` on a builtin type has no close match via + Levenshtein distance, the error message now checks a static table of common + method names from other languages (JavaScript, Java, Ruby, C#) and suggests + the Python equivalent: + + .. doctest:: + + >>> [1, 2, 3].push(4) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'? + + >>> 'hello'.toUpperCase() # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'? + + When the Python equivalent is a language construct rather than a method, + the hint describes the construct directly: + + .. doctest:: + + >>> {}.put("a", 1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v. + + When a mutable method is called on an immutable type, the hint suggests + the mutable counterpart: + + .. doctest:: + + >>> (1, 2, 3).append(4) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object? + + These hints also work for subclasses of builtin types. + + (Contributed by Matt Van Horn in :gh:`146406`.) + * The interpreter now tries to provide a suggestion when :func:`delattr` fails due to a missing attribute. When an attribute name that closely resembles an existing attribute is used, diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 909808825f055e..6624191f164bc1 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4565,6 +4565,95 @@ def __init__(self): actual = self.get_suggestion(Outer(), 'target') self.assertIn("'.normal.target'", actual) + @force_not_colorized + def test_cross_language(self): + cases = [ + # (type, attr, hint_attr) + (list, 'push', 'append'), + (list, 'concat', 'extend'), + (list, 'addAll', 'extend'), + (str, 'toUpperCase', 'upper'), + (str, 'toLowerCase', 'lower'), + (str, 'trimStart', 'lstrip'), + (str, 'trimEnd', 'rstrip'), + (dict, 'keySet', 'keys'), + (dict, 'entrySet', 'items'), + (dict, 'entries', 'items'), + (dict, 'putAll', 'update'), + ] + for test_type, attr, hint_attr in cases: + with self.subTest(type=test_type.__name__, attr=attr): + obj = test_type() + actual = self.get_suggestion(obj, attr) + self.assertEndsWith(actual, f"Did you mean '.{hint_attr}'?") + + cases = [ + # (type, attr, hint) + (list, 'contains', "Use 'x in list'."), + (list, 'add', "Did you mean to use a 'set' object?"), + (dict, 'put', "Use d[k] = v."), + ] + for test_type, attr, expected in cases: + with self.subTest(type=test_type, attr=attr): + obj = test_type() + actual = self.get_suggestion(obj, attr) + self.assertEndsWith(actual, expected) + + @force_not_colorized + def test_cross_language_levenshtein_fallback(self): + # When no cross-language entry exists, Levenshtein still works + # (e.g., trim->strip is not in the table but Levenshtein catches it) + actual = self.get_suggestion('', 'trim') + self.assertIn("strip", actual) + + @force_not_colorized + def test_cross_language_no_hint_for_unknown_attr(self): + actual = self.get_suggestion([], 'completely_unknown_method') + self.assertNotIn("Did you mean", actual) + + @force_not_colorized + def test_cross_language_works_for_subclasses(self): + # isinstance() check means subclasses also get hints + class MyList(list): + pass + actual = self.get_suggestion(MyList(), 'push') + self.assertEndsWith(actual, "Did you mean '.append'?") + + class MyDict(dict): + pass + actual = self.get_suggestion(MyDict(), 'keySet') + self.assertEndsWith(actual, "Did you mean '.keys'?") + + @force_not_colorized + def test_cross_language_mutable_on_immutable(self): + # Mutable method on immutable type suggests the mutable counterpart + cases = [ + (tuple, 'append', "Did you mean to use a 'list' object?"), + (tuple, 'extend', "Did you mean to use a 'list' object?"), + (tuple, 'insert', "Did you mean to use a 'list' object?"), + (tuple, 'remove', "Did you mean to use a 'list' object?"), + (frozenset, 'add', "Did you mean to use a 'set' object?"), + (frozenset, 'discard', "Did you mean to use a 'set' object?"), + (frozenset, 'remove', "Did you mean to use a 'set' object?"), + (frozenset, 'update', "Did you mean to use a 'set' object?"), + (frozendict, 'update', "Did you mean to use a 'dict' object?"), + ] + for test_type, attr, expected in cases: + with self.subTest(type=test_type.__name__, attr=attr): + obj = test_type() + actual = self.get_suggestion(obj, attr) + self.assertEndsWith(actual, expected) + + @force_not_colorized + def test_cross_language_float_bitwise(self): + # Bitwise operators on float suggest using int + cases = ['__or__', '__and__', '__xor__', '__lshift__', '__rshift__'] + for attr in cases: + with self.subTest(attr=attr): + actual = self.get_suggestion(1.0, attr) + self.assertIn("'int'", actual) + self.assertIn("Bitwise operators", actual) + def make_module(self, code): tmpdir = Path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, tmpdir) diff --git a/Lib/traceback.py b/Lib/traceback.py index 343d0e5f108c35..66e88d0a588af3 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1187,12 +1187,20 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, elif exc_type and issubclass(exc_type, AttributeError) and \ getattr(exc_value, "name", None) is not None: wrong_name = getattr(exc_value, "name", None) - suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) - if suggestion: - if suggestion.isascii(): - self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" - else: - self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?" + # Check cross-language/wrong-type hints first (more specific), + # then fall back to Levenshtein distance suggestions. + hint = None + if hasattr(exc_value, 'obj'): + hint = _get_cross_language_hint(exc_value.obj, wrong_name) + if hint: + self._str += f". {hint}" + else: + suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name) + if suggestion: + if suggestion.isascii(): + self._str += f". Did you mean '.{suggestion}' instead of '.{wrong_name}'?" + else: + self._str += f". Did you mean '.{suggestion}' ({suggestion!a}) instead of '.{wrong_name}' ({wrong_name!a})?" elif exc_type and issubclass(exc_type, NameError) and \ getattr(exc_value, "name", None) is not None: wrong_name = getattr(exc_value, "name", None) @@ -1689,6 +1697,62 @@ def print(self, *, file=None, chain=True, **kwargs): _MOVE_COST = 2 _CASE_COST = 1 +# Cross-language method suggestions for builtin types. +# Consulted as a fallback when Levenshtein-based suggestions find no match. +# +# Inclusion criteria: +# +# 1. Must have evidence of real cross-language confusion (Stack Overflow +# traffic, bug reports in production repos, developer survey data). +# 2. Must not be catchable by Levenshtein distance (too different from +# the correct Python method name). +# +# Each entry maps a wrong method name to a list of (type, suggestion, is_raw) +# tuples. The lookup checks isinstance() so subclasses are also matched. +# If is_raw is False, the suggestion is wrapped in "Did you mean '.X'?". +# If is_raw is True, the suggestion is rendered as-is. +# +# See https://github.com/python/cpython/issues/146406. +_CROSS_LANGUAGE_HINTS = frozendict({ + # list -- JavaScript/Ruby equivalents + "push": ((list, "append", False),), + "concat": ((list, "extend", False),), + # list -- Java/C# equivalents + "addAll": ((list, "extend", False),), + "contains": ((list, "Use 'x in list'.", True),), + # list -- wrong-type suggestion (user expected a set) + "add": ((list, "Did you mean to use a 'set' object?", True), + (frozenset, "Did you mean to use a 'set' object?", True)), + # str -- JavaScript equivalents + "toUpperCase": ((str, "upper", False),), + "toLowerCase": ((str, "lower", False),), + "trimStart": ((str, "lstrip", False),), + "trimEnd": ((str, "rstrip", False),), + # dict -- Java/JavaScript equivalents + "keySet": ((dict, "keys", False),), + "entrySet": ((dict, "items", False),), + "entries": ((dict, "items", False),), + "putAll": ((dict, "update", False),), + "put": ((dict, "Use d[k] = v.", True),), + # tuple -- mutable method on immutable type (user expected a list) + "append": ((tuple, "Did you mean to use a 'list' object?", True),), + "extend": ((tuple, "Did you mean to use a 'list' object?", True),), + "insert": ((tuple, "Did you mean to use a 'list' object?", True),), + "remove": ((tuple, "Did you mean to use a 'list' object?", True), + (frozenset, "Did you mean to use a 'set' object?", True)), + # frozenset -- mutable method on immutable type (user expected a set) + "discard": ((frozenset, "Did you mean to use a 'set' object?", True),), + # frozendict -- mutable method on immutable type (user expected a dict) + "update": ((frozenset, "Did you mean to use a 'set' object?", True), + (frozendict, "Did you mean to use a 'dict' object?", True)), + # float -- bitwise operators belong to int + "__or__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),), + "__and__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),), + "__xor__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),), + "__lshift__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),), + "__rshift__": ((float, "Did you mean to use an 'int' object? Bitwise operators are not supported by 'float'.", True),), +}) + def _substitution_cost(ch_a, ch_b): if ch_a == ch_b: @@ -1751,6 +1815,24 @@ def _check_for_nested_attribute(obj, wrong_name, attrs): return None +def _get_cross_language_hint(obj, wrong_name): + """Check if wrong_name is a common method name from another language, + a mutable method on an immutable type, or a method tried on None. + + Uses isinstance() so subclasses of builtin types also get hints. + Returns a formatted hint string, or None. + """ + entries = _CROSS_LANGUAGE_HINTS.get(wrong_name) + if entries is None: + return None + for check_type, hint, is_raw in entries: + if isinstance(obj, check_type): + if is_raw: + return hint + return f"Did you mean '.{hint}'?" + return None + + def _get_safe___dir__(obj): # Use obj.__dir__() to avoid a TypeError when calling dir(obj). # See gh-131001 and gh-139933. diff --git a/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst b/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst new file mode 100644 index 00000000000000..0f8107d2383ba9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-25-07-17-41.gh-issue-146406.ydsmqe.rst @@ -0,0 +1,6 @@ +Cross-language method suggestions are now shown for :exc:`AttributeError` on +builtin types and their subclasses. +For example, ``[].push()`` suggests ``append``, +``(1,2).append(3)`` suggests using a ``list``, +``None.keys()`` suggests expecting a ``dict``, +and ``1.0.__or__`` suggests using an ``int``. From 53a7f76501923059188922be231db855265fe9a4 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Mon, 4 May 2026 14:51:48 -0700 Subject: [PATCH 07/10] GH-130750: Restore quoting of choices in argparse error messages to match documentation and improve clarity (#144983) --- Lib/argparse.py | 2 +- Lib/test/test_argparse.py | 16 ++++++++-------- Lib/test/test_timeit.py | 2 +- ...026-02-19-04-40-57.gh-issue-130750.0hW52O.rst | 2 ++ 4 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-19-04-40-57.gh-issue-130750.0hW52O.rst diff --git a/Lib/argparse.py b/Lib/argparse.py index d91707d9eec546..9ea6b32163a9d1 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2758,7 +2758,7 @@ def _check_value(self, action, value): if value not in choices: args = {'value': str(value), - 'choices': ', '.join(map(str, action.choices))} + 'choices': ', '.join(repr(str(choice)) for choice in action.choices)} msg = _('invalid choice: %(value)r (choose from %(choices)s)') if self.suggest_on_error and isinstance(value, str): diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index e0c32976fd6f0d..4799c9947dd3ee 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1123,7 +1123,7 @@ def test_invalid_enum_value_raises_error(self): parser.add_argument('--color', choices=self.Color) self.assertRaisesRegex( argparse.ArgumentError, - r"invalid choice: 'yellow' \(choose from red, green, blue\)", + r"invalid choice: 'yellow' \(choose from 'red', 'green', 'blue'\)", parser.parse_args, ['--color', 'yellow'], ) @@ -2392,7 +2392,7 @@ def test_wrong_argument_error_with_suggestions(self): with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('bazz',)) self.assertIn( - "error: argument foo: invalid choice: 'bazz', maybe you meant 'baz'? (choose from bar, baz)", + "error: argument foo: invalid choice: 'bazz', maybe you meant 'baz'? (choose from 'bar', 'baz')", excinfo.exception.stderr ) @@ -2402,7 +2402,7 @@ def test_wrong_argument_error_no_suggestions(self): with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('bazz',)) self.assertIn( - "error: argument foo: invalid choice: 'bazz' (choose from bar, baz)", + "error: argument foo: invalid choice: 'bazz' (choose from 'bar', 'baz')", excinfo.exception.stderr, ) @@ -2415,7 +2415,7 @@ def test_wrong_argument_subparsers_with_suggestions(self): parser.parse_args(('baz',)) self.assertIn( "error: argument {foo,bar}: invalid choice: 'baz', maybe you meant" - " 'bar'? (choose from foo, bar)", + " 'bar'? (choose from 'foo', 'bar')", excinfo.exception.stderr, ) @@ -2427,7 +2427,7 @@ def test_wrong_argument_subparsers_no_suggestions(self): with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('baz',)) self.assertIn( - "error: argument {foo,bar}: invalid choice: 'baz' (choose from foo, bar)", + "error: argument {foo,bar}: invalid choice: 'baz' (choose from 'foo', 'bar')", excinfo.exception.stderr, ) @@ -2438,7 +2438,7 @@ def test_wrong_argument_with_suggestion_explicit(self): parser.parse_args(('bazz',)) self.assertIn( "error: argument foo: invalid choice: 'bazz', maybe you meant" - " 'baz'? (choose from bar, baz)", + " 'baz'? (choose from 'bar', 'baz')", excinfo.exception.stderr, ) @@ -2458,7 +2458,7 @@ def test_suggestions_choices_int(self): with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('3',)) self.assertIn( - "error: argument foo: invalid choice: '3' (choose from 1, 2)", + "error: argument foo: invalid choice: '3' (choose from '1', '2')", excinfo.exception.stderr, ) @@ -2468,7 +2468,7 @@ def test_suggestions_choices_mixed_types(self): with self.assertRaises(ArgumentParserError) as excinfo: parser.parse_args(('3',)) self.assertIn( - "error: argument foo: invalid choice: '3' (choose from 1, 2)", + "error: argument foo: invalid choice: '3' (choose from '1', '2')", excinfo.exception.stderr, ) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 81f1a9c97393d1..a2a09f9de61490 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -359,7 +359,7 @@ def test_main_with_time_unit(self): seconds_per_increment=0.003, switches=["-u", "parsec"] ) self.assertIn( - "choose from nsec, usec, msec, sec", error_stringio.getvalue() + "choose from 'nsec', 'usec', 'msec', 'sec'", error_stringio.getvalue() ) def test_main_exception(self): diff --git a/Misc/NEWS.d/next/Library/2026-02-19-04-40-57.gh-issue-130750.0hW52O.rst b/Misc/NEWS.d/next/Library/2026-02-19-04-40-57.gh-issue-130750.0hW52O.rst new file mode 100644 index 00000000000000..8bca48ab159476 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-19-04-40-57.gh-issue-130750.0hW52O.rst @@ -0,0 +1,2 @@ +Restore quoting of choices in :mod:`argparse` error messages for improved clarity and consistency with documentation. + From ef6f0635ce07e6785a78aef0c58649c2e8e832ba Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 5 May 2026 01:23:18 +0300 Subject: [PATCH 08/10] gh-142389: Add backticks to stdlib argparse help to display in colour (#149384) Co-authored-by: Savannah Ostrowski --- Lib/ast.py | 6 +-- Lib/asyncio/__main__.py | 1 - Lib/calendar.py | 2 +- Lib/code.py | 2 +- Lib/compileall.py | 23 +++++------ Lib/dis.py | 2 +- Lib/doctest.py | 6 +-- Lib/ensurepip/__init__.py | 2 +- Lib/gzip.py | 5 +-- Lib/http/server.py | 2 +- Lib/inspect.py | 4 +- Lib/json/tool.py | 8 ++-- Lib/mimetypes.py | 2 +- Lib/pdb.py | 3 +- Lib/pickle.py | 1 - Lib/pickletools.py | 2 +- Lib/platform.py | 2 +- Lib/profiling/sampling/cli.py | 26 ++++++------ Lib/py_compile.py | 2 +- Lib/random.py | 12 +++--- Lib/sqlite3/__main__.py | 9 ++--- Lib/tarfile.py | 2 +- Lib/test/libregrtest/cmdline.py | 40 +++++++++---------- Lib/timeit.py | 4 +- Lib/tokenize.py | 2 +- Lib/trace.py | 32 +++++++-------- Lib/unittest/main.py | 14 +++---- Lib/uuid.py | 7 ++-- Lib/venv/__init__.py | 1 - Lib/webbrowser.py | 2 +- Lib/zipapp.py | 6 +-- Lib/zipfile/__init__.py | 4 +- ...-05-05-00-30-04.gh-issue-142389.4daLzc.rst | 2 + 33 files changed, 115 insertions(+), 123 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-05-00-30-04.gh-issue-142389.4daLzc.rst diff --git a/Lib/ast.py b/Lib/ast.py index a7997c4b740635..4f88a554344cc9 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -666,7 +666,7 @@ def main(args=None): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('infile', nargs='?', default='-', - help='the file to parse; defaults to stdin') + help='the file to parse; defaults to `stdin`') parser.add_argument('-m', '--mode', default='exec', choices=('exec', 'single', 'eval', 'func_type'), help='specify what kind of code must be parsed') @@ -679,8 +679,8 @@ def main(args=None): help='indentation of nodes (number of spaces)') parser.add_argument('--feature-version', type=str, default=None, metavar='VERSION', - help='Python version in the format 3.x ' - '(for example, 3.10)') + help='Python version in the format `3.x` ' + '(for example, `3.10`)') parser.add_argument('-O', '--optimize', type=int, default=-1, metavar='LEVEL', help='optimization level for parser') diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 37eba9657ac5a8..7f0565d0b8ddc7 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -159,7 +159,6 @@ def interrupt(self) -> None: parser = argparse.ArgumentParser( prog="python3 -m asyncio", description="Interactive asyncio shell and CLI tools", - color=True, ) subparsers = parser.add_subparsers(help="sub-commands", dest="command") ps = subparsers.add_parser( diff --git a/Lib/calendar.py b/Lib/calendar.py index fa9775ab040b14..92fe6b7723fe26 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -922,7 +922,7 @@ def main(args=None): "-t", "--type", default="text", choices=("text", "html"), - help="output type (text or html)" + help="output type (`text` or `html`)" ) parser.add_argument( "-f", "--first-weekday", diff --git a/Lib/code.py b/Lib/code.py index f7e275d8801b7c..df1d7199e33934 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -385,7 +385,7 @@ def interact(banner=None, readfunc=None, local=None, exitmsg=None, local_exit=Fa if __name__ == "__main__": import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('-q', action='store_true', help="don't print version and copyright messages") args = parser.parse_args() diff --git a/Lib/compileall.py b/Lib/compileall.py index c452aed135838f..812a496611e043 100644 --- a/Lib/compileall.py +++ b/Lib/compileall.py @@ -326,7 +326,6 @@ def main(): parser = argparse.ArgumentParser( description='Utilities to support installing Python libraries.', - color=True, ) parser.add_argument('-l', action='store_const', const=0, default=None, dest='maxlevels', @@ -338,10 +337,10 @@ def main(): parser.add_argument('-f', action='store_true', dest='force', help='force rebuild even if timestamps are up to date') parser.add_argument('-q', action='count', dest='quiet', default=0, - help='output only error messages; -qq will suppress ' + help='output only error messages; `-qq` will suppress ' 'the error messages as well.') parser.add_argument('-b', action='store_true', dest='legacy', - help='use legacy (pre-PEP3147) compiled file locations') + help='use legacy (pre-PEP 3147) compiled file locations') parser.add_argument('-d', metavar='DESTDIR', dest='ddir', default=None, help=('directory to prepend to file paths for use in ' 'compile-time tracebacks and in runtime ' @@ -367,28 +366,28 @@ def main(): 'of each file considered for compilation')) parser.add_argument('-i', metavar='FILE', dest='flist', help=('add all the files and directories listed in ' - 'FILE to the list considered for compilation; ' - 'if "-", names are read from stdin')) + '`FILE` to the list considered for compilation; ' + 'if `"-"`, names are read from `stdin`')) parser.add_argument('compile_dest', metavar='FILE|DIR', nargs='*', help=('zero or more file and directory names ' 'to compile; if no arguments given, defaults ' - 'to the equivalent of -l sys.path')) + 'to the equivalent of `-l` `sys.path`')) parser.add_argument('-j', '--workers', default=1, type=int, help='Run compileall concurrently') invalidation_modes = [mode.name.lower().replace('_', '-') for mode in py_compile.PycInvalidationMode] parser.add_argument('--invalidation-mode', choices=sorted(invalidation_modes), - help=('set .pyc invalidation mode; defaults to ' - '"checked-hash" if the SOURCE_DATE_EPOCH ' + help=('set `.pyc` invalidation mode; defaults to ' + '`"checked-hash"` if the `SOURCE_DATE_EPOCH` ' 'environment variable is set, and ' - '"timestamp" otherwise.')) + '`"timestamp"` otherwise.')) parser.add_argument('-o', action='append', type=int, dest='opt_levels', help=('Optimization levels to run compilation with. ' - 'Default is -1 which uses the optimization level ' - 'of the Python interpreter itself (see -O).')) + 'Default is `-1` which uses the optimization level ' + 'of the Python interpreter itself (see `-O`).')) parser.add_argument('-e', metavar='DIR', dest='limit_sl_dest', - help='Ignore symlinks pointing outsite of the DIR') + help='Ignore symlinks pointing outsite of the `DIR`') parser.add_argument('--hardlink-dupes', action='store_true', dest='hardlink_dupes', help='Hardlink duplicated pyc files') diff --git a/Lib/dis.py b/Lib/dis.py index 64f3450da3071b..d60507ae473453 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -1146,7 +1146,7 @@ def dis(self): def main(args=None): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('-C', '--show-caches', action='store_true', help='show inline caches') parser.add_argument('-O', '--show-offsets', action='store_true', diff --git a/Lib/doctest.py b/Lib/doctest.py index 0fcfa1e3e97144..05acac1745ace9 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -2951,7 +2951,7 @@ def get(self): def _test(): import argparse - parser = argparse.ArgumentParser(description="doctest runner", color=True) + parser = argparse.ArgumentParser(description="doctest runner") parser.add_argument('-v', '--verbose', action='store_true', default=False, help='print very verbose output for all tests') parser.add_argument('-o', '--option', action='append', @@ -2961,8 +2961,8 @@ def _test(): ' than once to apply multiple options')) parser.add_argument('-f', '--fail-fast', action='store_true', help=('stop running tests after first failure (this' - ' is a shorthand for -o FAIL_FAST, and is' - ' in addition to any other -o options)')) + ' is a shorthand for `-o FAIL_FAST`, and is' + ' in addition to any other `-o` options)')) parser.add_argument('file', nargs='+', help='file containing the tests to run') args = parser.parse_args() diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 9077e84e958cce..ab6b95e0ba60c0 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -217,7 +217,7 @@ def _uninstall_helper(*, verbosity=0): def _main(argv=None): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument( "--version", action="version", diff --git a/Lib/gzip.py b/Lib/gzip.py index 8a2faf846bf894..971063aa24f871 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -664,15 +664,14 @@ def decompress(data): def main(): from argparse import ArgumentParser parser = ArgumentParser(description= - "A simple command line interface for the gzip module: act like gzip, " + "A simple command line interface for the `gzip` module: act like `gzip`, " "but do not delete the input file.", - color=True, ) group = parser.add_mutually_exclusive_group() group.add_argument('--fast', action='store_true', help='compress faster') group.add_argument('--best', action='store_true', help='compress better') group.add_argument("-d", "--decompress", action="store_true", - help="act like gunzip instead of gzip") + help="act like `gunzip` instead of `gzip`") parser.add_argument("args", nargs="*", default=["-"], metavar='file') args = parser.parse_args() diff --git a/Lib/http/server.py b/Lib/http/server.py index 16ea7f3f93693f..ebc85052aecb90 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1081,7 +1081,7 @@ def _main(args=None): import argparse import contextlib - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('-b', '--bind', metavar='ADDRESS', help='bind to this address ' '(default: all interfaces)') diff --git a/Lib/inspect.py b/Lib/inspect.py index af304f186a69bc..9eb87b0d277918 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -3445,11 +3445,11 @@ def _main(): import argparse import importlib - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument( 'object', help="The object to be analysed. " - "It supports the 'module:qualname' syntax") + "It supports the `module:qualname` syntax") parser.add_argument( '-d', '--details', action='store_true', help='Display info about the module rather than its source code') diff --git a/Lib/json/tool.py b/Lib/json/tool.py index e56a601c581ae5..6385d971f73304 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -44,13 +44,13 @@ def _replace_match_callback(match): def main(): description = ('A simple command line interface for json module ' 'to validate and pretty-print JSON objects.') - parser = argparse.ArgumentParser(description=description, color=True) + parser = argparse.ArgumentParser(description=description) parser.add_argument('infile', nargs='?', help='a JSON file to be validated or pretty-printed; ' - 'defaults to stdin', + 'defaults to `stdin`', default='-') parser.add_argument('outfile', nargs='?', - help='write the output of infile to outfile', + help='write the output of `infile` to `outfile`', default=None) parser.add_argument('--sort-keys', action='store_true', default=False, help='sort the output of dictionaries alphabetically by key') @@ -58,7 +58,7 @@ def main(): help='disable escaping of non-ASCII characters') parser.add_argument('--json-lines', action='store_true', default=False, help='parse input using the JSON Lines format. ' - 'Use with --no-indent or --compact to produce valid JSON Lines output.') + 'Use with `--no-indent` or `--compact` to produce valid JSON Lines output.') group = parser.add_mutually_exclusive_group() group.add_argument('--indent', default=4, type=int, help='separate items with newlines and use this number ' diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index ad18db09f6b340..6d9278bccf927e 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -712,7 +712,7 @@ def _parse_args(args): from argparse import ArgumentParser parser = ArgumentParser( - description='map filename extensions to MIME types', color=True + description='map filename extensions to MIME types', ) parser.add_argument( '-e', '--extension', diff --git a/Lib/pdb.py b/Lib/pdb.py index 569599d349c49e..bdc9caf80ec26e 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -3757,13 +3757,12 @@ def parse_args(): description=_usage, formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False, - color=True, ) # Get all the commands out first. For backwards compatibility, we allow # -c commands to be after the target. parser.add_argument('-c', '--command', action='append', default=[], metavar='command', dest='commands', - help='pdb commands to execute as if given in a .pdbrc file') + help='pdb commands to execute as if given in a `.pdbrc` file') opts, args = parser.parse_known_args() diff --git a/Lib/pickle.py b/Lib/pickle.py index 95836afdc2b43e..f92b1fde768fc7 100644 --- a/Lib/pickle.py +++ b/Lib/pickle.py @@ -1937,7 +1937,6 @@ def _main(args=None): import pprint parser = argparse.ArgumentParser( description='display contents of the pickle files', - color=True, ) parser.add_argument( 'pickle_file', diff --git a/Lib/pickletools.py b/Lib/pickletools.py index 976e218db19298..a9711538dae342 100644 --- a/Lib/pickletools.py +++ b/Lib/pickletools.py @@ -2899,7 +2899,7 @@ def _main(args=None): help='preserve memo between disassemblies') parser.add_argument( '-l', '--indentlevel', default=4, type=int, - help='the number of blanks by which to indent a new MARK level') + help='the number of blanks by which to indent a new `MARK` level') parser.add_argument( '-a', '--annotate', action='store_true', help='annotate each line with a short opcode description') diff --git a/Lib/platform.py b/Lib/platform.py index 9d7aa5c66a91cb..36489d4fdd98ae 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1400,7 +1400,7 @@ def invalidate_caches(): def _parse_args(args: list[str] | None): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument("args", nargs="*", choices=["nonaliased", "terse"]) parser.add_argument( "--terse", diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index bc879c43e15965..9900415ae8a927 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -389,13 +389,13 @@ def _add_sampling_options(parser): sampling_group.add_argument( "--native", action="store_true", - help='Include artificial "" frames to denote calls to non-Python code', + help='Include artificial `` frames to denote calls to non-Python code', ) sampling_group.add_argument( "--no-gc", action="store_false", dest="gc", - help='Don\'t include artificial "" frames to denote active garbage collection', + help='Don\'t include artificial `` frames to denote active garbage collection', ) sampling_group.add_argument( "--opcodes", @@ -432,14 +432,14 @@ def _add_mode_options(parser): help="Sampling mode: wall (all samples), cpu (only samples when thread is on CPU), " "gil (only samples when thread holds the GIL), " "exception (only samples when thread has an active exception). " - "Incompatible with --async-aware", + "Incompatible with `--async-aware`", ) mode_group.add_argument( "--async-mode", choices=["running", "all"], default="running", help='Async profiling mode: "running" (only running task) ' - 'or "all" (all tasks including waiting). Requires --async-aware', + 'or "all" (all tasks including waiting). Requires `--async-aware`', ) @@ -486,7 +486,7 @@ def _add_format_options(parser, include_compression=True, include_binary=True): "--diff-flamegraph", metavar="BASELINE", action=DiffFlamegraphAction, - help="Generate differential flamegraph comparing current profile to BASELINE binary file", + help="Generate differential flamegraph comparing current profile to `BASELINE` binary file", ) if include_binary: format_group.add_argument( @@ -494,7 +494,7 @@ def _add_format_options(parser, include_compression=True, include_binary=True): action="store_const", const="binary", dest="format", - help="Generate high-performance binary format (use 'replay' command to convert)", + help="Generate high-performance binary format (use `replay` command to convert)", ) parser.set_defaults(format="pstats", diff_baseline=None) @@ -510,14 +510,14 @@ def _add_format_options(parser, include_compression=True, include_binary=True): "-o", "--output", dest="outfile", - help="Output path (default: stdout for pstats text; with -o, pstats is binary). " - "Auto-generated for other formats. For heatmap: directory name (default: heatmap_PID)", + help="Output path (default: `stdout` for `pstats` text; with `-o`, `pstats` is binary). " + "Auto-generated for other formats. For heatmap: directory name (default: `heatmap_PID`)", ) output_group.add_argument( "--browser", action="store_true", help="Automatically open HTML output (flamegraph, heatmap) in browser. " - "When using --subprocesses, only the main process opens the browser", + "When using `--subprocesses`, only the main process opens the browser", ) @@ -564,13 +564,13 @@ def _add_dump_options(parser): dump_group.add_argument( "--native", action="store_true", - help='Include artificial "" frames to denote calls to non-Python code', + help='Include artificial `` frames to denote calls to non-Python code', ) dump_group.add_argument( "--no-gc", action="store_false", dest="gc", - help='Don\'t include artificial "" frames to denote active garbage collection', + help='Don\'t include artificial `` frames to denote active garbage collection', ) dump_group.add_argument( "--opcodes", @@ -588,7 +588,7 @@ def _add_dump_options(parser): default=argparse.SUPPRESS, help='Async stack mode: "running" (only running task) ' 'or "all" (all tasks including waiting, default for dump). ' - "Requires --async-aware", + "Requires `--async-aware`", ) dump_group.add_argument( "--blocking", @@ -998,7 +998,7 @@ def _main(): "-m", "--module", action="store_true", - help="Run target as a module (like python -m)", + help="Run target as a module (like `python -m`)", ) run_parser.add_argument( "target", diff --git a/Lib/py_compile.py b/Lib/py_compile.py index 694ea9304da9f9..7ca479141e01e4 100644 --- a/Lib/py_compile.py +++ b/Lib/py_compile.py @@ -177,7 +177,7 @@ def main(): import argparse description = 'A simple command-line interface for py_compile module.' - parser = argparse.ArgumentParser(description=description, color=True) + parser = argparse.ArgumentParser(description=description) parser.add_argument( '-q', '--quiet', action='store_true', diff --git a/Lib/random.py b/Lib/random.py index 726a71e782893c..4541267bab866a 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -1016,26 +1016,26 @@ def _test(N=10_000): def _parse_args(arg_list: list[str] | None): import argparse parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, color=True) + formatter_class=argparse.RawTextHelpFormatter) group = parser.add_mutually_exclusive_group() group.add_argument( "-c", "--choice", nargs="+", help="print a random choice") group.add_argument( "-i", "--integer", type=int, metavar="N", - help="print a random integer between 1 and N inclusive") + help="print a random integer between 1 and `N` inclusive") group.add_argument( "-f", "--float", type=float, metavar="N", - help="print a random floating-point number between 0 and N inclusive") + help="print a random floating-point number between 0 and `N` inclusive") group.add_argument( "--test", type=int, const=10_000, nargs="?", help=argparse.SUPPRESS) parser.add_argument("input", nargs="*", help="""\ if no options given, output depends on the input - string or multiple: same as --choice - integer: same as --integer - float: same as --float""") + string or multiple: same as `--choice` + integer: same as `--integer` + float: same as `--float`""") args = parser.parse_args(arg_list) return args, parser.format_help() diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py index 8805442b69e080..ec72c694390717 100644 --- a/Lib/sqlite3/__main__.py +++ b/Lib/sqlite3/__main__.py @@ -87,14 +87,11 @@ def runsource(self, source, filename="", symbol="single"): def main(*args): - parser = ArgumentParser( - description="Python sqlite3 CLI", - color=True, - ) + parser = ArgumentParser(description="Python sqlite3 CLI") parser.add_argument( "filename", type=str, default=":memory:", nargs="?", help=( - "SQLite database to open (defaults to ':memory:'). " + "SQLite database to open (defaults to `:memory:`). " "A new database is created if the file does not previously exist." ), ) @@ -102,7 +99,7 @@ def main(*args): "sql", type=str, nargs="?", help=( "An SQL query to execute. " - "Any returned rows are printed to stdout." + "Any returned rows are printed to `stdout`." ), ) parser.add_argument( diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 4f47aaab9028d0..d0e7dec5575047 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -3067,7 +3067,7 @@ def main(): import argparse description = 'A simple command-line interface for tarfile module.' - parser = argparse.ArgumentParser(description=description, color=True) + parser = argparse.ArgumentParser(description=description) parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose output') parser.add_argument('--filter', metavar='', diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index 45e229eb19f0f9..64c035307e6654 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -245,7 +245,7 @@ def _create_parser(): 'buildbot workers') group.add_argument('--timeout', metavar='TIMEOUT', help='dump the traceback and exit if a test takes ' - 'more than TIMEOUT seconds; disabled if TIMEOUT ' + 'more than `TIMEOUT` seconds; disabled if `TIMEOUT` ' 'is negative or equals to zero') group.add_argument('--wait', action='store_true', help='wait for user input, e.g., allow a debugger ' @@ -261,11 +261,11 @@ def _create_parser(): group = parser.add_argument_group('Verbosity') group.add_argument('-v', '--verbose', action='count', - help='run tests in verbose mode with output to stdout') + help='run tests in verbose mode with output to `stdout`') group.add_argument('-w', '--rerun', action='store_true', help='re-run failed tests in verbose mode') group.add_argument('--verbose2', action='store_true', dest='rerun', - help='deprecated alias to --rerun') + help='deprecated alias to `--rerun`') group.add_argument('-W', '--verbose3', action='store_true', help='display test output on failure') group.add_argument('-q', '--quiet', action='store_true', @@ -295,22 +295,22 @@ def _create_parser(): more_details) group.add_argument('-m', '--match', metavar='PAT', dest='match_tests', action=FilterAction, const=True, - help='match test cases and methods with glob pattern PAT') + help='match test cases and methods with glob pattern `PAT`') group.add_argument('-i', '--ignore', metavar='PAT', dest='match_tests', action=FilterAction, const=False, - help='ignore test cases and methods with glob pattern PAT') + help='ignore test cases and methods with glob pattern `PAT`') group.add_argument('--matchfile', metavar='FILENAME', dest='match_tests', action=FromFileFilterAction, const=True, - help='similar to --match but get patterns from a ' + help='similar to `--match` but get patterns from a ' 'text file, one pattern per line') group.add_argument('--ignorefile', metavar='FILENAME', dest='match_tests', action=FromFileFilterAction, const=False, - help='similar to --matchfile but it receives patterns ' + help='similar to `--matchfile` but it receives patterns ' 'from text file to ignore') group.add_argument('-G', '--failfast', action='store_true', - help='fail as soon as a test fails (only with -v or -W)') + help='fail as soon as a test fails (only with `-v` or `-W`)') group.add_argument('-u', '--use', metavar='RES1,RES2,...', action='extend', type=resources_list, help='specify which special resource intensive tests ' @@ -325,7 +325,7 @@ def _create_parser(): group = parser.add_argument_group('Special runs') group.add_argument('-L', '--runleaks', action='store_true', - help='run the leaks(1) command just before exit.' + + help='run the `leaks(1)` command just before exit.' + more_details) group.add_argument('-R', '--huntrleaks', metavar='RUNCOUNTS', type=huntrleaks, @@ -333,20 +333,20 @@ def _create_parser(): 'very slow).' + more_details) group.add_argument('-j', '--multiprocess', metavar='PROCESSES', dest='use_mp', type=int, - help='run PROCESSES processes at once') + help='run `PROCESSES` processes at once') group.add_argument('--single-process', action='store_true', dest='single_process', help='always run all tests sequentially in ' - 'a single process, ignore -jN option, ' + 'a single process, ignore `-jN` option, ' 'and failed tests are also rerun sequentially ' 'in the same process') group.add_argument('--parallel-threads', metavar='PARALLEL_THREADS', type=int, - help='run copies of each test in PARALLEL_THREADS at ' + help='run copies of each test in `PARALLEL_THREADS` at ' 'once') group.add_argument('-T', '--coverage', action='store_true', dest='trace', - help='turn on code coverage tracing using the trace ' + help='turn on code coverage tracing using the `trace` ' 'module') group.add_argument('-D', '--coverdir', metavar='DIR', type=relative_filename, @@ -356,18 +356,18 @@ def _create_parser(): help='put coverage files alongside modules') group.add_argument('-t', '--threshold', metavar='THRESHOLD', type=int, - help='call gc.set_threshold(THRESHOLD)') + help='call `gc.set_threshold(THRESHOLD)`') group.add_argument('-n', '--nowindows', action='store_true', help='suppress error message boxes on Windows') group.add_argument('-F', '--forever', action='store_true', help='run the specified tests in a loop, until an ' - 'error happens; imply --failfast') + 'error happens; imply `--failfast`') group.add_argument('--list-tests', action='store_true', help="only write the name of tests that will be run, " "don't execute them") group.add_argument('--list-cases', action='store_true', - help='only write the name of test cases that will be run' - ' , don\'t execute them') + help='only write the name of test cases that will be run, ' + 'don\'t execute them') group.add_argument('-P', '--pgo', dest='pgo', action='store_true', help='enable Profile Guided Optimization (PGO) training') group.add_argument('--pgo-extended', action='store_true', @@ -390,11 +390,11 @@ def _create_parser(): group.add_argument('--tempdir', metavar='PATH', help='override the working directory for the test run') group.add_argument('--cleanup', action='store_true', - help='remove old test_python_* directories') + help='remove old `test_python_*` directories') group.add_argument('--bisect', action='store_true', - help='if some tests fail, run test.bisect_cmd on them') + help='if some tests fail, run `test.bisect_cmd` on them') group.add_argument('--pythoninfo', action='store_true', - help="run python -m test.pythoninfo before tests") + help="run `python -m test.pythoninfo` before tests") group.add_argument('--dont-add-python-opts', dest='_add_python_opts', action='store_false', help="internal option, don't use it") diff --git a/Lib/timeit.py b/Lib/timeit.py index a897d9663c24e2..01bdfd901e30d5 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -299,14 +299,14 @@ def main(args=None, *, _wrap_timer=None): "-p", "--process", action="store_true", - help="use time.process_time() (default is time.perf_counter())", + help="use `time.process_time()` (default is `time.perf_counter()`)", ) parser.add_argument( "-t", "--target-time", type=float, default=default_target_time, - help="if --number is 0 the code will run until it takes " + help="if `--number` is 0 the code will run until it takes " "at least this many seconds (default %(default)s)", ) parser.add_argument( diff --git a/Lib/tokenize.py b/Lib/tokenize.py index 52cf3f0b7ccaa9..3545d92c4f5d7f 100644 --- a/Lib/tokenize.py +++ b/Lib/tokenize.py @@ -578,7 +578,7 @@ def error(message, filename=None, location=None): parser = argparse.ArgumentParser() parser.add_argument(dest='filename', nargs='?', metavar='filename.py', - help='the file to tokenize; defaults to stdin') + help='the file to tokenize; defaults to `stdin`') parser.add_argument('-e', '--exact', dest='exact', action='store_true', help='display token names using the exact type') args = parser.parse_args(args) diff --git a/Lib/trace.py b/Lib/trace.py index cd3a6d30661da3..43ec201c4696d1 100644 --- a/Lib/trace.py +++ b/Lib/trace.py @@ -604,7 +604,7 @@ def results(self): def main(): import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('--version', action='version', version='trace 2.0') grp = parser.add_argument_group('Main options', @@ -612,27 +612,27 @@ def main(): grp.add_argument('-c', '--count', action='store_true', help='Count the number of times each line is executed and write ' - 'the counts to .cover for each module executed, in ' - 'the module\'s directory. See also --coverdir, --file, ' - '--no-report below.') + 'the counts to `.cover` for each module executed, in ' + 'the module\'s directory. See also `--coverdir`, `--file`, ' + '`--no-report` below.') grp.add_argument('-t', '--trace', action='store_true', - help='Print each line to sys.stdout before it is executed') + help='Print each line to `sys.stdout` before it is executed') grp.add_argument('-l', '--listfuncs', action='store_true', help='Keep track of which functions are executed at least once ' - 'and write the results to sys.stdout after the program exits. ' - 'Cannot be specified alongside --trace or --count.') + 'and write the results to `sys.stdout` after the program exits. ' + 'Cannot be specified alongside `--trace` or `--count`.') grp.add_argument('-T', '--trackcalls', action='store_true', help='Keep track of caller/called pairs and write the results to ' - 'sys.stdout after the program exits.') + '`sys.stdout` after the program exits.') grp = parser.add_argument_group('Modifiers') _grp = grp.add_mutually_exclusive_group() _grp.add_argument('-r', '--report', action='store_true', help='Generate a report from a counts file; does not execute any ' - 'code. --file must specify the results file to read, which ' - 'must have been created in a previous run with --count ' - '--file=FILE') + 'code. `--file` must specify the results file to read, which ' + 'must have been created in a previous run with `--count` ' + '`--file=FILE`') _grp.add_argument('-R', '--no-report', action='store_true', help='Do not generate the coverage report files. ' 'Useful if you want to accumulate over several runs.') @@ -641,14 +641,14 @@ def main(): help='File to accumulate counts over several runs') grp.add_argument('-C', '--coverdir', help='Directory where the report files go. The coverage report ' - 'for . will be written to file ' - '

//.cover') + 'for `.` will be written to file ' + '`//.cover`') grp.add_argument('-m', '--missing', action='store_true', help='Annotate executable lines that were not executed with ' '">>>>>> "') grp.add_argument('-s', '--summary', action='store_true', - help='Write a brief summary for each file to sys.stdout. ' - 'Can only be used with --count or --report') + help='Write a brief summary for each file to `sys.stdout`. ' + 'Can only be used with `--count` or `--report`') grp.add_argument('-g', '--timing', action='store_true', help='Prefix each line with the time since the program started. ' 'Only used while tracing') @@ -661,7 +661,7 @@ def main(): 'module names.') grp.add_argument('--ignore-dir', action='append', default=[], help='Ignore files in the given directory ' - '(multiple directories can be joined by os.pathsep).') + '(multiple directories can be joined by `os.pathsep`).') parser.add_argument('--module', action='store_true', default=False, help='Trace a module. ') diff --git a/Lib/unittest/main.py b/Lib/unittest/main.py index be99d93c78cca6..6eeebf9657a3c7 100644 --- a/Lib/unittest/main.py +++ b/Lib/unittest/main.py @@ -172,7 +172,7 @@ def _getParentArgParser(self): help='Show local variables in tracebacks') parser.add_argument('--durations', dest='durations', type=int, default=None, metavar="N", - help='Show the N slowest test cases (N=0 for all)') + help='Show the `N` slowest test cases (`N=0` for all)') if self.failfast is None: parser.add_argument('-f', '--failfast', dest='failfast', action='store_true', @@ -181,12 +181,12 @@ def _getParentArgParser(self): if self.catchbreak is None: parser.add_argument('-c', '--catch', dest='catchbreak', action='store_true', - help='Catch Ctrl-C and display results so far') + help='Catch `Ctrl-C` and display results so far') self.catchbreak = False if self.buffer is None: parser.add_argument('-b', '--buffer', dest='buffer', action='store_true', - help='Buffer stdout and stderr during tests') + help='Buffer `stdout` and `stderr` during tests') self.buffer = False if self.testNamePatterns is None: parser.add_argument('-k', dest='testNamePatterns', @@ -197,7 +197,7 @@ def _getParentArgParser(self): return parser def _getMainArgParser(self, parent): - parser = argparse.ArgumentParser(parents=[parent], color=True) + parser = argparse.ArgumentParser(parents=[parent]) parser.prog = self.progName parser.print_help = self._print_help @@ -208,16 +208,16 @@ def _getMainArgParser(self, parent): return parser def _getDiscoveryArgParser(self, parent): - parser = argparse.ArgumentParser(parents=[parent], color=True) + parser = argparse.ArgumentParser(parents=[parent]) parser.prog = '%s discover' % self.progName parser.epilog = ('For test discovery all test modules must be ' 'importable from the top level directory of the ' 'project.') parser.add_argument('-s', '--start-directory', dest='start', - help="Directory to start discovery ('.' default)") + help="Directory to start discovery (`.` default)") parser.add_argument('-p', '--pattern', dest='pattern', - help="Pattern to match tests ('test*.py' default)") + help="Pattern to match tests (`test*.py` default)") parser.add_argument('-t', '--top-level-directory', dest='top', help='Top level directory of project (defaults to ' 'start directory)') diff --git a/Lib/uuid.py b/Lib/uuid.py index 8c59581464b0d0..4bdcb67775a2ea 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -955,7 +955,6 @@ def main(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description="Generate a UUID using the selected UUID function.", - color=True, ) parser.add_argument("-u", "--uuid", choices=uuid_funcs.keys(), @@ -963,14 +962,14 @@ def main(): help="function to generate the UUID") parser.add_argument("-n", "--namespace", metavar=f"{{any UUID,{','.join(namespaces)}}}", - help="uuid3/uuid5 only: " + help="`uuid3`/`uuid5` only: " "a UUID, or a well-known predefined UUID addressed " "by namespace name") parser.add_argument("-N", "--name", - help="uuid3/uuid5 only: " + help="`uuid3`/`uuid5` only: " "name used as part of generating the UUID") parser.add_argument("-C", "--count", metavar="NUM", type=int, default=1, - help="generate NUM fresh UUIDs") + help="generate `NUM` fresh UUIDs") args = parser.parse_args() uuid_func = uuid_funcs[args.uuid] diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 002f4ebc988a3b..0653a43a8b1776 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -621,7 +621,6 @@ def main(args=None): 'activate it, e.g. by ' 'sourcing an activate script ' 'in its bin directory.', - color=True, ) parser.add_argument('dirs', metavar='ENV_DIR', nargs='+', help='A directory to create the environment in.') diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index 97aad6eea509eb..c2ee0df0ef8885 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -734,7 +734,7 @@ def open(self, url, new=0, autoraise=True): def parse_args(arg_list: list[str] | None): import argparse parser = argparse.ArgumentParser( - description="Open URL in a web browser.", color=True, + description="Open URL in a web browser.", ) parser.add_argument("url", help="URL to open") diff --git a/Lib/zipapp.py b/Lib/zipapp.py index 7a4ef96ea0f077..a1cef18ada9d05 100644 --- a/Lib/zipapp.py +++ b/Lib/zipapp.py @@ -187,16 +187,16 @@ def main(args=None): """ import argparse - parser = argparse.ArgumentParser(color=True) + parser = argparse.ArgumentParser() parser.add_argument('--output', '-o', default=None, help="The name of the output archive. " - "Required if SOURCE is an archive.") + "Required if `SOURCE` is an archive.") parser.add_argument('--python', '-p', default=None, help="The name of the Python interpreter to use " "(default: no shebang line).") parser.add_argument('--main', '-m', default=None, help="The main function of the application " - "(default: use an existing __main__.py).") + "(default: use an existing `__main__.py`).") parser.add_argument('--compress', '-c', action='store_true', help="Compress files with the deflate method. " "Files are stored uncompressed by default.") diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 1e0cc5f6234f28..86c3bc36b695c7 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2328,7 +2328,7 @@ def main(args=None): import argparse description = 'A simple command-line interface for zipfile module.' - parser = argparse.ArgumentParser(description=description, color=True) + parser = argparse.ArgumentParser(description=description) group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-l', '--list', metavar='', help='Show listing of a zipfile') @@ -2341,7 +2341,7 @@ def main(args=None): group.add_argument('-t', '--test', metavar='', help='Test if a zipfile is valid') parser.add_argument('--metadata-encoding', metavar='', - help='Specify encoding of member names for -l, -e and -t') + help='Specify encoding of member names for `-l`, `-e` and `-t`') args = parser.parse_args(args) encoding = args.metadata_encoding diff --git a/Misc/NEWS.d/next/Library/2026-05-05-00-30-04.gh-issue-142389.4daLzc.rst b/Misc/NEWS.d/next/Library/2026-05-05-00-30-04.gh-issue-142389.4daLzc.rst new file mode 100644 index 00000000000000..4079854ef29349 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-05-00-30-04.gh-issue-142389.4daLzc.rst @@ -0,0 +1,2 @@ +Add backticks to stdlib argparse help to display in colour. Patch by Hugo +van Kemenade. From ffe050ad9b93e158dcea32891dc34722b8aef148 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 5 May 2026 01:58:04 +0300 Subject: [PATCH 09/10] gh-142389: Add support for backtick colorisation in argparse help text (#149375) Co-authored-by: Savannah Ostrowski --- Doc/library/argparse.rst | 13 ++--- Doc/whatsnew/3.15.rst | 8 ++- Lib/argparse.py | 12 +++-- Lib/test/test_argparse.py | 49 +++++++++++++++++-- ...-05-04-18-01-35.gh-issue-142389.4Faqpq.rst | 2 + 5 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-04-18-01-35.gh-issue-142389.4Faqpq.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index e37afd6d0b6d5a..db5fae2006678a 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -637,25 +637,22 @@ are set. .. versionadded:: 3.14 -To highlight inline code in your description or epilog text, you can use -backticks:: +To highlight inline code in your description, epilog, or argument ``help`` +text, you can use single or double backticks:: >>> parser = argparse.ArgumentParser( ... formatter_class=argparse.RawDescriptionHelpFormatter, + ... description='Run ``python -m myapp`` to start.', ... epilog='''Examples: ... `python -m myapp --verbose` - ... `python -m myapp --config settings.json` + ... ``python -m myapp --config settings.json`` ... ''') + >>> parser.add_argument('--foo', help='set the `foo` value') When colors are enabled, the text inside backticks will be displayed in a distinct color to help examples stand out. When colors are disabled, backticks are preserved as-is, which is readable in plain text. -.. note:: - - Backtick markup only applies to description and epilog text. It does not - apply to individual argument ``help`` strings. - .. versionadded:: 3.15 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9409b41f574222..7f12cc04a460d4 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -756,10 +756,14 @@ argparse default to ``True``. This enables suggestions for mistyped arguments by default. (Contributed by Jakob Schluse in :gh:`140450`.) -* Added backtick markup support in description and epilog text to highlight - inline code when color output is enabled. +* Added backtick markup support in :class:`~argparse.ArgumentParser` description + and epilog text to highlight inline code when color output is enabled. (Contributed by Savannah Ostrowski in :gh:`142390`.) +* Extended backtick markup to argument ``help`` text and added support for + double backticks (RST inline-literal style). + (Contributed by Hugo van Kemenade in :gh:`149375`.) + array ----- diff --git a/Lib/argparse.py b/Lib/argparse.py index 9ea6b32163a9d1..9bc3ea64431e52 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -529,7 +529,7 @@ def _apply_text_markup(self, text): """Apply color markup to text. Supported markup: - `...` - inline code (rendered with prog_extra color) + `...` or ``...`` - inline code (rendered with prog_extra color) When colors are disabled, backticks are preserved as-is. """ @@ -537,8 +537,8 @@ def _apply_text_markup(self, text): if not t.reset: return text text = _re.sub( - r'`([^`]+)`', - rf'{t.prog_extra}\1{t.reset}', + r'(`{1,2})([^`]+)\1', + rf'{t.prog_extra}\2{t.reset}', text, ) return text @@ -682,7 +682,7 @@ def _format_args(self, action, default_metavar): def _expand_help(self, action): help_string = self._get_help_string(action) if '%' not in help_string: - return help_string + return self._apply_text_markup(help_string) params = dict(vars(action), prog=self._prog) for name in list(params): value = params[name] @@ -726,7 +726,9 @@ def colorize(match): # bare %s etc. - format with full params dict, no colorization return spec % params - return _re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE) + return self._apply_text_markup( + _re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE) + ) def _iter_indented_subactions(self, action): try: diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 4799c9947dd3ee..88c1a21aa28551 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7620,21 +7620,25 @@ def test_backtick_markup_in_description(self): parser = argparse.ArgumentParser( prog='PROG', color=True, - description='Run `python -m myapp` to start.', + description='Run `python myapp` or ``python -m myapp`` to start.', ) prog_extra = self.theme.prog_extra reset = self.theme.reset help_text = parser.format_help() - self.assertIn(f'Run {prog_extra}python -m myapp{reset} to start.', - help_text) + self.assertIn( + f'Run {prog_extra}python myapp{reset} or ' + f'{prog_extra}python -m myapp{reset} to start.', + help_text, + ) + self.assertNotIn("`", help_text) def test_backtick_markup_multiple(self): parser = argparse.ArgumentParser( prog='PROG', color=True, - epilog='Try `app run` or `app test`.', + epilog='Try `app run` or ``app test``.', ) prog_extra = self.theme.prog_extra @@ -7643,17 +7647,19 @@ def test_backtick_markup_multiple(self): help_text = parser.format_help() self.assertIn(f'{prog_extra}app run{reset}', help_text) self.assertIn(f'{prog_extra}app test{reset}', help_text) + self.assertNotIn('`', help_text) def test_backtick_markup_not_applied_when_color_disabled(self): # When color is disabled, backticks are preserved as-is parser = argparse.ArgumentParser( prog='PROG', color=False, - epilog='Example: `python -m myapp`', + epilog='Examples: `python -m myapp` or ``python -m myapp --x``', ) help_text = parser.format_help() self.assertIn('`python -m myapp`', help_text) + self.assertIn('``python -m myapp --x``', help_text) self.assertNotIn('\x1b[', help_text) def test_backtick_markup_with_format_string(self): @@ -7696,6 +7702,39 @@ def test_backtick_markup_special_regex_chars(self): help_text = parser.format_help() self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text) + def test_backtick_markup_in_argument_help(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument("--foo", help="set the `foo` value") + parser.add_argument("--bar", help="set the ``bar`` value") + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"set the {prog_extra}foo{reset} value", help_text) + self.assertIn(f"set the {prog_extra}bar{reset} value", help_text) + self.assertNotIn("`", help_text) + + def test_backtick_markup_in_argument_help_with_format(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--foo", default="bar", help="set `foo` (default: %(default)s)" + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"set {prog_extra}foo{reset}", help_text) + + def test_backtick_markup_in_argument_help_color_disabled(self): + parser = argparse.ArgumentParser(prog="PROG", color=False) + parser.add_argument("--foo", help="set the `foo` value") + + help_text = parser.format_help() + self.assertIn("set the `foo` value", help_text) + self.assertNotIn("\x1b[", help_text) + def test_help_with_format_specifiers(self): # GH-142950: format specifiers like %x should work with color=True parser = argparse.ArgumentParser(prog='PROG', color=True) diff --git a/Misc/NEWS.d/next/Library/2026-05-04-18-01-35.gh-issue-142389.4Faqpq.rst b/Misc/NEWS.d/next/Library/2026-05-04-18-01-35.gh-issue-142389.4Faqpq.rst new file mode 100644 index 00000000000000..725f2debe2c615 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-04-18-01-35.gh-issue-142389.4Faqpq.rst @@ -0,0 +1,2 @@ +Add backtick markup support in :mod:`argparse` option help text to highlight +inline code when color output is enabled. Patch by Hugo van Kemenade. From 8c796782fc3fbd380ca599d7b2dcf896375c5e60 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Tue, 5 May 2026 00:00:23 +0100 Subject: [PATCH 10/10] gh-146462: Add dict introspection debug offsets (#148633) --- Include/internal/pycore_debug_offsets.h | 14 ++++++++++++++ .../2026-04-15-12-00-00.gh-issue-146462.1YfK6v.rst | 3 +++ .../_remote_debugging/debug_offsets_validation.h | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-12-00-00.gh-issue-146462.1YfK6v.rst diff --git a/Include/internal/pycore_debug_offsets.h b/Include/internal/pycore_debug_offsets.h index 1dd10f8d94cfd8..18490f98a918a7 100644 --- a/Include/internal/pycore_debug_offsets.h +++ b/Include/internal/pycore_debug_offsets.h @@ -158,8 +158,16 @@ typedef struct _Py_DebugOffsets { uint64_t tp_name; uint64_t tp_repr; uint64_t tp_flags; + uint64_t tp_basicsize; + uint64_t tp_dictoffset; } type_object; + // PyHeapTypeObject offset; + struct _heap_type_object { + uint64_t size; + uint64_t ht_cached_keys; + } heap_type_object; + // PyTuple object offset; struct _tuple_object { uint64_t size; @@ -330,6 +338,12 @@ typedef struct _Py_DebugOffsets { .tp_name = offsetof(PyTypeObject, tp_name), \ .tp_repr = offsetof(PyTypeObject, tp_repr), \ .tp_flags = offsetof(PyTypeObject, tp_flags), \ + .tp_basicsize = offsetof(PyTypeObject, tp_basicsize), \ + .tp_dictoffset = offsetof(PyTypeObject, tp_dictoffset), \ + }, \ + .heap_type_object = { \ + .size = sizeof(PyHeapTypeObject), \ + .ht_cached_keys = offsetof(PyHeapTypeObject, ht_cached_keys), \ }, \ .tuple_object = { \ .size = sizeof(PyTupleObject), \ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-12-00-00.gh-issue-146462.1YfK6v.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-12-00-00.gh-issue-146462.1YfK6v.rst new file mode 100644 index 00000000000000..44019b7562a344 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-15-12-00-00.gh-issue-146462.1YfK6v.rst @@ -0,0 +1,3 @@ +Added ``PyTypeObject.tp_basicsize``, ``PyTypeObject.tp_dictoffset``, and +``PyHeapTypeObject.ht_cached_keys`` offsets to :c:type:`!_Py_DebugOffsets` to +support version-independent read-only dict introspection tools. diff --git a/Modules/_remote_debugging/debug_offsets_validation.h b/Modules/_remote_debugging/debug_offsets_validation.h index 1507026306192e..f070f03ac459dc 100644 --- a/Modules/_remote_debugging/debug_offsets_validation.h +++ b/Modules/_remote_debugging/debug_offsets_validation.h @@ -31,7 +31,7 @@ #define FIELD_SIZE(type, member) sizeof(((type *)0)->member) enum { - PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 848, + PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 880, PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE = 104, };