Update ten leaf stdlib modules to Python 3.15 (#9208)

* Update ten leaf standard-library modules to Python 3.15

Migrate mail, TOML, zoneinfo and CLI leaves in dependency order with canonical tests and preserved RustPython guards.

Assisted-by: OpenAI Codex:model version unavailable

* Defer warning initialization for lazy standard-library imports

Match CPython startup option processing and load the Python display helper on the first native warning.

Assisted-by: OpenAI Codex:model version unavailable

* Update the canonical webbrowser consumer for argparse colors

Copy the complete pinned CPython 3.15 webbrowser module and tests via update_lib. The canonical CLI test disables coloring while preserving all assertions. This consumer overlaps the pending HTTP/doc split #9193.

Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-19 20:31:03 +09:00
1 parent c434bfdacd
commit 75e77c30e3
35 files changed
+2286 -649

No files matched your search

+235 -50
View File
@@ -64,7 +64,6 @@ considered public as object names -- the API of the formatter objects is
still considered an implementation detail.)
"""
__version__ = '1.1'
__all__ = [
'ArgumentParser',
'ArgumentError',
@@ -90,8 +89,10 @@ __all__ = [
import os as _os
import re as _re
import sys as _sys
from gettext import gettext as _
from gettext import ngettext
from gettext import gettext as _, ngettext
lazy import _colorize
SUPPRESS = '==SUPPRESS=='
@@ -157,6 +158,17 @@ def _identity(value):
# Formatting Help
# ===============
class _ColorlessTheme:
# A 'fake' theme for no colors
def __getattr__(self, name):
# _colorize's no_color themes are just all empty strings
# by directly using empty strings the import is avoided
if name.startswith("_"):
raise AttributeError(name)
return ""
_colorless_theme = _ColorlessTheme()
class HelpFormatter(object):
"""Formatter for generating usage messages and argument help strings.
@@ -171,7 +183,6 @@ class HelpFormatter(object):
indent_increment=2,
max_help_position=24,
width=None,
color=True,
):
# default setting for width
if width is None:
@@ -179,7 +190,6 @@ class HelpFormatter(object):
width = shutil.get_terminal_size().columns
width -= 2
self._set_color(color)
self._prog = prog
self._indent_increment = indent_increment
self._max_help_position = min(max_help_position,
@@ -196,15 +206,35 @@ class HelpFormatter(object):
self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII)
self._long_break_matcher = _re.compile(r'\n\n\n+')
def _set_color(self, color):
from _colorize import can_colorize, decolor, get_theme
self._set_color(False)
if color and can_colorize():
self._theme = get_theme(force_color=True).argparse
self._decolor = decolor
def _set_color(self, color, *, file=None):
# Set a new color setting and file, clear caches for theme and decolor
self._theme_color = color
self._theme_file = file
self._cached_theme = None
self._cached_decolor = None
def _get_theme_and_decolor(self):
# If self._theme_color is false, this prevents _colorize from importing
if self._theme_color and _colorize.can_colorize(file=self._theme_file):
self._cached_theme = _colorize.get_theme(force_color=True).argparse
self._cached_decolor = _colorize.decolor
else:
self._theme = get_theme(force_no_color=True).argparse
self._decolor = _identity
self._cached_theme = _colorless_theme
self._cached_decolor = _identity
@property
def _theme(self):
if self._cached_theme is None:
self._get_theme_and_decolor()
return self._cached_theme
@property
def _decolor(self):
if self._cached_decolor is None:
self._get_theme_and_decolor()
return self._cached_decolor
# ===============================
# Section and indentation methods
@@ -522,7 +552,27 @@ class HelpFormatter(object):
text = text % dict(prog=self._prog)
text_width = max(self._width - self._current_indent, 11)
indent = ' ' * self._current_indent
return self._fill_text(text, text_width, indent) + '\n\n'
text = self._fill_text(text, text_width, indent)
text = self._apply_text_markup(text)
return text + '\n\n'
def _apply_text_markup(self, text):
"""Apply color markup to text.
Supported markup:
`...` or ``...`` - inline code (rendered with prog_extra color)
When colors are disabled, backticks are preserved as-is.
"""
t = self._theme
if not t.reset:
return text
text = _re.sub(
r'(`{1,2})([^`]+)\1',
rf'{t.prog_extra}\2{t.reset}',
text,
)
return text
def _format_action(self, action):
# determine the required width and the entry label
@@ -661,9 +711,9 @@ class HelpFormatter(object):
return result
def _expand_help(self, action):
help_string = self._get_help_string(action)
help_string = str(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]
@@ -673,7 +723,43 @@ class HelpFormatter(object):
params[name] = value.__name__
if params.get('choices') is not None:
params['choices'] = ', '.join(map(str, params['choices']))
return help_string % params
t = self._theme
result = help_string % params
if not t.reset:
return result
# Match format specifiers like: %s, %d, %(key)s, etc.
fmt_spec = r'''
%
(?:
% # %% escape
|
(?:\((?P<key>[^)]*)\))? # key
[-#0\ +]* # flags
(?:\*|\d+)? # width
(?:\.(?:\*|\d+))? # precision
[hlL]? # length modifier
[diouxXeEfFgGcrsa] # conversion type
)
'''
def colorize(match):
spec, key = match.group(0, 'key')
if spec == '%%':
return '%'
if key is not None:
# %(key)... - format and colorize
formatted = spec % {key: params[key]}
return f'{t.interpolated_value}{formatted}{t.reset}'
# bare %s etc. - format with full params dict, no colorization
return spec % params
return self._apply_text_markup(
_re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE)
)
def _iter_indented_subactions(self, action):
try:
@@ -690,7 +776,35 @@ class HelpFormatter(object):
# The textwrap module is used only for formatting help.
# Delay its import for speeding up the common usage of argparse.
import textwrap
return textwrap.wrap(text, width)
decolored = self._decolor(text)
if decolored == text:
return textwrap.wrap(text, width)
# gh-142035: colors inflate textwrap's length counts, so wrap
# the decolored text and re-apply colors per word; if textwrap
# split a word, keep the plain lines (colors can't be mapped).
plain = self._whitespace_matcher.sub(' ', decolored).strip()
if not plain:
# nothing visible to wrap (e.g. an empty interpolated value)
return [text]
plain_lines = textwrap.wrap(plain, width)
plain_words = plain.split()
colored_words = text.split()
# Drop escape-only tokens (e.g. an empty interpolated value).
if len(colored_words) != len(plain_words):
colored_words = [
word for word in colored_words if self._decolor(word)
]
colored_lines = []
start = 0
for plain_line in plain_lines:
plain_line_words = plain_line.split()
end = start + len(plain_line_words)
if plain_words[start:end] != plain_line_words:
return plain_lines
colored_lines.append(' '.join(colored_words[start:end]))
start = end
return colored_lines
def _fill_text(self, text, width, indent):
text = self._whitespace_matcher.sub(' ', text).strip()
@@ -750,7 +864,14 @@ class ArgumentDefaultsHelpFormatter(HelpFormatter):
):
defaulting_nargs = (OPTIONAL, ZERO_OR_MORE)
if action.option_strings or action.nargs in defaulting_nargs:
help += _(' (default: %(default)s)')
t = self._theme
default_str = _(" (default: %(default)s)")
prefix, suffix = default_str.split("%(default)s")
help += (
f" {t.default}{prefix.lstrip()}{t.reset}"
f"%(default)s"
f"{t.default}{suffix}{t.reset}"
)
return help
@@ -934,15 +1055,26 @@ class BooleanOptionalAction(Action):
deprecated=False):
_option_strings = []
neg_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)
if option_string.startswith('--'):
if option_string.startswith('--no-'):
if len(option_string) > 2 and option_string[0] == option_string[1]:
# two-dash long option: '--foo' -> '--no-foo'
if option_string.startswith('no-', 2):
raise ValueError(f'invalid option name {option_string!r} '
f'for BooleanOptionalAction')
option_string = '--no-' + option_string[2:]
option_string = option_string[:2] + 'no-' + option_string[2:]
_option_strings.append(option_string)
neg_option_strings.append(option_string)
elif len(option_string) > 2 and option_string[0] != option_string[1]:
# single-dash long option: '-foo' -> '-nofoo'
if option_string.startswith('no', 1):
raise ValueError(f'invalid option name {option_string!r} '
f'for BooleanOptionalAction')
option_string = option_string[:1] + 'no' + option_string[1:]
_option_strings.append(option_string)
neg_option_strings.append(option_string)
super().__init__(
option_strings=_option_strings,
@@ -952,11 +1084,12 @@ class BooleanOptionalAction(Action):
required=required,
help=help,
deprecated=deprecated)
self.neg_option_strings = neg_option_strings
def __call__(self, parser, namespace, values, option_string=None):
if option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith('--no-'))
setattr(namespace, self.dest, option_string not in self.neg_option_strings)
def format_usage(self):
return ' | '.join(self.option_strings)
@@ -1662,29 +1795,35 @@ class _ActionsContainer(object):
def _get_optional_kwargs(self, *args, **kwargs):
# determine short and long option strings
option_strings = []
long_option_strings = []
for option_string in args:
# error on strings that don't start with an appropriate prefix
if not option_string[0] in self.prefix_chars:
if option_string[0] not in self.prefix_chars:
raise ValueError(
f'invalid option string {option_string!r}: '
f'must start with a character {self.prefix_chars!r}')
# strings starting with two prefix characters are long options
option_strings.append(option_string)
if len(option_string) > 1 and option_string[1] in self.prefix_chars:
long_option_strings.append(option_string)
# infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
dest = kwargs.pop('dest', None)
if dest is None:
if long_option_strings:
dest_option_string = long_option_strings[0]
else:
dest_option_string = option_strings[0]
dest = dest_option_string.lstrip(self.prefix_chars)
priority = 0
for option_string in option_strings:
if len(option_string) <= 2:
# short option: '-x' -> 'x'
if priority < 1:
dest = option_string.lstrip(self.prefix_chars)
priority = 1
elif option_string[1] not in self.prefix_chars:
# single-dash long option: '-foo' -> 'foo'
if priority < 2:
dest = option_string.lstrip(self.prefix_chars)
priority = 2
else:
# two-dash long option: '--foo' -> 'foo'
dest = option_string.lstrip(self.prefix_chars)
break
if not dest:
msg = f'dest= is required for options like {option_string!r}'
msg = f'dest= is required for options like {repr(option_strings)[1:-1]}'
raise TypeError(msg)
dest = dest.replace('-', '_')
@@ -1862,7 +2001,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
- exit_on_error -- Determines whether or not ArgumentParser exits with
error info when an error occurs
- suggest_on_error - Enables suggestions for mistyped argument choices
and subparser names (default: ``False``)
and subparser names (default: ``True``)
- color - Allow color output in help messages (default: ``False``)
"""
@@ -1881,7 +2020,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
allow_abbrev=True,
exit_on_error=True,
*,
suggest_on_error=False,
suggest_on_error=True,
color=True,
):
superinit = super(ArgumentParser, self).__init__
@@ -1963,14 +2102,16 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
self._subparsers = self._positionals
# prog defaults to the usage message of this parser, skipping
# optional arguments and with no "usage:" prefix
# non-required optional arguments and with no "usage:" prefix
if kwargs.get('prog') is None:
# Create formatter without color to avoid storing ANSI codes in prog
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(False)
positionals = self._get_positional_actions()
required_optionals = [action for action in self._get_optional_actions()
if action.required]
groups = self._mutually_exclusive_groups
formatter.add_usage(None, positionals, groups, '')
formatter.add_usage(None, required_optionals + positionals, groups, '')
kwargs['prog'] = formatter.format_help().strip()
# create the parsers action and add it to the positionals list
@@ -2437,7 +2578,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
return None
# if it doesn't start with a prefix, it was meant to be positional
if not arg_string[0] in self.prefix_chars:
if arg_string[0] not in self.prefix_chars:
return None
# if the option string is present in the parser, return the action
@@ -2546,7 +2687,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# allow any number of options or arguments
elif nargs == REMAINDER:
nargs_pattern = '([AO]*)' if option else '(.*)'
nargs_pattern = '(.*)'
# allow one argument followed by any number of options or arguments
elif nargs == PARSER:
@@ -2699,14 +2840,16 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# Help-formatting methods
# =======================
def format_usage(self):
formatter = self._get_formatter()
def format_usage(self, formatter=None):
if formatter is None:
formatter = self._get_formatter()
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
return formatter.format_help()
def format_help(self):
formatter = self._get_formatter()
def format_help(self, formatter=None):
if formatter is None:
formatter = self._get_formatter()
# usage
formatter.add_usage(self.usage, self._actions,
@@ -2728,16 +2871,20 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# determine help from format above
return formatter.format_help()
def _get_formatter(self):
def _get_formatter(self, file=None):
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(self.color)
formatter._set_color(self.color, file=file)
return formatter
def _get_validation_formatter(self):
# Return cached formatter for read-only validation operations
# (_expand_help and _format_args). Avoids repeated slow _set_color calls.
# Validation never renders output, so force color off to avoid
# importing _colorize during add_argument.
if self._cached_formatter is None:
self._cached_formatter = self._get_formatter()
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(False)
self._cached_formatter = formatter
return self._cached_formatter
# =====================
@@ -2747,12 +2894,26 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
def print_usage(self, file=None):
if file is None:
file = _sys.stdout
self._print_message(self.format_usage(), file)
formatter = self._get_formatter(file=file)
try:
usage_text = self.format_usage(formatter=formatter)
except TypeError:
# Backward compatibility for formatter classes that
# do not accept the 'formatter' keyword argument.
usage_text = self.format_usage()
self._print_message(usage_text, file)
def print_help(self, file=None):
if file is None:
file = _sys.stdout
self._print_message(self.format_help(), file)
formatter = self._get_formatter(file=file)
try:
help_text = self.format_help(formatter=formatter)
except TypeError:
# Backward compatibility for formatter classes that
# do not accept the 'formatter' keyword argument.
help_text = self.format_help()
self._print_message(help_text, file)
def _print_message(self, message, file=None):
if message:
@@ -2762,6 +2923,13 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
except (AttributeError, OSError):
pass
def _get_theme(self, file=None):
# If self.color is False, _colorize is not imported
if self.color and _colorize.can_colorize(file=file):
return _colorize.get_theme(force_color=True).argparse
else:
return _colorless_theme
# ===============
# Exiting methods
# ===============
@@ -2781,9 +2949,26 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
should either exit or raise an exception.
"""
self.print_usage(_sys.stderr)
theme = self._get_theme(file=_sys.stderr)
fmt = _('%(prog)s: error: %(message)s\n')
fmt = fmt.replace('error: %(message)s',
f'{theme.error}error:{theme.reset} {theme.message}%(message)s{theme.reset}')
args = {'prog': self.prog, 'message': message}
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
self.exit(2, fmt % args)
def _warning(self, message):
theme = self._get_theme(file=_sys.stderr)
fmt = _('%(prog)s: warning: %(message)s\n')
fmt = fmt.replace('warning: %(message)s',
f'{theme.warning}warning:{theme.reset} {theme.message}%(message)s{theme.reset}')
args = {'prog': self.prog, 'message': message}
self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr)
self._print_message(fmt % args, _sys.stderr)
def __getattr__(name):
if name == "__version__":
from warnings import _deprecated
_deprecated("__version__", remove=(3, 20))
return "1.1" # Do not change
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+137 -66
View File
@@ -14,8 +14,9 @@ from itertools import repeat
__all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
"firstweekday", "isleap", "leapdays", "weekday", "monthrange",
"monthcalendar", "prmonth", "month", "prcal", "calendar",
"timegm", "month_name", "month_abbr", "day_name", "day_abbr",
"Calendar", "TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
"timegm", "month_name", "month_abbr", "standalone_month_name",
"standalone_month_abbr", "day_name", "day_abbr", "Calendar",
"TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
"LocaleHTMLCalendar", "weekheader",
"Day", "Month", "JANUARY", "FEBRUARY", "MARCH",
"APRIL", "MAY", "JUNE", "JULY",
@@ -139,6 +140,24 @@ day_abbr = _localized_day('%a')
month_name = _localized_month('%B')
month_abbr = _localized_month('%b')
# On platforms that support the %OB and %Ob specifiers, they are used
# to get the standalone form of the month name. This is required for
# some languages such as Greek, Slavic, and Baltic languages.
try:
standalone_month_name = _localized_month('%OB')
standalone_month_abbr = _localized_month('%Ob')
except ValueError:
standalone_month_name = month_name
standalone_month_abbr = month_abbr
else:
# Some systems that do not support '%OB' will keep it as-is (i.e.,
# we get [..., '%OB', '%OB', '%OB']), so for non-distinct names,
# we fall back to month_name/month_abbr.
if len(set(standalone_month_name)) != len(set(month_name)):
standalone_month_name = month_name
if len(set(standalone_month_abbr)) != len(set(month_abbr)):
standalone_month_abbr = month_abbr
def isleap(year):
"""Return True for leap years, False for non-leap years."""
@@ -359,7 +378,7 @@ class TextCalendar(Calendar):
"""
Returns a formatted week day name.
"""
if width >= 9:
if width >= max(map(len, day_name)):
names = day_name
else:
names = day_abbr
@@ -377,7 +396,7 @@ class TextCalendar(Calendar):
"""
_validate_month(themonth)
s = month_name[themonth]
s = standalone_month_name[themonth]
if withyear:
s = "%s %r" % (s, theyear)
return s.center(width)
@@ -479,30 +498,29 @@ class HTMLCalendar(Calendar):
"""
if day == 0:
# day outside month
return '<td class="%s">&nbsp;</td>' % self.cssclass_noday
return f'<td class="{self.cssclass_noday}">&nbsp;</td>'
else:
return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
return f'<td class="{self.cssclasses[weekday]}">{day}</td>'
def formatweek(self, theweek):
"""
Return a complete week as a table row.
"""
s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
return '<tr>%s</tr>' % s
return f'<tr>{s}</tr>'
def formatweekday(self, day):
"""
Return a weekday name as a table header.
"""
return '<th class="%s">%s</th>' % (
self.cssclasses_weekday_head[day], day_abbr[day])
return f'<th class="{self.cssclasses_weekday_head[day]}">{day_abbr[day]}</th>'
def formatweekheader(self):
"""
Return a header for a week as a table row.
"""
s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
return '<tr>%s</tr>' % s
return f'<tr>{s}</tr>'
def formatmonthname(self, theyear, themonth, withyear=True):
"""
@@ -510,11 +528,10 @@ class HTMLCalendar(Calendar):
"""
_validate_month(themonth)
if withyear:
s = '%s %s' % (month_name[themonth], theyear)
s = f'{standalone_month_name[themonth]} {theyear}'
else:
s = '%s' % month_name[themonth]
return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
self.cssclass_month_head, s)
s = standalone_month_name[themonth]
return f'<tr><th colspan="7" class="{self.cssclass_month_head}">{s}</th></tr>'
def formatmonth(self, theyear, themonth, withyear=True):
"""
@@ -522,8 +539,7 @@ class HTMLCalendar(Calendar):
"""
v = []
a = v.append
a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' % (
self.cssclass_month))
a(f'<table class="{self.cssclass_month}">')
a('\n')
a(self.formatmonthname(theyear, themonth, withyear=withyear))
a('\n')
@@ -543,11 +559,9 @@ class HTMLCalendar(Calendar):
v = []
a = v.append
width = max(width, 1)
a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
self.cssclass_year)
a(f'<table class="{self.cssclass_year}">')
a('\n')
a('<tr><th colspan="%d" class="%s">%s</th></tr>' % (
width, self.cssclass_year_head, theyear))
a(f'<tr><th colspan="{width}" class="{self.cssclass_year_head}">{theyear}</th></tr>')
for i in range(JANUARY, JANUARY+12, width):
# months in this row
months = range(i, min(i+width, 13))
@@ -560,28 +574,47 @@ class HTMLCalendar(Calendar):
a('</table>')
return ''.join(v)
def _format_html_page(self, theyear, content, css, encoding):
"""
Return a complete HTML page with the given content.
"""
if encoding is None:
encoding = 'utf-8'
v = []
a = v.append
a('<!DOCTYPE html>\n')
a('<html lang="en">\n')
a('<head>\n')
a(f'<meta charset="{encoding}">\n')
a('<meta name="viewport" content="width=device-width, initial-scale=1">\n')
a(f'<title>Calendar for {theyear}</title>\n')
a('<style>\n')
a(':root { color-scheme: light dark; }\n')
a('table.year { border: solid; }\n')
a('table.year > tbody > tr > td { border: solid; vertical-align: top; }\n')
a('</style>\n')
if css is not None:
a(f'<link rel="stylesheet" href="{css}">\n')
a('</head>\n')
a('<body>\n')
a(content)
a('</body>\n')
a('</html>\n')
return ''.join(v).encode(encoding, "xmlcharrefreplace")
def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
"""
Return a formatted year as a complete HTML page.
"""
if encoding is None:
encoding = sys.getdefaultencoding()
v = []
a = v.append
a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
a('<html>\n')
a('<head>\n')
a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
if css is not None:
a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
a('<title>Calendar for %d</title>\n' % theyear)
a('</head>\n')
a('<body>\n')
a(self.formatyear(theyear, width))
a('</body>\n')
a('</html>\n')
return ''.join(v).encode(encoding, "xmlcharrefreplace")
content = self.formatyear(theyear, width)
return self._format_html_page(theyear, content, css, encoding)
def formatmonthpage(self, theyear, themonth, *, css='calendar.css', encoding=None):
"""
Return a formatted month as a complete HTML page.
"""
content = self.formatmonth(theyear, themonth)
return self._format_html_page(theyear, content, css, encoding)
class different_locale:
@@ -653,28 +686,61 @@ class _CLIDemoCalendar(TextCalendar):
super().__init__(*args, **kwargs)
self.highlight_day = highlight_day
def _get_theme(self):
from _colorize import get_theme
return get_theme(tty_file=sys.stdout)
def formatday(self, day, weekday, width, *, highlight_day=None):
"""
Returns a formatted day.
"""
if day == 0:
s = ''
else:
s = f'{day:2}'
s = s.center(width)
if day == highlight_day:
theme = self._get_theme().calendar
s = f"{theme.highlight}{s}{theme.reset}"
return s
def formatweek(self, theweek, width, *, highlight_day=None):
"""
Returns a single week in a string (no newline).
"""
if highlight_day:
from _colorize import get_colors
ansi = get_colors()
highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}"
reset = ansi.RESET
else:
highlight = reset = ""
return ' '.join(
(
f"{highlight}{self.formatday(d, wd, width)}{reset}"
if d == highlight_day
else self.formatday(d, wd, width)
)
self.formatday(d, wd, width, highlight_day=highlight_day)
for (d, wd) in theweek
)
def formatweekheader(self, width):
"""
Return a header for a week.
"""
header = super().formatweekheader(width)
theme = self._get_theme().calendar
return f"{theme.weekday}{header}{theme.reset}"
def formatmonthname(self, theyear, themonth, width, withyear=True):
"""
Return a formatted month name.
"""
name = super().formatmonthname(theyear, themonth, width, withyear)
theme = self._get_theme().calendar
if (
self.highlight_day
and self.highlight_day.year == theyear
and self.highlight_day.month == themonth
):
color = theme.highlight
name_only = name.strip()
colored_name = f"{color}{name_only}{theme.reset}"
return name.replace(name_only, colored_name, 1)
else:
color = theme.header
return f"{color}{name}{theme.reset}"
def formatmonth(self, theyear, themonth, w=0, l=0):
"""
Return a month's calendar string (multi-line).
@@ -709,7 +775,9 @@ class _CLIDemoCalendar(TextCalendar):
colwidth = (w + 1) * 7 - 1
v = []
a = v.append
a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
theme = self._get_theme().calendar
year = repr(theyear).center(colwidth*m+c*(m-1)).rstrip()
a(f"{theme.header}{year}{theme.reset}")
a('\n'*l)
header = self.formatweekheader(w)
for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
@@ -810,28 +878,30 @@ def timegm(tuple):
def main(args=None):
import argparse
parser = argparse.ArgumentParser(color=True)
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
textgroup = parser.add_argument_group('text only arguments')
htmlgroup = parser.add_argument_group('html only arguments')
textgroup.add_argument(
"-w", "--width",
type=int, default=2,
help="width of date column (default 2)"
help="width of date column"
)
textgroup.add_argument(
"-l", "--lines",
type=int, default=1,
help="number of lines for each week (default 1)"
help="number of lines for each week"
)
textgroup.add_argument(
"-s", "--spacing",
type=int, default=6,
help="spacing between months (default 6)"
help="spacing between months"
)
textgroup.add_argument(
"-m", "--months",
type=int, default=3,
help="months per row (default 3)"
help="months per row"
)
htmlgroup.add_argument(
"-c", "--css",
@@ -852,12 +922,12 @@ 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",
type=int, default=0,
help="weekday (0 is Monday, 6 is Sunday) to start each week (default 0)"
help="weekday (0 is Monday, 6 is Sunday) to start each week"
)
parser.add_argument(
"year",
@@ -867,7 +937,7 @@ def main(args=None):
parser.add_argument(
"month",
nargs='?', type=int,
help="month number (1-12, text only)"
help="month number (1-12)"
)
options = parser.parse_args(args)
@@ -880,9 +950,6 @@ def main(args=None):
today = datetime.date.today()
if options.type == "html":
if options.month:
parser.error("incorrect number of arguments")
sys.exit(1)
if options.locale:
cal = LocaleHTMLCalendar(locale=locale)
else:
@@ -890,13 +957,17 @@ def main(args=None):
cal.setfirstweekday(options.first_weekday)
encoding = options.encoding
if encoding is None:
encoding = sys.getdefaultencoding()
encoding = 'utf-8'
optdict = dict(encoding=encoding, css=options.css)
write = sys.stdout.buffer.write
if options.year is None:
write(cal.formatyearpage(today.year, **optdict))
else:
write(cal.formatyearpage(options.year, **optdict))
if options.month:
write(cal.formatmonthpage(options.year, options.month, **optdict))
else:
write(cal.formatyearpage(options.year, **optdict))
else:
if options.locale:
cal = _CLIDemoLocaleCalendar(highlight_day=today, locale=locale)
+223 -29
View File
@@ -26,6 +26,45 @@ __all__ = ["getpass","getuser","GetPassWarning"]
class GetPassWarning(UserWarning): pass
# Default POSIX control character mappings
_POSIX_CTRL_CHARS = frozendict({
'BS': '\x08', # Backspace
'ERASE': '\x7f', # DEL
'KILL': '\x15', # Ctrl+U - kill line
'WERASE': '\x17', # Ctrl+W - erase word
'LNEXT': '\x16', # Ctrl+V - literal next
'EOF': '\x04', # Ctrl+D - EOF
'INTR': '\x03', # Ctrl+C - interrupt
'SOH': '\x01', # Ctrl+A - start of heading (beginning of line)
'ENQ': '\x05', # Ctrl+E - enquiry (end of line)
'VT': '\x0b', # Ctrl+K - vertical tab (kill forward)
})
def _get_terminal_ctrl_chars(fd):
"""Extract control characters from terminal settings.
Returns a dict mapping control char names to their str values.
Falls back to POSIX defaults if termios is not available
or if the control character is not supported by termios.
"""
ctrl = dict(_POSIX_CTRL_CHARS)
try:
old = termios.tcgetattr(fd)
cc = old[6] # Index 6 is the control characters array
except (termios.error, OSError):
return ctrl
# Use defaults for Backspace (BS) and Ctrl+A/E/K (SOH/ENQ/VT)
# as they are not in the termios control characters array.
for name in ('ERASE', 'KILL', 'WERASE', 'LNEXT', 'EOF', 'INTR'):
cap = getattr(termios, f'V{name}')
if cap < len(cc):
ctrl[name] = cc[cap].decode('latin-1')
return ctrl
def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
"""Prompt for a password, with echo turned off.
@@ -73,15 +112,27 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
old = termios.tcgetattr(fd) # a copy to save
new = old[:]
new[3] &= ~termios.ECHO # 3 == 'lflags'
# Extract control characters before changing terminal mode.
term_ctrl_chars = None
if echo_char:
# ICANON enables canonical (line-buffered) mode where
# the terminal handles line editing. Disable it so we
# can read input char by char and handle editing ourselves.
new[3] &= ~termios.ICANON
# IEXTEN enables implementation-defined input processing
# such as LNEXT (Ctrl+V). Disable it so the terminal
# driver does not intercept these characters before our
# code can handle them.
new[3] &= ~termios.IEXTEN
term_ctrl_chars = _get_terminal_ctrl_chars(fd)
tcsetattr_flags = termios.TCSAFLUSH
if hasattr(termios, 'TCSASOFT'):
tcsetattr_flags |= termios.TCSASOFT
try:
termios.tcsetattr(fd, tcsetattr_flags, new)
passwd = _raw_input(prompt, stream, input=input,
echo_char=echo_char)
echo_char=echo_char,
term_ctrl_chars=term_ctrl_chars)
finally:
termios.tcsetattr(fd, tcsetattr_flags, old)
@@ -159,7 +210,8 @@ def _check_echo_char(echo_char):
f"character, got: {echo_char!r}")
def _raw_input(prompt="", stream=None, input=None, echo_char=None):
def _raw_input(prompt="", stream=None, input=None, echo_char=None,
term_ctrl_chars=None):
# This doesn't save the string in the GNU readline history.
if not stream:
stream = sys.stderr
@@ -177,7 +229,8 @@ def _raw_input(prompt="", stream=None, input=None, echo_char=None):
stream.flush()
# NOTE: The Python C API calls flockfile() (and unlock) during readline.
if echo_char:
return _readline_with_echo_char(stream, input, echo_char)
return _readline_with_echo_char(stream, input, echo_char,
term_ctrl_chars, prompt)
line = input.readline()
if not line:
raise EOFError
@@ -186,33 +239,174 @@ def _raw_input(prompt="", stream=None, input=None, echo_char=None):
return line
def _readline_with_echo_char(stream, input, echo_char):
passwd = ""
eof_pressed = False
while True:
char = input.read(1)
if char == '\n' or char == '\r':
break
elif char == '\x03':
raise KeyboardInterrupt
elif char == '\x7f' or char == '\b':
if passwd:
stream.write("\b \b")
stream.flush()
passwd = passwd[:-1]
elif char == '\x04':
if eof_pressed:
break
else:
eof_pressed = True
elif char == '\x00':
continue
def _readline_with_echo_char(stream, input, echo_char, term_ctrl_chars=None,
prompt=""):
"""Read password with echo character and line editing support."""
if term_ctrl_chars is None:
term_ctrl_chars = _POSIX_CTRL_CHARS
editor = _PasswordLineEditor(stream, echo_char, term_ctrl_chars, prompt)
return editor.readline(input)
class _PasswordLineEditor:
"""Handles line editing for password input with echo character."""
def __init__(self, stream, echo_char, ctrl_chars, prompt=""):
self.stream = stream
self.echo_char = echo_char
self.prompt = prompt
self.password = []
self.cursor_pos = 0
self.eof_pressed = False
self.literal_next = False
self.ctrl = ctrl_chars
self.dispatch = {
ctrl_chars['SOH']: self.handle_move_start, # Ctrl+A
ctrl_chars['ENQ']: self.handle_move_end, # Ctrl+E
ctrl_chars['VT']: self.handle_kill_forward, # Ctrl+K
ctrl_chars['KILL']: self.handle_kill_line, # Ctrl+U
ctrl_chars['WERASE']: self.handle_erase_word, # Ctrl+W
ctrl_chars['ERASE']: self.handle_erase, # DEL
ctrl_chars['BS']: self.handle_erase, # Backspace
# special characters
ctrl_chars['LNEXT']: self.handle_literal_next, # Ctrl+V
ctrl_chars['EOF']: self.handle_eof, # Ctrl+D
ctrl_chars['INTR']: self.handle_interrupt, # Ctrl+C
'\x00': self.handle_nop, # ignore NUL
}
def refresh_display(self, prev_len=None):
"""Redraw the entire password line with *echo_char*.
If *prev_len* is not specified, the current password length is used.
"""
prompt_len = len(self.prompt)
clear_len = prev_len if prev_len is not None else len(self.password)
# Clear the entire line (prompt + password) and rewrite.
self.stream.write('\r' + ' ' * (prompt_len + clear_len) + '\r')
self.stream.write(self.prompt + self.echo_char * len(self.password))
if self.cursor_pos < len(self.password):
self.stream.write('\b' * (len(self.password) - self.cursor_pos))
self.stream.flush()
def insert_char(self, char):
"""Insert *char* at cursor position."""
self.password.insert(self.cursor_pos, char)
self.cursor_pos += 1
# Only refresh if inserting in middle.
if self.cursor_pos < len(self.password):
self.refresh_display()
else:
passwd += char
stream.write(echo_char)
stream.flush()
eof_pressed = False
return passwd
self.stream.write(self.echo_char)
self.stream.flush()
def is_eol(self, char):
"""Check if *char* is a line terminator."""
return char in ('\r', '\n')
def is_eof(self, char):
"""Check if *char* is a file terminator."""
return char == self.ctrl['EOF']
def handle_move_start(self):
"""Move cursor to beginning (Ctrl+A)."""
self.cursor_pos = 0
self.refresh_display()
def handle_move_end(self):
"""Move cursor to end (Ctrl+E)."""
self.cursor_pos = len(self.password)
self.refresh_display()
def handle_erase(self):
"""Delete character before cursor (Backspace/DEL)."""
if self.cursor_pos == 0:
return
assert self.cursor_pos > 0
self.cursor_pos -= 1
prev_len = len(self.password)
del self.password[self.cursor_pos]
self.refresh_display(prev_len)
def handle_kill_line(self):
"""Erase entire line (Ctrl+U)."""
prev_len = len(self.password)
self.password.clear()
self.cursor_pos = 0
self.refresh_display(prev_len)
def handle_kill_forward(self):
"""Kill from cursor to end (Ctrl+K)."""
prev_len = len(self.password)
del self.password[self.cursor_pos:]
self.refresh_display(prev_len)
def handle_erase_word(self):
"""Erase previous word (Ctrl+W)."""
old_cursor = self.cursor_pos
# Calculate the starting position of the previous word,
# ignoring trailing whitespaces.
while self.cursor_pos > 0 and self.password[self.cursor_pos - 1] == ' ':
self.cursor_pos -= 1
while self.cursor_pos > 0 and self.password[self.cursor_pos - 1] != ' ':
self.cursor_pos -= 1
# Delete the previous word and refresh the screen.
prev_len = len(self.password)
del self.password[self.cursor_pos:old_cursor]
self.refresh_display(prev_len)
def handle_literal_next(self):
"""State transition to indicate that the next character is literal."""
assert self.literal_next is False
self.literal_next = True
def handle_eof(self):
"""State transition to indicate that the pressed character was EOF."""
assert self.eof_pressed is False
self.eof_pressed = True
def handle_interrupt(self):
"""Raise a KeyboardInterrupt after Ctrl+C has been received."""
raise KeyboardInterrupt
def handle_nop(self):
"""Handler for an ignored character."""
def handle(self, char):
"""Handle a single character input. Returns True if handled."""
handler = self.dispatch.get(char)
if handler:
handler()
return True
return False
def readline(self, input):
"""Read a line of password input with echo character support."""
while True:
assert self.cursor_pos >= 0
char = input.read(1)
if self.is_eol(char):
break
# Handle literal next mode first as Ctrl+V quotes characters.
elif self.literal_next:
self.insert_char(char)
self.literal_next = False
# Handle EOF now as Ctrl+D must be pressed twice
# consecutively to stop reading from the input.
elif self.is_eof(char):
if self.eof_pressed:
break
elif self.handle(char):
# Dispatched to handler.
pass
else:
# Insert as normal character.
self.insert_char(char)
self.eof_pressed = self.is_eof(char)
return ''.join(self.password)
def getuser():
+38 -22
View File
@@ -21,8 +21,6 @@ Public functions: Internaldate2tuple
# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
# IDLE contributed by Forest <forestix@nom.one> August 2024.
__version__ = "2.60"
import binascii, errno, random, re, socket, subprocess, sys, time, calendar
from datetime import datetime, timezone, timedelta
from io import DEFAULT_BUFFER_SIZE
@@ -258,7 +256,6 @@ class IMAP4:
self._cmd_log_idx = 0
self._cmd_log = {} # Last '_cmd_log_len' interactions
if self.debug >= 1:
self._mesg('imaplib version %s' % __version__)
self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
self.welcome = self._get_response()
@@ -331,25 +328,34 @@ class IMAP4:
self.host = host
self.port = port
self.sock = self._create_socket(timeout)
self._file = self.sock.makefile('rb')
# Since IMAP4 implements its own read() and readline() buffering,
# the '_imaplib_file' attribute is unused. Nonetheless it is kept
# and exposed solely for backward compatibility purposes.
self._imaplib_file = self.sock.makefile('rb')
@property
def file(self):
# The old 'file' attribute is no longer used now that we do our own
# read() and readline() buffering, with which it conflicts.
# As an undocumented interface, it should never have been accessed by
# external code, and therefore does not warrant deprecation.
# Nevertheless, we provide this property for now, to avoid suddenly
# breaking any code in the wild that might have been using it in a
# harmless way.
import warnings
warnings.warn(
'IMAP4.file is unsupported, can cause errors, and may be removed.',
RuntimeWarning,
stacklevel=2)
return self._file
warnings._deprecated("IMAP4.file", remove=(3, 19))
return self._imaplib_file
@file.setter
def file(self, value):
import warnings
warnings._deprecated("IMAP4.file", remove=(3, 19))
# Ideally, we would want to close the previous file,
# but since we do not know how subclasses will use
# that setter, it is probably better to leave it to
# the caller.
self._imaplib_file = value
def _close_imaplib_file(self):
file = self._imaplib_file
if file is not None:
try:
file.close()
except OSError:
pass
def read(self, size):
"""Read 'size' bytes from remote."""
@@ -435,7 +441,7 @@ class IMAP4:
def shutdown(self):
"""Close I/O established in "open"."""
self._file.close()
self._close_imaplib_file()
try:
self.sock.shutdown(socket.SHUT_RDWR)
except OSError as exc:
@@ -952,9 +958,10 @@ class IMAP4:
ssl_context = ssl._create_stdlib_context()
typ, dat = self._simple_command(name)
if typ == 'OK':
self._close_imaplib_file()
self.sock = ssl_context.wrap_socket(self.sock,
server_hostname=self.host)
self._file = self.sock.makefile('rb')
self._imaplib_file = self.sock.makefile('rb')
self._tls_established = True
self._get_capabilities()
else:
@@ -1392,7 +1399,7 @@ class IMAP4:
try:
self._get_response()
except self.abort as val:
except self.abort:
if __debug__:
if self.debug >= 1:
self.print_log()
@@ -1800,7 +1807,7 @@ class IMAP4_stream(IMAP4):
self.host = None # For compatibility with parent class
self.port = None
self.sock = None
self._file = None
self._imaplib_file = None
self.process = subprocess.Popen(self.command,
bufsize=DEFAULT_BUFFER_SIZE,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -1988,7 +1995,7 @@ if __name__ == '__main__':
try:
optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
except getopt.error as val:
except getopt.error:
optlist, args = (), ()
stream_command = None
@@ -2083,3 +2090,12 @@ try: %s -d5
''' % sys.argv[0])
raise
def __getattr__(name):
if name == "__version__":
from warnings import _deprecated
_deprecated("__version__", remove=(3, 20))
return "2.60" # Do not change
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+7 -2
View File
@@ -39,6 +39,13 @@ class Mailbox:
self._path = os.path.abspath(os.path.expanduser(path))
self._factory = factory
def __enter__(self):
self.lock()
return self
def __exit__(self, type, value, traceback):
self.close()
def add(self, message):
"""Add message and return assigned key."""
raise NotImplementedError('Method must be implemented by subclass')
@@ -2090,8 +2097,6 @@ class _ProxyFile:
return False
return self._file.closed
__class_getitem__ = classmethod(GenericAlias)
class _PartialFile(_ProxyFile):
"""A read-only wrapper of part of a file."""
+613 -26
View File
@@ -7,7 +7,6 @@ import inspect
import io
import operator
import os
import py_compile
import shutil
import stat
import sys
@@ -81,6 +80,108 @@ class StdStreamTest(unittest.TestCase):
self.assertRegex(mocked_stderr.getvalue(), r'usage:')
class TestLazyImports(unittest.TestCase):
LAZY_IMPORTS = {
"_colorize",
"copy",
"difflib",
"shutil",
"textwrap",
"warnings",
}
def test_module_import(self):
import_helper.ensure_lazy_imports(
"argparse",
self.LAZY_IMPORTS,
)
def test_create_parser(self):
# Test imports are still unused after
# creating a parser
create_parser = "argparse.ArgumentParser()"
imported_modules = {"shutil"}
import_helper.ensure_lazy_imports(
"argparse",
self.LAZY_IMPORTS - imported_modules,
additional_code=create_parser,
)
def test_add_subparser(self):
add_subparser = textwrap.dedent(
"""
parser = argparse.ArgumentParser()
parser.add_subparsers(dest='command', required=False)
"""
)
imported_modules = {"shutil"}
import_helper.ensure_lazy_imports(
"argparse",
self.LAZY_IMPORTS - imported_modules,
additional_code=add_subparser,
)
def test_parse_args(self):
example_parser = textwrap.dedent(
"""
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('-f', '--foo')
parser.add_argument('bar')
parser.parse_args(['BAR'])
parser.parse_args(['BAR', '--foo', 'FOO'])
"""
)
imported_modules = {"shutil"}
import_helper.ensure_lazy_imports(
"argparse",
self.LAZY_IMPORTS - imported_modules,
additional_code=example_parser
)
class TestArgumentParserCopiable(unittest.TestCase):
def _get_parser(self):
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('--foo', type=int, default=42)
parser.add_argument('bar', nargs='?', default='baz')
return parser
@force_not_colorized
def test_copiable(self):
import copy
parser = self._get_parser()
parser2 = copy.copy(parser)
ns = parser2.parse_args(['--foo', '123', 'quux'])
self.assertEqual(ns.foo, 123)
self.assertEqual(ns.bar, 'quux')
ns2 = parser2.parse_args([])
self.assertEqual(ns2.foo, 42)
self.assertEqual(ns2.bar, 'baz')
# Test shallow copy also gets new arguments
parser.add_argument("--extra")
ns3 = parser2.parse_args(["--extra", "bar"])
self.assertEqual(ns3.extra, "bar")
@force_not_colorized
def test_deepcopiable(self):
import copy
parser = self._get_parser()
parser2 = copy.deepcopy(parser)
ns = parser2.parse_args(['--foo', '123', 'quux'])
self.assertEqual(ns.foo, 123)
self.assertEqual(ns.bar, 'quux')
ns2 = parser2.parse_args([])
self.assertEqual(ns2.foo, 42)
self.assertEqual(ns2.bar, 'baz')
# Test deep copy does not get new arguments
parser.add_argument("--extra")
with self.assertRaises(argparse.ArgumentError):
parser2.parse_args(["--extra", "bar"])
class TestArgumentParserPickleable(unittest.TestCase):
@force_not_colorized
@@ -602,13 +703,22 @@ class TestOptionalsShortLong(ParserTestCase):
class TestOptionalsDest(ParserTestCase):
"""Tests various means of setting destination"""
argument_signatures = [Sig('--foo-bar'), Sig('--baz', dest='zabbaz')]
argument_signatures = [
Sig('-x', '-foobar', '--foo-bar', '-barfoo', '-X'),
Sig('--baz', dest='zabbaz'),
Sig('-y', '-qux', '-Y'),
Sig('-z'),
]
failures = ['a']
successes = [
('--foo-bar f', NS(foo_bar='f', zabbaz=None)),
('--baz g', NS(foo_bar=None, zabbaz='g')),
('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i')),
('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j')),
('--foo-bar f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)),
('-x f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)),
('--baz g', NS(foo_bar=None, zabbaz='g', qux=None, z=None)),
('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i', qux=None, z=None)),
('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j', qux=None, z=None)),
('-qux l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)),
('-y l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)),
('-z m', NS(foo_bar=None, zabbaz=None, qux=None, z='m')),
]
@@ -817,6 +927,76 @@ class TestBooleanOptionalAction(ParserTestCase):
self.assertEqual(str(cm.exception),
"invalid option name '--no-foo' for BooleanOptionalAction")
class TestBooleanOptionalActionSingleDash(ParserTestCase):
"""Tests BooleanOptionalAction with single dash"""
argument_signatures = [
Sig('-foo', '-x', action=argparse.BooleanOptionalAction),
]
failures = ['--foo', '--no-foo', '-no-foo', '-no-x', '-nox']
successes = [
('', NS(foo=None)),
('-foo', NS(foo=True)),
('-nofoo', NS(foo=False)),
('-x', NS(foo=True)),
]
def test_invalid_name(self):
parser = argparse.ArgumentParser()
with self.assertRaises(ValueError) as cm:
parser.add_argument('-nofoo', action=argparse.BooleanOptionalAction)
self.assertEqual(str(cm.exception),
"invalid option name '-nofoo' for BooleanOptionalAction")
class TestBooleanOptionalActionAlternatePrefixChars(ParserTestCase):
"""Tests BooleanOptionalAction with custom prefixes"""
parser_signature = Sig(prefix_chars='+-', add_help=False)
argument_signatures = [Sig('++foo', action=argparse.BooleanOptionalAction)]
failures = ['--foo', '--no-foo']
successes = [
('', NS(foo=None)),
('++foo', NS(foo=True)),
('++no-foo', NS(foo=False)),
]
def test_invalid_name(self):
parser = argparse.ArgumentParser(prefix_chars='+/')
with self.assertRaisesRegex(ValueError,
'BooleanOptionalAction.*is not valid for positional arguments'):
parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
with self.assertRaises(ValueError) as cm:
parser.add_argument('++no-foo', action=argparse.BooleanOptionalAction)
self.assertEqual(str(cm.exception),
"invalid option name '++no-foo' for BooleanOptionalAction")
class TestBooleanOptionalActionSingleAlternatePrefixChar(ParserTestCase):
"""Tests BooleanOptionalAction with single alternate prefix char"""
parser_signature = Sig(prefix_chars='+/', add_help=False)
argument_signatures = [
Sig('+foo', '+x', action=argparse.BooleanOptionalAction),
]
failures = ['++foo', '++no-foo', '++nofoo',
'-no-foo', '-nofoo', '+no-foo', '-nofoo',
'+no-x', '+nox', '-no-x', '-nox']
successes = [
('', NS(foo=None)),
('+foo', NS(foo=True)),
('+nofoo', NS(foo=False)),
('+x', NS(foo=True)),
]
def test_invalid_name(self):
parser = argparse.ArgumentParser(prefix_chars='+/')
with self.assertRaisesRegex(ValueError,
'BooleanOptionalAction.*is not valid for positional arguments'):
parser.add_argument('-foo', action=argparse.BooleanOptionalAction)
with self.assertRaises(ValueError) as cm:
parser.add_argument('+nofoo', action=argparse.BooleanOptionalAction)
self.assertEqual(str(cm.exception),
"invalid option name '+nofoo' for BooleanOptionalAction")
class TestBooleanOptionalActionRequired(ParserTestCase):
"""Tests BooleanOptionalAction required"""
@@ -1856,7 +2036,7 @@ BIN_STDERR_SENTINEL = object()
class StdStreamComparer:
def __init__(self, attr):
# We try to use the actual stdXXX.buffer attribute as our
# marker, but but under some test environments,
# marker, but under some test environments,
# sys.stdout/err are replaced by io.StringIO which won't have .buffer,
# so we use a sentinel simply to show that the tests do the right thing
# for any buffer supporting object
@@ -2304,11 +2484,12 @@ class TestNegativeNumber(ParserTestCase):
('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
]
@force_not_colorized_test_class
class TestArgumentAndSubparserSuggestions(TestCase):
"""Test error handling and suggestion when a user makes a typo"""
def test_wrong_argument_error_with_suggestions(self):
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
parser = ErrorRaisingArgumentParser()
parser.add_argument('foo', choices=['bar', 'baz'])
with self.assertRaises(ArgumentParserError) as excinfo:
parser.parse_args(('bazz',))
@@ -2328,7 +2509,7 @@ class TestArgumentAndSubparserSuggestions(TestCase):
)
def test_wrong_argument_subparsers_with_suggestions(self):
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
parser = ErrorRaisingArgumentParser()
subparsers = parser.add_subparsers(required=True)
subparsers.add_parser('foo')
subparsers.add_parser('bar')
@@ -2352,18 +2533,19 @@ class TestArgumentAndSubparserSuggestions(TestCase):
excinfo.exception.stderr,
)
def test_wrong_argument_no_suggestion_implicit(self):
parser = ErrorRaisingArgumentParser()
def test_wrong_argument_with_suggestion_explicit(self):
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
parser.add_argument('foo', choices=['bar', 'baz'])
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', maybe you meant"
" 'baz'? (choose from 'bar', 'baz')",
excinfo.exception.stderr,
)
def test_suggestions_choices_empty(self):
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
parser = ErrorRaisingArgumentParser()
parser.add_argument('foo', choices=[])
with self.assertRaises(ArgumentParserError) as excinfo:
parser.parse_args(('bazz',))
@@ -2373,7 +2555,7 @@ class TestArgumentAndSubparserSuggestions(TestCase):
)
def test_suggestions_choices_int(self):
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
parser = ErrorRaisingArgumentParser()
parser.add_argument('foo', choices=[1, 2])
with self.assertRaises(ArgumentParserError) as excinfo:
parser.parse_args(('3',))
@@ -2383,7 +2565,7 @@ class TestArgumentAndSubparserSuggestions(TestCase):
)
def test_suggestions_choices_mixed_types(self):
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
parser = ErrorRaisingArgumentParser()
parser.add_argument('foo', choices=[1, '2'])
with self.assertRaises(ArgumentParserError) as excinfo:
parser.parse_args(('3',))
@@ -2710,6 +2892,16 @@ class TestAddSubparsers(TestCase):
ret = parser.parse_args(())
self.assertIsNone(ret.command)
def test_subparser_help_with_parent_required_optional(self):
parser = ErrorRaisingArgumentParser(prog='PROG')
parser.add_argument('--foo', required=True)
parser.add_argument('--bar')
subparsers = parser.add_subparsers()
parser_sub = subparsers.add_parser('sub')
parser_sub.add_argument('arg')
self.assertEqual(parser_sub.format_usage(),
'usage: PROG --foo FOO sub [-h] arg\n')
def test_help(self):
self.assertEqual(self.parser.format_usage(),
'usage: PROG [-h] [--foo] bar {1,2,3} ...\n')
@@ -5626,6 +5818,11 @@ class TestHelpCustomHelpFormatter(TestCase):
a-very-long-command command that does something
'''))
def test_direct_formatter_instantiation(self):
formatter = argparse.HelpFormatter(prog="program")
formatter.add_usage(usage=None, actions=[], groups=[])
help_text = formatter.format_help()
self.assertEqual(help_text, "usage: program\n")
# =====================================
# Optional/Positional constructor tests
@@ -5660,6 +5857,8 @@ class TestInvalidArgumentConstructors(TestCase):
self.assertTypeError('-', errmsg='dest= is required')
self.assertTypeError('--', errmsg='dest= is required')
self.assertTypeError('---', errmsg='dest= is required')
self.assertTypeError('-', '--', '---',
errmsg="dest= is required for options like '-', '--', '---'")
def test_invalid_prefix(self):
self.assertValueError('--foo', '+foo',
@@ -6197,6 +6396,7 @@ class TestTypeFunctionCallOnlyOnce(TestCase):
# Check that deprecated arguments output warning
# ==============================================
@force_not_colorized_test_class
class TestDeprecatedArguments(TestCase):
def test_deprecated_option(self):
@@ -6507,6 +6707,20 @@ class TestDoubleDash(TestCase):
args = parser.parse_args(['--foo', 'a', '--', 'b', '--', 'c'])
self.assertEqual(NS(foo='a', bar=['--', 'b', '--', 'c']), args)
def test_optional_remainder(self):
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('--foo', nargs='...')
parser.add_argument('bar', nargs='*')
args = parser.parse_args(['--', '--foo', 'a', 'b'])
self.assertEqual(NS(foo=None, bar=['--foo', 'a', 'b']), args)
args = parser.parse_args(['--foo', '--', 'a', 'b'])
self.assertEqual(NS(foo=['--', 'a', 'b'], bar=[]), args)
args = parser.parse_args(['--foo', 'a', '--', 'b'])
self.assertEqual(NS(foo=['a', '--', 'b'], bar=[]), args)
args = parser.parse_args(['--foo', 'a', 'b', '--'])
self.assertEqual(NS(foo=['a', 'b', '--'], bar=[]), args)
def test_subparser(self):
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('foo')
@@ -7084,9 +7298,8 @@ class TestProgName(TestCase):
script_name = script_helper.make_script(dirname, basename, self.source)
if not compiled:
return script_name
py_compile.compile(script_name, doraise=True)
pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True)
os.remove(script_name)
pyc_file = import_helper.make_legacy_pyc(script_name)
return pyc_file
def make_zip_script(self, script_name, name_in_zip=None):
@@ -7132,6 +7345,9 @@ class TestProgName(TestCase):
basename = 'module' + os_helper.FS_NONASCII
modulename = f'{self.dirname}.{basename}'
self.make_script(self.dirname, basename)
# The filesystem encoding may be non-UTF-8,
# but the runner runs in UTF-8 mode
modulename = os.fsencode(modulename).decode("utf-8", "surrogateescape")
runner_source = textwrap.dedent(f'''\
import runpy
runpy._run_module_as_main({modulename!r}, alter_argv=False)
@@ -7243,6 +7459,13 @@ class TestColorized(TestCase):
choices=("Aaaaa", "Bbbbb", "Ccccc", "Ddddd"),
help="pick one",
)
parser.add_argument(
"--optional8",
default="A",
metavar="X",
choices=("A", "B", "C"),
help="among %(choices)s, default is %(default)s",
)
parser.add_argument("+f")
parser.add_argument("++bar")
@@ -7268,6 +7491,8 @@ class TestColorized(TestCase):
short_b = self.theme.short_option
label_b = self.theme.label
pos_b = self.theme.action
default = self.theme.default
interp = self.theme.interpolated_value
reset = self.theme.reset
# Act
@@ -7280,8 +7505,8 @@ class TestColorized(TestCase):
f"""\
{heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}-v{reset} | {short}-q{reset}] [{short}-o{reset}] [{long}--optional2 {label}OPTIONAL2{reset}] [{long}--optional3 {label}{{X,Y,Z}}{reset}]
[{long}--optional4 {label}{{X,Y,Z}}{reset}] [{long}--optional5 {label}{{X,Y,Z}}{reset}] [{long}--optional6 {label}{{X,Y,Z}}{reset}]
[{short}-p {label}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}] [{short}+f {label}F{reset}] [{long}++bar {label}BAR{reset}] [{long}-+baz {label}BAZ{reset}]
[{short}-c {label}COUNT{reset}]
[{short}-p {label}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}] [{long}--optional8 {label}X{reset}] [{short}+f {label}F{reset}] [{long}++bar {label}BAR{reset}]
[{long}-+baz {label}BAZ{reset}] [{short}-c {label}COUNT{reset}]
{pos}x{reset} {pos}y{reset} {pos}this_indeed_is_a_very_long_action_name{reset} {pos}{{sub1,sub2}} ...{reset}
Colorful help
@@ -7294,17 +7519,18 @@ class TestColorized(TestCase):
{heading}options:{reset}
{short_b}-h{reset}, {long_b}--help{reset} show this help message and exit
{short_b}-v{reset}, {long_b}--verbose{reset} more spam (default: False)
{short_b}-q{reset}, {long_b}--quiet{reset} less spam (default: False)
{short_b}-v{reset}, {long_b}--verbose{reset} more spam {default}(default: {reset}{interp}False{reset}{default}){reset}
{short_b}-q{reset}, {long_b}--quiet{reset} less spam {default}(default: {reset}{interp}False{reset}{default}){reset}
{short_b}-o{reset}, {long_b}--optional1{reset}
{long_b}--optional2{reset} {label_b}OPTIONAL2{reset}
pick one (default: None)
pick one {default}(default: {reset}{interp}None{reset}{default}){reset}
{long_b}--optional3{reset} {label_b}{{X,Y,Z}}{reset}
{long_b}--optional4{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None)
{long_b}--optional5{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None)
{long_b}--optional6{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None)
{long_b}--optional4{reset} {label_b}{{X,Y,Z}}{reset} pick one {default}(default: {reset}{interp}None{reset}{default}){reset}
{long_b}--optional5{reset} {label_b}{{X,Y,Z}}{reset} pick one {default}(default: {reset}{interp}None{reset}{default}){reset}
{long_b}--optional6{reset} {label_b}{{X,Y,Z}}{reset} pick one {default}(default: {reset}{interp}None{reset}{default}){reset}
{short_b}-p{reset}, {long_b}--optional7{reset} {label_b}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}
pick one (default: None)
pick one {default}(default: {reset}{interp}None{reset}{default}){reset}
{long_b}--optional8{reset} {label_b}X{reset} among {interp}A, B, C{reset}, default is {interp}A{reset}
{short_b}+f{reset} {label_b}F{reset}
{long_b}++bar{reset} {label_b}BAR{reset}
{long_b}-+baz{reset} {label_b}BAZ{reset}
@@ -7370,6 +7596,62 @@ class TestColorized(TestCase):
),
)
def test_argparse_color_wrapping_matches_uncolored(self):
# gh-142035: color codes must not affect where help text wraps.
# Stripping the escapes from colored help must yield exactly the
# same text as the uncolored help across representative widths.
def build(color, path="output.txt"):
parser = argparse.ArgumentParser(prog="PROG", color=color)
parser.add_argument(
"--mode",
default="auto",
choices=("auto", "fast", "slow"),
help="select the operating mode from the available choices "
"%(choices)s and note the default is %(default)s here",
)
parser.add_argument(
"--path",
default=path,
help="write output to %(default)s and continue processing",
)
return parser
env = self.enterContext(os_helper.EnvironmentVarGuard())
paths = (
"output.txt",
"/var/lib/application/cache/unusually_long_generated_filename",
"production-read-only-replica",
)
for path in paths:
for columns in ("80", "60", "45", "30", "20"):
with self.subTest(path=path, columns=columns):
env["COLUMNS"] = columns
colored = build(color=True, path=path).format_help()
plain = build(color=False, path=path).format_help()
self.assertIn(
f"{self.theme.interpolated_value}auto"
f"{self.theme.reset}",
colored,
)
self.assertEqual(_colorize.decolor(colored), plain)
def test_argparse_color_preserved_when_wrapping_between_words(self):
parser = argparse.ArgumentParser(prog="PROG", color=True)
parser.add_argument(
"--mode", default="auto",
help="select the %(default)s operating mode from the available "
"options and continue with several more words",
)
env = self.enterContext(os_helper.EnvironmentVarGuard())
env["COLUMNS"] = "40"
help_text = parser.format_help()
self.assertIn(
f"{self.theme.interpolated_value}auto{self.theme.reset}",
help_text,
)
def test_custom_formatter_function(self):
def custom_formatter(prog):
return argparse.RawTextHelpFormatter(prog, indent_increment=5)
@@ -7454,6 +7736,311 @@ class TestColorized(TestCase):
help_text = demo_parser.format_help()
self.assertNotIn('\x1b[', help_text)
def test_error_and_warning_keywords_colorized(self):
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('foo')
with self.assertRaises(SystemExit):
with captured_stderr() as stderr:
parser.parse_args([])
err = stderr.getvalue()
error_color = self.theme.error
reset = self.theme.reset
self.assertIn(f'{error_color}error:{reset}', err)
with captured_stderr() as stderr:
parser._warning('test warning')
warn = stderr.getvalue()
warning_color = self.theme.warning
self.assertIn(f'{warning_color}warning:{reset}', warn)
def test_error_and_warning_not_colorized_when_disabled(self):
parser = argparse.ArgumentParser(prog='PROG', color=False)
parser.add_argument('foo')
with self.assertRaises(SystemExit):
with captured_stderr() as stderr:
parser.parse_args([])
err = stderr.getvalue()
self.assertNotIn('\x1b[', err)
self.assertIn('error:', err)
with captured_stderr() as stderr:
parser._warning('test warning')
warn = stderr.getvalue()
self.assertNotIn('\x1b[', warn)
self.assertIn('warning:', warn)
def test_backtick_markup_in_epilog(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
epilog='Example: `python -m myapp --verbose`',
)
prog_extra = self.theme.prog_extra
reset = self.theme.reset
help_text = parser.format_help()
self.assertIn(f'Example: {prog_extra}python -m myapp --verbose{reset}',
help_text)
self.assertNotIn('`', help_text)
def test_backtick_markup_in_description(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
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 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``.',
)
prog_extra = self.theme.prog_extra
reset = self.theme.reset
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='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):
parser = argparse.ArgumentParser(
prog='myapp',
color=True,
epilog='Run `%(prog)s --help` for more info.',
)
prog_extra = self.theme.prog_extra
reset = self.theme.reset
help_text = parser.format_help()
self.assertIn(f'{prog_extra}myapp --help{reset}', help_text)
def test_backtick_markup_in_subparser(self):
parser = argparse.ArgumentParser(prog='PROG', color=True)
subparsers = parser.add_subparsers()
sub = subparsers.add_parser(
'sub',
description='Run `PROG sub --foo` to start.',
)
prog_extra = self.theme.prog_extra
reset = self.theme.reset
help_text = sub.format_help()
self.assertIn(f'{prog_extra}PROG sub --foo{reset}', help_text)
def test_backtick_markup_special_regex_chars(self):
parser = argparse.ArgumentParser(
prog='PROG',
color=True,
epilog='`grep "foo.*bar" | sort`',
)
prog_extra = self.theme.prog_extra
reset = self.theme.reset
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_argument_help_interpolation_accepts_string_like_proxy(self):
class LazyStr:
def __init__(self, message):
self._message = message
def __str__(self):
return self._message
def __getattr__(self, name):
return getattr(str(self), name)
def __contains__(self, item):
return item in self._message
def __mod__(self, other):
return self._message % other
parser = argparse.ArgumentParser(prog="PROG", color=True)
parser.add_argument(
"--foo",
default="bar",
help=LazyStr("foo (default: %(default)s)"),
)
parser.add_argument(
"--baz",
help=LazyStr("baz plain text"),
)
interp = self.theme.interpolated_value
reset = self.theme.reset
help_text = parser.format_help()
self.assertIn(f"foo (default: {interp}bar{reset})", help_text)
self.assertIn("baz plain text", 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)
parser.add_argument('--hex', type=int, default=255,
help='hex: %(default)x, alt: %(default)#x')
parser.add_argument('--zero', type=int, default=7,
help='zero: %(default)05d')
parser.add_argument('--str', default='test',
help='str: %(default)s')
parser.add_argument('--pct', type=int, default=50,
help='pct: %(default)d%%')
parser.add_argument('--literal', help='literal: 100%%')
parser.add_argument('--prog', help='prog: %(prog)s')
parser.add_argument('--type', type=int, help='type: %(type)s')
parser.add_argument('--choices', choices=['a', 'b'],
help='choices: %(choices)s')
help_text = parser.format_help()
interp = self.theme.interpolated_value
reset = self.theme.reset
self.assertIn(f'hex: {interp}ff{reset}', help_text)
self.assertIn(f'alt: {interp}0xff{reset}', help_text)
self.assertIn(f'zero: {interp}00007{reset}', help_text)
self.assertIn(f'str: {interp}test{reset}', help_text)
self.assertIn(f'pct: {interp}50{reset}%', help_text)
self.assertIn('literal: 100%', help_text)
self.assertIn(f'prog: {interp}PROG{reset}', help_text)
self.assertIn(f'type: {interp}int{reset}', help_text)
self.assertIn(f'choices: {interp}a, b{reset}', help_text)
def test_print_help_uses_target_file_for_color_decision(self):
parser = argparse.ArgumentParser(prog='PROG', color=True)
parser.add_argument('--opt')
output = io.StringIO()
calls = []
def fake_can_colorize(*, file=None):
calls.append(file)
return file is None
with swap_attr(_colorize, 'can_colorize', fake_can_colorize):
parser.print_help(file=output)
self.assertIs(calls[-1], output)
self.assertIn(output, calls)
self.assertNotIn('\x1b[', output.getvalue())
def test_print_usage_uses_target_file_for_color_decision(self):
parser = argparse.ArgumentParser(prog='PROG', color=True)
parser.add_argument('--opt')
output = io.StringIO()
calls = []
def fake_can_colorize(*, file=None):
calls.append(file)
return file is None
with swap_attr(_colorize, 'can_colorize', fake_can_colorize):
parser.print_usage(file=output)
self.assertIs(calls[-1], output)
self.assertIn(output, calls)
self.assertNotIn('\x1b[', output.getvalue())
def test_fake_color_theme_matches_real(self):
from argparse import _colorless_theme
# Check the attributes match those of the 'real' theme
_colorize_nocolor = _colorize.get_theme(force_no_color=True).argparse
for k in _colorize_nocolor:
self.assertEqual(
getattr(_colorless_theme, k), getattr(_colorize_nocolor, k)
)
def test_fake_color_theme_raises(self):
from argparse import _colorless_theme
# Make sure the _colorless_theme doesn't return empty strings
# for magic methods or private attributes
with self.assertRaises(AttributeError):
_colorless_theme.__unknown_dunder__
with self.assertRaises(AttributeError):
_colorless_theme._private_attribute
class TestModule(unittest.TestCase):
def test_deprecated__version__(self):
with self.assertWarnsRegex(
DeprecationWarning,
"'__version__' is deprecated and slated for removal in Python 3.20",
) as cm:
getattr(argparse, "__version__")
self.assertEqual(cm.filename, __file__)
def tearDownModule():
# Remove global references to avoid looking like we have refleaks.
+172 -28
View File
@@ -8,6 +8,7 @@ import datetime
import io
import locale
import os
import platform
import sys
import time
@@ -112,18 +113,25 @@ Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su
default_format = dict(year="year", month="month", encoding="ascii")
result_2004_css = """<style>
:root { color-scheme: light dark; }
table.year { border: solid; }
table.year > tbody > tr > td { border: solid; vertical-align: top; }
</style>"""
result_2004_html = """\
<?xml version="1.0" encoding="{encoding}"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
<link rel="stylesheet" type="text/css" href="calendar.css" />
<meta charset="{encoding}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calendar for 2004</title>
{css_styles}
<link rel="stylesheet" href="calendar.css">
</head>
<body>
<table border="0" cellpadding="0" cellspacing="0" class="{year}">
<tr><th colspan="3" class="{year}">2004</th></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
<table class="{year}">
<tr><th colspan="3" class="{year}">2004</th></tr><tr><td><table class="{month}">
<tr><th colspan="7" class="{month}">January</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="thu">1</td><td class="fri">2</td><td class="sat">3</td><td class="sun">4</td></tr>
@@ -132,7 +140,7 @@ result_2004_html = """\
<tr><td class="mon">19</td><td class="tue">20</td><td class="wed">21</td><td class="thu">22</td><td class="fri">23</td><td class="sat">24</td><td class="sun">25</td></tr>
<tr><td class="mon">26</td><td class="tue">27</td><td class="wed">28</td><td class="thu">29</td><td class="fri">30</td><td class="sat">31</td><td class="noday">&nbsp;</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">February</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="sun">1</td></tr>
@@ -141,7 +149,7 @@ result_2004_html = """\
<tr><td class="mon">16</td><td class="tue">17</td><td class="wed">18</td><td class="thu">19</td><td class="fri">20</td><td class="sat">21</td><td class="sun">22</td></tr>
<tr><td class="mon">23</td><td class="tue">24</td><td class="wed">25</td><td class="thu">26</td><td class="fri">27</td><td class="sat">28</td><td class="sun">29</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">March</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="mon">1</td><td class="tue">2</td><td class="wed">3</td><td class="thu">4</td><td class="fri">5</td><td class="sat">6</td><td class="sun">7</td></tr>
@@ -150,7 +158,7 @@ result_2004_html = """\
<tr><td class="mon">22</td><td class="tue">23</td><td class="wed">24</td><td class="thu">25</td><td class="fri">26</td><td class="sat">27</td><td class="sun">28</td></tr>
<tr><td class="mon">29</td><td class="tue">30</td><td class="wed">31</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td></tr><tr><td><table class="{month}">
<tr><th colspan="7" class="{month}">April</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="thu">1</td><td class="fri">2</td><td class="sat">3</td><td class="sun">4</td></tr>
@@ -159,7 +167,7 @@ result_2004_html = """\
<tr><td class="mon">19</td><td class="tue">20</td><td class="wed">21</td><td class="thu">22</td><td class="fri">23</td><td class="sat">24</td><td class="sun">25</td></tr>
<tr><td class="mon">26</td><td class="tue">27</td><td class="wed">28</td><td class="thu">29</td><td class="fri">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">May</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="sat">1</td><td class="sun">2</td></tr>
@@ -169,7 +177,7 @@ result_2004_html = """\
<tr><td class="mon">24</td><td class="tue">25</td><td class="wed">26</td><td class="thu">27</td><td class="fri">28</td><td class="sat">29</td><td class="sun">30</td></tr>
<tr><td class="mon">31</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">June</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="tue">1</td><td class="wed">2</td><td class="thu">3</td><td class="fri">4</td><td class="sat">5</td><td class="sun">6</td></tr>
@@ -178,7 +186,7 @@ result_2004_html = """\
<tr><td class="mon">21</td><td class="tue">22</td><td class="wed">23</td><td class="thu">24</td><td class="fri">25</td><td class="sat">26</td><td class="sun">27</td></tr>
<tr><td class="mon">28</td><td class="tue">29</td><td class="wed">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td></tr><tr><td><table class="{month}">
<tr><th colspan="7" class="{month}">July</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="thu">1</td><td class="fri">2</td><td class="sat">3</td><td class="sun">4</td></tr>
@@ -187,7 +195,7 @@ result_2004_html = """\
<tr><td class="mon">19</td><td class="tue">20</td><td class="wed">21</td><td class="thu">22</td><td class="fri">23</td><td class="sat">24</td><td class="sun">25</td></tr>
<tr><td class="mon">26</td><td class="tue">27</td><td class="wed">28</td><td class="thu">29</td><td class="fri">30</td><td class="sat">31</td><td class="noday">&nbsp;</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">August</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="sun">1</td></tr>
@@ -197,7 +205,7 @@ result_2004_html = """\
<tr><td class="mon">23</td><td class="tue">24</td><td class="wed">25</td><td class="thu">26</td><td class="fri">27</td><td class="sat">28</td><td class="sun">29</td></tr>
<tr><td class="mon">30</td><td class="tue">31</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">September</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="wed">1</td><td class="thu">2</td><td class="fri">3</td><td class="sat">4</td><td class="sun">5</td></tr>
@@ -206,7 +214,7 @@ result_2004_html = """\
<tr><td class="mon">20</td><td class="tue">21</td><td class="wed">22</td><td class="thu">23</td><td class="fri">24</td><td class="sat">25</td><td class="sun">26</td></tr>
<tr><td class="mon">27</td><td class="tue">28</td><td class="wed">29</td><td class="thu">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td></tr><tr><td><table class="{month}">
<tr><th colspan="7" class="{month}">October</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="fri">1</td><td class="sat">2</td><td class="sun">3</td></tr>
@@ -215,7 +223,7 @@ result_2004_html = """\
<tr><td class="mon">18</td><td class="tue">19</td><td class="wed">20</td><td class="thu">21</td><td class="fri">22</td><td class="sat">23</td><td class="sun">24</td></tr>
<tr><td class="mon">25</td><td class="tue">26</td><td class="wed">27</td><td class="thu">28</td><td class="fri">29</td><td class="sat">30</td><td class="sun">31</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">November</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="mon">1</td><td class="tue">2</td><td class="wed">3</td><td class="thu">4</td><td class="fri">5</td><td class="sat">6</td><td class="sun">7</td></tr>
@@ -224,7 +232,7 @@ result_2004_html = """\
<tr><td class="mon">22</td><td class="tue">23</td><td class="wed">24</td><td class="thu">25</td><td class="fri">26</td><td class="sat">27</td><td class="sun">28</td></tr>
<tr><td class="mon">29</td><td class="tue">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
</td><td><table class="{month}">
<tr><th colspan="7" class="{month}">December</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="wed">1</td><td class="thu">2</td><td class="fri">3</td><td class="sat">4</td><td class="sun">5</td></tr>
@@ -237,6 +245,34 @@ result_2004_html = """\
</html>
"""
result_2009_6_html = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calendar for 2009</title>
<style>
:root { color-scheme: light dark; }
table.year { border: solid; }
table.year > tbody > tr > td { border: solid; vertical-align: top; }
</style>
<link rel="stylesheet" href="calendar.css">
</head>
<body>
<table class="month">
<tr><th colspan="7" class="month">June 2009</th></tr>
<tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
<tr><td class="mon">1</td><td class="tue">2</td><td class="wed">3</td><td class="thu">4</td><td class="fri">5</td><td class="sat">6</td><td class="sun">7</td></tr>
<tr><td class="mon">8</td><td class="tue">9</td><td class="wed">10</td><td class="thu">11</td><td class="fri">12</td><td class="sat">13</td><td class="sun">14</td></tr>
<tr><td class="mon">15</td><td class="tue">16</td><td class="wed">17</td><td class="thu">18</td><td class="fri">19</td><td class="sat">20</td><td class="sun">21</td></tr>
<tr><td class="mon">22</td><td class="tue">23</td><td class="wed">24</td><td class="thu">25</td><td class="fri">26</td><td class="sat">27</td><td class="sun">28</td></tr>
<tr><td class="mon">29</td><td class="tue">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
</table>
</body>
</html>
"""
result_2004_days = [
[[[0, 0, 0, 1, 2, 3, 4],
[5, 6, 7, 8, 9, 10, 11],
@@ -384,10 +420,12 @@ class OutputTestCase(unittest.TestCase):
cal = calendar.HTMLCalendar()
format_ = default_format.copy()
format_["encoding"] = req or 'utf-8'
format_with_css = {**format_, "css_styles": result_2004_css}
formatted_html = result_2004_html.format(**format_with_css)
output = cal.formatyearpage(2004, encoding=req)
self.assertEqual(
output,
result_2004_html.format(**format_).encode(res)
formatted_html.encode(res)
)
def test_output(self):
@@ -417,7 +455,7 @@ class OutputTestCase(unittest.TestCase):
self.check_htmlcalendar_encoding('utf-8', 'utf-8')
def test_output_htmlcalendar_encoding_default(self):
self.check_htmlcalendar_encoding(None, sys.getdefaultencoding())
self.check_htmlcalendar_encoding(None, 'utf-8')
def test_yeardatescalendar(self):
def shrink(cal):
@@ -501,6 +539,19 @@ class OutputTestCase(unittest.TestCase):
calendar.format(["1", "2", "3"], colwidth=3, spacing=1)
self.assertEqual(out.getvalue().strip(), "1 2 3")
def test_format_html_year_with_month(self):
self.assertEqual(
calendar.HTMLCalendar().formatmonthpage(2009, 6).decode("ascii"),
result_2009_6_html
)
def test_formatmonthpage_no_width(self):
# gh-140212: formatmonthpage must not accept a 'width' argument.
cal = calendar.HTMLCalendar()
self.assertIn(b'class="month">June 2009', cal.formatmonthpage(2009, 6))
self.assertRaises(TypeError, cal.formatmonthpage, 2009, 6, width=3)
class CalendarTestCase(unittest.TestCase):
def test_deprecation_warning(self):
@@ -556,7 +607,8 @@ class CalendarTestCase(unittest.TestCase):
self.assertEqual(value[::-1], list(reversed(value)))
def test_months(self):
for attr in "month_name", "month_abbr":
for attr in ("month_name", "month_abbr", "standalone_month_name",
"standalone_month_abbr"):
value = getattr(calendar, attr)
self.assertEqual(len(value), 13)
self.assertEqual(len(value[:]), 13)
@@ -566,6 +618,38 @@ class CalendarTestCase(unittest.TestCase):
# verify it "acts like a sequence" in two forms of iteration
self.assertEqual(value[::-1], list(reversed(value)))
@support.run_with_locale('LC_ALL', 'pl_PL')
@unittest.skipUnless(sys.platform == 'darwin' or platform.libc_ver()[0] == 'glibc',
"Guaranteed to work with glibc and macOS")
def test_standalone_month_name_and_abbr_pl_locale(self):
expected_standalone_month_names = [
"", "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec",
"lipiec", "sierpień", "wrzesień", "październik", "listopad",
"grudzień"
]
expected_standalone_month_abbr = [
"", "sty", "lut", "mar", "kwi", "maj", "cze",
"lip", "sie", "wrz", "paź", "lis", "gru"
]
self.assertEqual(
list(calendar.standalone_month_name),
expected_standalone_month_names
)
self.assertEqual(
list(calendar.standalone_month_abbr),
expected_standalone_month_abbr
)
def test_standalone_month_name_and_abbr_C_locale(self):
# Ensure that the standalone month names and abbreviations are
# equal to the regular month names and abbreviations for
# the "C" locale.
with calendar.different_locale("C"):
self.assertListEqual(list(calendar.month_name),
list(calendar.standalone_month_name))
self.assertListEqual(list(calendar.month_abbr),
list(calendar.standalone_month_abbr))
def test_locale_text_calendar(self):
try:
cal = calendar.LocaleTextCalendar(locale='')
@@ -672,6 +756,57 @@ class CalendarTestCase(unittest.TestCase):
except locale.Error:
raise unittest.SkipTest('cannot set the en_US locale')
# These locales have weekday names all shorter than English's longest
# 'Wednesday'. They should not be abbreviated unnecessarily
@support.run_with_locales("LC_ALL",
'Chinese', 'zh_CN.UTF-8',
'French', 'fr_FR.UTF-8',
'Norwegian', 'nb_NO.UTF-8',
'Malay', 'ms_MY.UTF8'
)
def test_locale_calendar_short_weekday_names(self):
names = (datetime.date(2001, 1, i+1).strftime('%A') for i in range(7))
max_length = max(map(len, names))
if max_length >= 9:
self.skipTest('weekday names are too long')
def get_weekday_names(width):
return calendar.TextCalendar().formatweekheader(width).split()
# Weekday names should not be abbreviated if the width is sufficient
self.assertEqual(
get_weekday_names(max_length),
get_weekday_names(max_length + 10)
)
# Any width shorter than necessary should produce abbreviations
self.assertNotEqual(
get_weekday_names(max_length),
get_weekday_names(max_length - 1)
)
# These locales have a weekday name longer than 'Wednesday'
# They should be properly abbreviated rather than truncated
@support.run_with_locales("LC_ALL",
'Portuguese', 'pt_PT.UTF-8',
'German', 'de_DE.UTF-8',
'Russian', 'ru_RU.UTF-8',
)
def test_locale_calendar_long_weekday_names(self):
names = (datetime.date(2001, 1, i+1).strftime('%A') for i in range(7))
max_length = max(map(len, names))
abbrev_names = (datetime.date(2001, 1, i+1).strftime('%a') for i in range(7))
abbrev_max_length = max(map(len, abbrev_names))
if max_length <= 9:
self.skipTest('weekday names are too short')
if abbrev_max_length >= 9:
self.skipTest('abbreviated weekday names are too long')
def get_weekday_names(width):
return calendar.TextCalendar().formatweekheader(width).split()
self.assertEqual(get_weekday_names(abbrev_max_length), get_weekday_names(max_length-1))
def test_locale_calendar_formatmonthname(self):
try:
# formatmonthname uses the same month names regardless of the width argument.
@@ -944,6 +1079,7 @@ class LeapdaysTestCase(unittest.TestCase):
def conv(s):
return s.replace('\n', os.linesep).encode()
@support.force_not_colorized_test_class
class CommandLineTestCase(unittest.TestCase):
def setUp(self):
self.runners = [self.run_cli_ok, self.run_cmd_ok]
@@ -997,7 +1133,6 @@ class CommandLineTestCase(unittest.TestCase):
self.assertCLIFails(*args)
self.assertCmdFails(*args)
@support.force_not_colorized
def test_help(self):
stdout = self.run_cmd_ok('-h')
self.assertIn(b'usage:', stdout)
@@ -1018,7 +1153,6 @@ class CommandLineTestCase(unittest.TestCase):
self.assertFailure('2004', '1', 'spam')
self.assertFailure('2004', '1', '1')
self.assertFailure('2004', '1', '1', 'spam')
self.assertFailure('-t', 'html', '2004', '1')
def test_output_current_year(self):
for run in self.runners:
@@ -1108,7 +1242,7 @@ class CommandLineTestCase(unittest.TestCase):
output = run('--type', 'text', '2004')
self.assertEqual(output, conv(result_2004_text))
output = run('--type', 'html', '2004')
self.assertStartsWith(output, b'<?xml ')
self.assertStartsWith(output, b'<!DOCTYPE html>')
self.assertIn(b'<title>Calendar for 2004</title>', output)
def test_html_output_current_year(self):
@@ -1121,15 +1255,25 @@ class CommandLineTestCase(unittest.TestCase):
def test_html_output_year_encoding(self):
for run in self.runners:
output = run('-t', 'html', '--encoding', 'ascii', '2004')
self.assertEqual(output, result_2004_html.format(**default_format).encode('ascii'))
format_with_css = default_format.copy()
format_with_css["css_styles"] = result_2004_css
self.assertEqual(output, result_2004_html.format(**format_with_css).encode('ascii'))
def test_html_output_year_css(self):
self.assertFailure('-t', 'html', '-c')
self.assertFailure('-t', 'html', '--css')
for run in self.runners:
output = run('-t', 'html', '--css', 'custom.css', '2004')
self.assertIn(b'<link rel="stylesheet" type="text/css" '
b'href="custom.css" />', output)
self.assertIn(b'<link rel="stylesheet" href="custom.css">', output)
@support.force_colorized_test_class
class ColorTestCase(unittest.TestCase):
def test_formatmonth_color(self):
today = datetime.date(2026, 5, 4)
cal = calendar._CLIDemoCalendar(highlight_day=today)
output = cal.formatmonth(2026, 5)
self.assertIn("\x1b[30m\x1b[43mMay 2026\x1b[0m\n\x1b[36m", output)
class MiscTestCase(unittest.TestCase):
@@ -1183,7 +1327,7 @@ class TestSubClassingCase(unittest.TestCase):
def test_format_year(self):
self.assertIn(
('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
('<table class="%s">' %
self.cal.cssclass_year), self.cal.formatyear(2017))
def test_format_year_head(self):
+143 -25
View File
@@ -88,6 +88,147 @@ class GetpassRawinputTest(unittest.TestCase):
input = StringIO('test\n')
self.assertEqual('test', getpass._raw_input(input=input))
def check_raw_input(self, inputs, expect_result, prompt='Password: '):
mock_input = StringIO(inputs)
mock_output = StringIO()
result = getpass._raw_input(prompt, mock_output, mock_input, '*')
self.assertEqual(result, expect_result)
return mock_output.getvalue()
def test_null_char(self):
self.check_raw_input('pass\x00word\n', 'password')
def test_raw_input_with_echo_char(self):
output = self.check_raw_input('my1pa$$word!\n', 'my1pa$$word!')
self.assertEqual('Password: ************', output)
def test_control_chars_with_echo_char(self):
output = self.check_raw_input('pass\twd\b\n', 'pass\tw')
# After backspace: refresh rewrites prompt + 6 echo chars
self.assertEqual(
'Password: *******' # initial prompt + 7 echo chars
'\r' + ' ' * 17 + '\r' # clear line (10 prompt + 7 prev)
'Password: ******', # rewrite prompt + 6 echo chars
output
)
def test_kill_ctrl_u_with_echo_char(self):
# Ctrl+U (KILL) should clear the entire line
output = self.check_raw_input('foo\x15bar\n', 'bar')
# Should show "***" then refresh to clear, then show "***" for "bar"
self.assertIn('***', output)
# Display refresh uses \r to rewrite the line including prompt
self.assertIn('\r', output)
def test_werase_ctrl_w_with_echo_char(self):
# Ctrl+W (WERASE) should delete the previous word
self.check_raw_input('hello world\x17end\n', 'hello end')
def test_ctrl_w_display_preserves_prompt(self):
# Reproducer from gh-138577: type "hello world", Ctrl+W
# Display must show "Password: ******" not "******rd: ***********"
output = self.check_raw_input('hello world\x17\n', 'hello ')
# The final visible state should be "Password: ******"
# Verify prompt is rewritten during refresh, not overwritten by stars
self.assertEndsWith(output, 'Password: ******')
def test_ctrl_a_insert_display_preserves_prompt(self):
# Reproducer from gh-138577: type "abc", Ctrl+A, type "x"
# Display must show "Password: ****" not "****word: ***"
output = self.check_raw_input('abc\x01x\n', 'xabc')
# The final visible state should be "Password: ****"
self.assertEndsWith(output, 'Password: ****\x08\x08\x08')
def test_lnext_ctrl_v_with_echo_char(self):
# Ctrl+V (LNEXT) should insert the next character literally
self.check_raw_input('test\x16\x15more\n', 'test\x15more')
def test_ctrl_a_move_to_start_with_echo_char(self):
# Ctrl+A should move cursor to start
self.check_raw_input('end\x01start\n', 'startend')
def test_ctrl_a_cursor_position(self):
# After Ctrl+A, cursor is at position 0.
# Refresh writes backspaces to move cursor from end to start.
output = self.check_raw_input('abc\x01\n', 'abc')
self.assertEndsWith(output, 'Password: ***\x08\x08\x08')
def test_ctrl_a_on_empty(self):
# Ctrl+A on empty line should be a no-op
self.check_raw_input('\x01hello\n', 'hello')
def test_ctrl_a_already_at_start(self):
# Double Ctrl+A should be same as single Ctrl+A
self.check_raw_input('abc\x01\x01start\n', 'startabc')
def test_ctrl_a_then_backspace(self):
# Backspace after Ctrl+A should do nothing (cursor at 0)
self.check_raw_input('abc\x01\x7f\n', 'abc')
def test_ctrl_e_move_to_end_with_echo_char(self):
# Ctrl+E should move cursor to end
self.check_raw_input('start\x01X\x05end\n', 'Xstartend')
def test_ctrl_e_cursor_position(self):
# After Ctrl+A then Ctrl+E, cursor is back at end.
# Refresh has no backspaces since cursor is at end.
output = self.check_raw_input('abc\x01\x05\n', 'abc')
self.assertEndsWith(output, 'Password: ***')
def test_ctrl_e_on_empty(self):
# Ctrl+E on empty line should be a no-op
self.check_raw_input('\x05hello\n', 'hello')
def test_ctrl_e_already_at_end(self):
# Ctrl+E when already at end should be a no-op
self.check_raw_input('abc\x05more\n', 'abcmore')
def test_ctrl_a_then_ctrl_e(self):
# Ctrl+A then Ctrl+E should return cursor to end, typing appends
self.check_raw_input('abc\x01\x05def\n', 'abcdef')
def test_ctrl_k_kill_forward_with_echo_char(self):
# Ctrl+K should kill from cursor to end
self.check_raw_input('delete\x01\x0bkeep\n', 'keep')
def test_ctrl_c_interrupt_with_echo_char(self):
# Ctrl+C should raise KeyboardInterrupt
with self.assertRaises(KeyboardInterrupt):
self.check_raw_input('test\x03more', '')
def test_ctrl_d_eof_with_echo_char(self):
# Ctrl+D twice should cause EOF
self.check_raw_input('test\x04\x04', 'test')
def test_backspace_at_start_with_echo_char(self):
# Backspace at start should do nothing
self.check_raw_input('\x7fhello\n', 'hello')
def test_ctrl_k_at_end_with_echo_char(self):
# Ctrl+K at end should do nothing
self.check_raw_input('hello\x0b\n', 'hello')
def test_ctrl_w_on_empty_with_echo_char(self):
# Ctrl+W on empty line should do nothing
self.check_raw_input('\x17hello\n', 'hello')
def test_ctrl_u_on_empty_with_echo_char(self):
# Ctrl+U on empty line should do nothing
self.check_raw_input('\x15hello\n', 'hello')
def test_multiple_ctrl_operations_with_echo_char(self):
# Test combination: type, move, insert, delete
# "world", Ctrl+A, "hello ", Ctrl+E, "!", Ctrl+A, Ctrl+K, "start"
self.check_raw_input('world\x01hello \x05!\x01\x0bstart\n', 'start')
def test_ctrl_w_multiple_words_with_echo_char(self):
# Ctrl+W should delete only the last word
self.check_raw_input('one two three\x17\n', 'one two ')
def test_ctrl_v_then_ctrl_c_with_echo_char(self):
# Ctrl+V should make Ctrl+C literal (not interrupt)
self.check_raw_input('test\x16\x03end\n', 'test\x03end')
# Some of these tests are a bit white-box. The functional requirement is that
# the password input be taken directly from the tty, and that it not be echoed
@@ -174,33 +315,10 @@ class UnixGetpassTest(unittest.TestCase):
result = getpass.unix_getpass(echo_char='*')
mock_input.assert_called_once_with('Password: ', textio(),
input=textio(), echo_char='*')
input=textio(), echo_char='*',
term_ctrl_chars=mock.ANY)
self.assertEqual(result, mock_result)
def test_raw_input_with_echo_char(self):
passwd = 'my1pa$$word!'
mock_input = StringIO(f'{passwd}\n')
mock_output = StringIO()
with mock.patch('sys.stdin', mock_input), \
mock.patch('sys.stdout', mock_output):
result = getpass._raw_input('Password: ', mock_output, mock_input,
'*')
self.assertEqual(result, passwd)
self.assertEqual('Password: ************', mock_output.getvalue())
def test_control_chars_with_echo_char(self):
passwd = 'pass\twd\b'
expect_result = 'pass\tw'
mock_input = StringIO(f'{passwd}\n')
mock_output = StringIO()
with mock.patch('sys.stdin', mock_input), \
mock.patch('sys.stdout', mock_output):
result = getpass._raw_input('Password: ', mock_output, mock_input,
'*')
self.assertEqual(result, expect_result)
self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue())
class GetpassEchoCharTest(unittest.TestCase):
def test_accept_none(self):
+39 -13
View File
@@ -13,8 +13,7 @@ import select
import socket
from test.support import verbose, run_with_tz, run_with_locale, cpython_only
from test.support import hashlib_helper
from test.support import threading_helper
from test.support import hashlib_helper, threading_helper
import unittest
from unittest import mock
from datetime import datetime, timezone, timedelta
@@ -684,17 +683,12 @@ class NewIMAPTestsMixin:
ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf")
self.assertEqual(ret, "OK")
@hashlib_helper.block_algorithm("md5")
def test_login_cram_md5_blocked(self):
def side_effect(*a, **kw):
raise ValueError
client, _ = self._setup(AuthHandler_CRAM_MD5)
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
msg = re.escape("CRAM-MD5 authentication is not supported")
with (
mock.patch("hmac.HMAC", side_effect=side_effect),
self.assertRaisesRegex(imaplib.IMAP4.error, msg)
):
with self.assertRaisesRegex(imaplib.IMAP4.error, msg):
client.login_cram_md5("tim", b"tanstaaftanstaaf")
def test_aborted_authentication(self):
@@ -1831,11 +1825,33 @@ class NewIMAPTestsMixin:
# property tests
def test_file_property_should_not_be_accessed(self):
def test_file_property_getter(self):
client, _ = self._setup(SimpleIMAPHandler)
# the 'file' property replaced a private attribute that is now unsafe
with self.assertWarns(RuntimeWarning):
client.file
with self.assertWarns(DeprecationWarning):
self.assertIsInstance(client.file.raw, socket.SocketIO)
def test_file_property_setter(self):
client, _ = self._setup(SimpleIMAPHandler)
with self.assertWarns(DeprecationWarning):
# ensure that the caller closes the existing file
client.file.close()
for new_file in [mock.Mock(), None]:
with self.assertWarns(DeprecationWarning):
client.file = new_file
with self.assertWarns(DeprecationWarning):
self.assertIs(client.file, new_file)
def test_file_property_setter_should_not_close_previous_file(self):
client, _ = self._setup(SimpleIMAPHandler)
with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f:
f.close.assert_not_called()
with self.assertWarns(DeprecationWarning):
self.assertIs(client.file, f)
with self.assertWarns(DeprecationWarning):
client.file = None
with self.assertWarns(DeprecationWarning):
self.assertIsNone(client.file)
f.close.assert_not_called()
class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
@@ -2288,5 +2304,15 @@ class ThreadedNetworkedTestsSSL(ThreadedNetworkedTests):
client.shutdown()
class TestModule(unittest.TestCase):
def test_deprecated__version__(self):
with self.assertWarnsRegex(
DeprecationWarning,
"'__version__' is deprecated and slated for removal in Python 3.20",
) as cm:
getattr(imaplib, "__version__")
self.assertEqual(cm.filename, __file__)
if __name__ == "__main__":
unittest.main()
+20 -5
View File
@@ -8,9 +8,10 @@ import re
import io
import tempfile
from test import support
from test.support import import_helper
from test.support import import_helper, warnings_helper
from test.support import os_helper
from test.support import refleak_helper
from test.support import requires_root_user
from test.support import socket_helper
import unittest
import textwrap
@@ -542,6 +543,11 @@ class TestMailbox(TestBase):
self.assertIn(self._box.get_string(key), contents)
oldbox.close()
def test_use_context_manager(self):
# Mailboxes are usable as a context manager
with self._box as box:
self.assertIs(self._box, box)
def test_dump_message(self):
# Write message representations to disk
for input in (email.message_from_string(_sample_message),
@@ -1081,6 +1087,7 @@ class _TestSingleFile(TestMailbox):
self.assertEqual(os.stat(self._path).st_mode, mode)
@requires_root_user
@unittest.skipUnless(hasattr(os, 'chown'), 'requires os.chown')
def test_ownership_after_flush(self):
# See issue gh-117467
@@ -1103,10 +1110,7 @@ class _TestSingleFile(TestMailbox):
else:
self.skipTest("test needs more than one group")
try:
os.chown(self._path, other_uid, other_gid)
except OSError:
self.skipTest('test needs root privilege')
os.chown(self._path, other_uid, other_gid)
# Change permissions as in test_permissions_after_flush.
mode = st.st_mode | 0o666
os.chmod(self._path, mode)
@@ -1122,6 +1126,16 @@ class _TestSingleFile(TestMailbox):
self.assertEqual(st.st_gid, other_gid)
self.assertEqual(st.st_mode, mode)
def test_context_manager_locks_and_closes(self):
# Context manager locks/unlocks and closes.
# (This test uses an implementation detail to get the state.)
self.assertFalse(self._box._locked)
with self._box as context_object:
self.assertIs(self._box, context_object)
self.assertTrue(self._box._locked)
self.assertFalse(self._box._file.closed)
self.assertFalse(self._box._locked)
self.assertTrue(self._box._file.closed)
class _TestMboxMMDF(_TestSingleFile):
@@ -1212,6 +1226,7 @@ class _TestMboxMMDF(_TestSingleFile):
self.assertEqual(contents, f.read())
self._box = self._factory(self._path)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@support.requires_fork()
@unittest.skipUnless(hasattr(socket, 'socketpair'), "Test needs socketpair().")
def test_lock_conflict(self):
+66 -16
View File
@@ -4,8 +4,14 @@ import sys
import io
from textwrap import dedent
from test.support import captured_stdout
from test.support import captured_stderr
from test.support import (
captured_stderr,
captured_stdout,
force_colorized,
force_not_colorized_test_class,
)
from _colorize import get_theme
# timeit's default number of iterations.
DEFAULT_NUMBER = 1000000
@@ -41,6 +47,7 @@ class FakeTimer:
self.saved_timer = timer
return self
@force_not_colorized_test_class
class TestTimeit(unittest.TestCase):
def tearDown(self):
@@ -252,11 +259,12 @@ class TestTimeit(unittest.TestCase):
return s.getvalue()
def test_main_bad_switch(self):
s = self.run_main(switches=['--bad-switch'])
self.assertEqual(s, dedent("""\
option --bad-switch not recognized
use -h/--help for command line help
"""))
with captured_stderr() as error_stringio:
s = self.run_main(switches=["--bad-switch"])
self.assertEqual(s, "")
self.assertIn(
"unrecognized arguments: --bad-switch", error_stringio.getvalue()
)
def test_main_seconds(self):
s = self.run_main(seconds_per_increment=5.5)
@@ -294,10 +302,11 @@ class TestTimeit(unittest.TestCase):
s = self.run_main(seconds_per_increment=60.0, switches=['-r-5'])
self.assertEqual(s, "1 loop, best of 1: 60 sec per loop\n")
@unittest.skipIf(sys.flags.optimize >= 2, "need __doc__")
def test_main_help(self):
s = self.run_main(switches=['-h'])
self.assertEqual(s, timeit.__doc__)
self.assertIn("Tool for measuring execution time", s)
self.assertIn("-n", s)
self.assertIn("--number", s)
def test_main_verbose(self):
s = self.run_main(switches=['-v'])
@@ -346,10 +355,12 @@ class TestTimeit(unittest.TestCase):
"100 loops, best of 5: 3e+03 usec per loop\n")
# Test invalid unit input
with captured_stderr() as error_stringio:
invalid = self.run_main(seconds_per_increment=0.003,
switches=['-u', 'parsec'])
self.assertEqual(error_stringio.getvalue(),
"Unrecognized unit. Please select nsec, usec, msec, or sec.\n")
invalid = self.run_main(
seconds_per_increment=0.003, switches=["-u", "parsec"]
)
self.assertIn(
"choose from 'nsec', 'usec', 'msec', 'sec'", error_stringio.getvalue()
)
def test_main_exception(self):
with captured_stderr() as error_stringio:
@@ -361,10 +372,10 @@ class TestTimeit(unittest.TestCase):
s = self.run_main(switches=['-n1', '1/0'])
self.assert_exc_string(error_stringio.getvalue(), 'ZeroDivisionError')
def autorange(self, seconds_per_increment=1/1024, callback=None):
def autorange(self, seconds_per_increment=1/1024, callback=None, target_time=0.2):
timer = FakeTimer(seconds_per_increment=seconds_per_increment)
t = timeit.Timer(stmt=self.fake_stmt, setup=self.fake_setup, timer=timer)
return t.autorange(callback)
return t.autorange(callback, target_time=target_time)
def test_autorange(self):
num_loops, time_taken = self.autorange()
@@ -376,6 +387,11 @@ class TestTimeit(unittest.TestCase):
self.assertEqual(num_loops, 1)
self.assertEqual(time_taken, 1.0)
def test_autorange_with_target_time(self):
num_loops, time_taken = self.autorange(target_time=1.0)
self.assertEqual(num_loops, 2000)
self.assertEqual(time_taken, 2000/1024)
def test_autorange_with_callback(self):
def callback(a, b):
print("{} {:.3f}".format(a, b))
@@ -395,5 +411,39 @@ class TestTimeit(unittest.TestCase):
self.assertEqual(s.getvalue(), expected)
if __name__ == '__main__':
class TestTimeitColor(unittest.TestCase):
fake_stmt = TestTimeit.fake_stmt
run_main = TestTimeit.run_main
@force_colorized
def test_main_colorized(self):
t = get_theme(force_color=True).timeit
s = self.run_main(seconds_per_increment=5.5)
self.assertEqual(
s,
"1 loop, best of 5: "
f"{t.best}5.5 sec{t.reset} "
f"{t.per_loop}per loop{t.reset}\n",
)
@force_colorized
def test_main_verbose_colorized(self):
t = get_theme(force_color=True).timeit
s = self.run_main(switches=["-v"])
self.assertEqual(
s,
f"1 loop {t.punctuation}-> {t.timing}1 sec{t.reset}\n\n"
"raw times: "
f"{t.timing}1 sec{t.punctuation}, "
f"{t.timing}1 sec{t.punctuation}, "
f"{t.timing}1 sec{t.punctuation}, "
f"{t.timing}1 sec{t.punctuation}, "
f"{t.timing}1 sec{t.reset}\n\n"
f"1 loop, best of 5: {t.best}1 sec{t.reset} "
f"{t.per_loop}per loop{t.reset}\n",
)
if __name__ == "__main__":
unittest.main()
+7 -24
View File
@@ -7,19 +7,8 @@
import datetime
from typing import Any
# Aliases for converting TOML compliance format [1] to BurntSushi format [2]
# [1] https://github.com/toml-lang/compliance/blob/db7c3211fda30ff9ddb10292f4aeda7e2e10abc4/docs/json-encoding.md # noqa: E501
# [2] https://github.com/BurntSushi/toml-test/blob/4634fdf3a6ecd6aaea5f4cdcd98b2733c2694993/README.md # noqa: E501
_aliases = {
"boolean": "bool",
"offset datetime": "datetime",
"local datetime": "datetime-local",
"local date": "date-local",
"local time": "time-local",
}
def convert(obj): # noqa: C901
def convert(obj):
if isinstance(obj, str):
return {"type": "string", "value": obj}
elif isinstance(obj, bool):
@@ -53,31 +42,25 @@ def convert(obj): # noqa: C901
def normalize(obj: Any) -> Any:
"""Normalize test objects.
This normalizes primitive values (e.g. floats), and also converts from
TOML compliance format [1] to BurntSushi format [2].
[1] https://github.com/toml-lang/compliance/blob/db7c3211fda30ff9ddb10292f4aeda7e2e10abc4/docs/json-encoding.md # noqa: E501
[2] https://github.com/BurntSushi/toml-test/blob/4634fdf3a6ecd6aaea5f4cdcd98b2733c2694993/README.md # noqa: E501
"""
This normalizes primitive values (e.g. floats)."""
if isinstance(obj, list):
return [normalize(item) for item in obj]
if isinstance(obj, dict):
if "type" in obj and "value" in obj:
type_ = obj["type"]
norm_type = _aliases.get(type_, type_)
value = obj["value"]
if norm_type == "float":
if type_ == "float":
norm_value = _normalize_float_str(value)
elif norm_type in {"datetime", "datetime-local"}:
elif type_ in {"datetime", "datetime-local"}:
norm_value = _normalize_datetime_str(value)
elif norm_type == "time-local":
elif type_ == "time-local":
norm_value = _normalize_localtime_str(value)
else:
norm_value = value
if norm_type == "array":
if type_ == "array":
return [normalize(item) for item in value]
return {"type": norm_type, "value": norm_value}
return {"type": type_, "value": norm_value}
return {k: normalize(v) for k, v in obj.items()}
raise AssertionError("Burntsushi fixtures should be dicts/lists only")
@@ -1,4 +1,5 @@
{
"local-dt": {"type":"datetime-local","value":"1988-10-27t01:01:01"},
"local-dt-no-seconds": {"type":"datetime-local","value":"2025-04-18t20:05:00"},
"zulu-dt": {"type":"datetime","value":"1988-10-27t01:01:01z"}
}
@@ -1,2 +1,3 @@
local-dt=1988-10-27t01:01:01
local-dt-no-seconds=2025-04-18T20:05
zulu-dt=1988-10-27t01:01:01z
@@ -1,2 +1,4 @@
{"t":
{"type":"time-local","value":"00:00:00.999999"}}
{"type":"time-local","value":"00:00:00.999999"},
"t2":
{"type":"time-local","value":"00:00:00"}}
@@ -1 +1,2 @@
t=00:00:00.99999999999999
t=00:00:00.99999999999999
t2=00:00
@@ -0,0 +1,35 @@
{
"multiline": {
"a": {
"type": "integer",
"value": "1"
},
"b": {
"type": "integer",
"value": "2"
},
"c": [
{
"type": "integer",
"value": "1"
},
{
"type": "integer",
"value": "2"
},
{
"type": "integer",
"value": "3"
}
],
"d": {
"type": "integer",
"value": "3"
},
"e": {
"type": "integer",
"value": "4"
},
"f": {}
}
}
@@ -0,0 +1,12 @@
multiline = {
"a" = 1, "b" = 2,
c = [
1,
2,
3,
],# comment
d = 3,
e = 4, f = {
# comment
},
}
@@ -0,0 +1,6 @@
{
"escape": {"type":"string","value":"\u001B"},
"tab": {"type":"string","value":"\t"},
"upper-j": {"type":"string","value":"J"},
"upper-j-2": {"type":"string","value":"J"}
}
@@ -0,0 +1,4 @@
escape = "\e"
tab = "\x09"
upper-j = "\x4a"
upper-j-2 = "\x4A"
+1 -18
View File
@@ -8,12 +8,6 @@ import unittest
from . import burntsushi, tomllib
class MissingFile:
def __init__(self, path: Path):
self.path = path
DATA_DIR = Path(__file__).parent / "data"
VALID_FILES = tuple((DATA_DIR / "valid").glob("**/*.toml"))
@@ -22,10 +16,7 @@ assert VALID_FILES, "Valid TOML test files not found"
_expected_files = []
for p in VALID_FILES:
json_path = p.with_suffix(".json")
try:
text = json.loads(json_path.read_bytes().decode())
except FileNotFoundError:
text = MissingFile(json_path)
text = json.loads(json_path.read_bytes().decode())
_expected_files.append(text)
VALID_FILES_EXPECTED = tuple(_expected_files)
@@ -49,14 +40,6 @@ class TestData(unittest.TestCase):
def test_valid(self):
for valid, expected in zip(VALID_FILES, VALID_FILES_EXPECTED):
with self.subTest(msg=valid.stem):
if isinstance(expected, MissingFile):
# For a poor man's xfail, assert that this is one of the
# test cases where expected data is known to be missing.
assert valid.stem in {
"qa-array-inline-nested-1000",
"qa-table-inline-nested-1000",
}
continue
toml_str = valid.read_bytes().decode()
actual = tomllib.loads(toml_str)
actual = burntsushi.convert(actual)
+19
View File
@@ -9,8 +9,10 @@ import importlib
from pathlib import Path
import sys
import tempfile
import textwrap
import unittest
from test import support
from test.support.script_helper import assert_python_ok
from . import tomllib
@@ -137,3 +139,20 @@ class TestMiscellaneous(unittest.TestCase):
never imported by tests.
"""
importlib.import_module(f"{tomllib.__name__}._types")
def test_lazy_import(self):
# Test the TOML file can be parsed without importing regular
# expressions (tomllib._re)
code = textwrap.dedent("""
import sys, tomllib, textwrap
document = textwrap.dedent('''
[metadata]
key = "text"
array = ["array", "of", "text"]
booleans = [true, false]
''')
tomllib.loads(document)
print("lazy import?", 'tomllib._re' not in sys.modules)
""")
proc = assert_python_ok("-c", code)
self.assertIn(b"lazy import? True", proc.out)
+3
View File
@@ -120,3 +120,6 @@ def update_test_data(fname: str = "zoneinfo_data.json") -> None:
if __name__ == "__main__":
update_test_data()
print("Remember to update the HAS_TZDATA_PKG version requirement in "
"test_zoneinfo.py!")
+188 -160
View File
@@ -1,190 +1,218 @@
{
"data": {
"Africa/Abidjan": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~f{VGF<>F7KxBg5R*{Ksocg8-YYVul=v7vZzaHN",
"uC=da5UI2rH18c!OnjV{y4u(+A!!VBKmY&$ORw>7UO^(500B;v0RR91bXh%WvBYQl0ssI2",
"00dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~f{VGF<>F7KxBg5R*{Ksocg8-",
"YYVul=v7vZzaHNuC=da5UI2rH18c!OnjV{y4u(+A!!VBKmY&$ORw>7UO^(500B;v0RR91b",
"Xh%WvBYQl0ssI200dcD"
],
"Africa/Casablanca": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0b&Kz+C_;7KxBg5R*{N&yjMUR~;C-fDaSOU;q-~",
"FqW+4{YBjbcw}`a!dW>b)R2-0a+uwf`P3{_Y@HuCz}S$J$ZJ>R_V<~|Fk>sgX4=%0vUrh-",
"lt@YP^Wrus;j?`Th#xRPzf<<~Hp4DH^gZX>d{+WOp~HNu8!{uWu}&XphAd{j1;rB4|9?R!",
"pqruAFUMt8#*WcrVS{;kLlY(cJRV$w?d2car%R<ALOSO?^`4;ZZtI)%f^^G^>s>q9BgTU4",
"Ht-tQKZ7Z`9QqOb?R#b%z?rk>!CkH7jy3wja4NG2q)H}fNRKg8v{);Em;K3Cncf4C6&Oaj",
"V+DbX%o4+)CV3+e!Lm6dutu(0BQpH1T?W(~cQtKV*^_Pdx!LirjpTs?Bmt@vktjLq4;)O!",
"rrly=c*rwTwMJFd0I57`hgkc?=nyI4RZf9W$6DCWugmf&)wk^tWH17owj=#PGH7Xv-?9$j",
"njwDlkOE+BFNR9YXEmBpO;rqEw=e2IR-8^(W;8ma?M3JVd($2T>IW+0tk|Gm8>ftukRQ9J",
"8k3brzqMnVyjsLI-CKneFa)Lxvp_a<CkQEd#(pMA^rr}rBNElGA=*!M)puBdoErR9{kWL@",
"w=svMc6eZ^-(vQZrV<u^PY#nOIUDJ8%A&;BUVlY9=;@i2j2J1_`P>q40f}0J3VVoWL5rox",
"`Kptivcp}o5xA^@>qNI%?zo=Yj4AMV?kbAA)j(1%)+Pp)bSn+7Yk`M{oE}L-Z!G6<Dgq&*",
"(C-mFJfbEGDH5M^vBr65rcnsx*~|Em_GeU#B)(+T!|MG-nxj0@IPbp-nHejH3~>OMr5G+h",
"p)$3Lg{ono{4cN>Vr&>L4kXH;_VnBL5U!LgzqE%P7QQ*<E!guRW2SE@ayq@)G2nXqA2tGo",
"QIgc6>tue}O`3(TZ0`aKn&~8trOQ-rBXCp)f@P6RMO4l0+;b|5-pk9_ryNh}Zc*v%mvz_#",
"yd<xXt%~gT90dn4e{Ac<baL-)Y{L7&5G($I$>6fjB0g9{MmMnu8bG%#C~ugXK^S^k@?ab#",
"O|aE>dDTt4s4n69(~@t~!wniV%g<uWQat_i6>7khFx~I*4>Y|V$4j5%KPF*-FyKIi@!Ho&",
"x8QQsksYt8)D+W)Ni!=G`ogSu^vLL-l#7A7=iIAKL2SuZk9F}NfNk86VI)9WZE?%2wC-ya",
"F~z#Qsq)LH0|_D8^5fU8X%GeQ4TB>R-dlziA&tZe&1ada208!$nk`7bOFO2S00G<w{Sp8G",
"{cR_IvBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0b&Kz+C_;7KxBg5R*{N&yjMUR~;C-fDaSOU;q-",
"~FqW+4{YBjbcw}`a!dW>b)R2-",
"0a+uwf`P3{_Y@HuCz}S$J$ZJ>R_V<~|Fk>sgX4=%0vUrh-lt@YP^Wrus;j?`Th#xRPzf<<",
"~Hp4DH^gZX>d{+WOp~HNu8!{uWu}&XphAd{j1;rB4|9?R!pqruAFUMt8#*WcrVS{;kLlY(",
"cJRV$w?d2car%R<ALOSO?^`4;ZZtI)%f^^G^>s>q9BgTU4Ht-tQKZ7Z`9QqOb?R#b%z?rk",
">!CkH7jy3wja4NG2q)H}fNRKg8v{);Em;K3Cncf4C6&OajV+DbX%o4+)CV3+e!Lm6dutu(",
"0BQpH1T?W(~cQtKV*^_Pdx!LirjpTs?Bmt@vktjLq4;)O!rrly=c*rwTwMJFd0I57`hgkc",
"?=nyI4RZf9W$6DCWugmf&)wk^tWH17owj=#PGH7Xv-",
"?9$jnjwDlkOE+BFNR9YXEmBpO;rqEw=e2IR-",
"8^(W;8ma?M3JVd($2T>IW+0tk|Gm8>ftukRQ9J8k3brzqMnVyjsLI-CKneFa)Lxvp_a<Ck",
"QEd#(pMA^rr}rBNElGA=*!M)puBdoErR9{kWL@w=svMc6eZ^-",
"(vQZrV<u^PY#nOIUDJ8%A&;BUVlY9=;@i2j2J1_`P>q40f}0J3VVoWL5rox`Kptivcp}o5",
"xA^@>qNI%?zo=Yj4AMV?kbAA)j(1%)+Pp)bSn+7Yk`M{oE}L-Z!G6<Dgq&*(C-",
"mFJfbEGDH5M^vBr65rcnsx*~|Em_GeU#B)(+T!|MG-nxj0@IPbp-nHejH3~>OMr5G+hp)$",
"3Lg{ono{4cN>Vr&>L4kXH;_VnBL5U!LgzqE%P7QQ*<E!guRW2SE@ayq@)G2nXqA2tGoQIg",
"c6>tue}O`3(TZ0`aKn&~8trOQ-rBXCp)f@P6RMO4l0+;b|5-",
"pk9_ryNh}Zc*v%mvz_#yd<xXt%~gT90dn4e{Ac<baL-",
")Y{L7&5G($I$>6fjB0g9{MmMnu8bG%#C~ugXK^S^k@?ab#O|aE>dDTt4s4n69(~@t~!wni",
"V%g<uWQat_i6>7khFx~I*4>Y|V$4j5%KPF*-",
"FyKIi@!Ho&x8QQsksYt8)D+W)Ni!=G`ogSu^vLL-",
"l#7A7=iIAKL2SuZk9F}NfNk86VI)9WZE?%2wC-yaF~z#Qsq)LH0|_D8^5fU8X%GeQ4TB>R",
"-dlziA&tZe&1ada208!$nk`7bOFO2S00G<w{Sp8G{cR_IvBYQl0ssI200dcD"
],
"America/Los_Angeles": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0qH3OkDsf7KxBg5R*;z{h&-RlhRYu$%jt%!jv+I",
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0q%JPh9{i7KxBg5R*;#4G>H)lhRYu$%jt%!jv+I",
"JxhE=%W1?wYb!37Rb?(rgwFIAQI{L#8r*zy!$TMtER_1(vn(Zix^{AVB1(jwr$iL6h0Z!2",
"8Gb~UW@0~e512{Z%8}Qzdnjl~wJ1{c2>`Z@1A~t&lyL{p{eM{5)QGf7Mo5FW9==mlyXJt2",
"UwpntR7H0eSq!(aYq#aqUz&RM*tvuMI)AsM?K3-dV3-TT{t)!Iy#JTo=tXkzAM9~j2YbiO",
"ls3(H8Dc>Y|D1aqL51vjLbpYG;GvGTQB4bXuJ%mA;(B4eUpu$$@zv2vVcq-Y)VKbzp^tei",
"uzy}R{Luv<C;_cPe*n$Z<jeC9ogWF9=1mvvUYXS>DjpuVb`79O+CBmg{Wx!bvx$eu4zRE&",
"PehMb=&G<9$>iZ|bFE)0=4I?KLFGBC0I(0_svgw0%FiMsT%koo*!nEYc6GY@QnU}&4Isg;",
"l=|khi(!VaiSE2=Ny`&&tpi~~;{$u<GHlsr3Ze!iYsU205RFKsLnrXwOL?Mq08xffgS{6h",
"E|figx+&N%wbO}re@|}$l;g_6J-Wl%j|qev8A<T?NJ)`;2neGi_DHE4ET*W!c*ggPAgU+L",
"E9=bH7;maCUikw^R)UM;TdVvNkQ;FGgN=yQER`SZ1nOgPXr0LCebLety&}kVdmVmB=8eSg",
"td!1%p=a2wooIL!Da}OPXvKBfRo?YxqS>N}%f|7mBhAy;<Er2&_LfND#qXN~Mkgf!@4VFA",
"Hr%$c)wrKA2cJYWK2>s3YT^sy!$eG~?`9mNJC9@4Bac_p^BZh)Yd_rWW5qh-?tKY(>5VHO",
"L*iT8P@wCavLj^yYbnDR+4ukhS+xPrpl)iqB?u)bj9a2aW==g6G3lCJd>(+Blf<d4CF%7u",
"tlBUDki}J-!_Dy}5S(MrxSXy~$Z+hgH3P^<<w7D72L7I-R%H3(xm&q_DXxkp$owLTS6Wzk",
"hc3nn;laROa3)6hl&gH#)2Lif8fZe$@CdeJ-Zn&*>r)~^40F4f>cRZ^UF;RibfZ>0m73hR",
"C{$vTfC(STN`g7(B<=Z2556{}0`?p&|Akkst!4Xy4OT;A@c$XTUI3FRRjy*KA7uC56FD)z",
"^X{WV*sr(w!c$W357o!&eLO2wTDNOyw@gf(&R<<LF_3URI4=Ei`-%dM3T66j#9!aG7&b_@",
"g1-9vo?DzXZ5vGaf~w__p_@_X?OdvQ_r5bvy2hpESTf+{p?jL+!~!{g8-<-5$@d8EZV&-5",
"@a|;^1gB*R-~{EHFA-td_G2bt;~Y}>t;=-Tu1TV{>%8ZVATC9tjD8|(&`$9YHvZ9bVe#>w",
"|8c;Tg|xE&)`*}LwM*E}q}q8^Qja%p`_U)*5DdLI9O@!e=3jFjOCrCq28b_bb;s>%D#iJB",
"CWJi{JH!Js;6nfayos$kq^OEX00HO-lokL0!mqm{vBYQl0ssI200dcD"
"UwpntR7H0eSq!(aYq#aqUz&RM*tvuMI)AsM?K3-dV3-",
"TT{t)!Iy#JTo=tXkzAM9~j2YbiOls3(H8Dc>Y|D1aqL51vjLbpYG;GvGTQB4bXuJ%mA;(B",
"4eUpu$$@zv2vVcq-",
"Y)VKbzp^teiuzy}R{Luv<C;_cPe*n$Z<jeC9ogWF9=1mvvUYXS>DjpuVb`79O+CBmg{Wx!",
"bvx$eu4zRE&PehMb=&G<9$>iZ|bFE)0=4I?KLFGBC0I(0_svgw0%FiMsT%koo*!nEYc6GY",
"@QnU}&4Isg;l=|khi(!VaiSE2=Ny`&&tpi~~;{$u<GHlsr3Ze!iYsU205RFKsLnrXwOL?M",
"q08xffgS{6hE|figx+&N%wbO}re@|}$l;g_6J-",
"Wl%j|qev8A<T?NJ)`;2neGi@^_$pxs;>M<D!I{tC-j+T1~*cwYttg9!+Q#-",
"3v6Saj3h8^~MB$h|*(;kAscyJKINq{Cf5mX^-&hWdY!FT=0og-vvZbyAQtkOs7Q$E{N_AV",
"!!c1+D7Ub{;^)2(>iozmmC;M)^ZwTGGtj|3E=aVrD5Bqa^;RV?wWs=hsohk2ql1XcqaOW|",
"E4urOCR0a_;Id)cJ-GU5w3iJ10Aox!FH|8SHoiD_{JMEEOV0O?MDsDaI;Y>v=$rgN_q~t`",
";bL0p0cr+3oR<~L*q!A1N=YxYRoqaQo%ua#07hqE;U_X@B)L3Jd;2oN%<UMR&=A0TqSiEB",
"wgSrE^0emfRx6XBw90Y7S-{?+qA5<Lkx|CXym1<j0It$``e-",
"X+>}66Gz+0zO^<`*Jt_si5v;Qg-",
"3q})ae)#%Dl>20mc9;6%A4(VObs;0JpQAGD7ADLJ?<x`z`b)-",
"OhP%tJ&2xIc5Yafu_9A(Mj>Rqiv#V5ZljDomsklGi_^8gtLkmRF>EstlBz)cI_>MG*?Z%S",
"T;72Q8^Ku7t$MI_A1{5SEE>mII;1T8kfJf_FgrXNwx!AelhM`I@+>J+gg!uCyW&Vjnd0r~",
"xZpKPsHdS4@$h%w6Eq6>tdfnmQLWvk`|e2>DM-E4Qm9Nc-",
"(V_Gh@)x^Nj3H9p`fLm1IA_eaBVtRg`l1J@5pcFi$-",
"31XWMxJ!;}xqC^I};&9=Az*&VH*lfN0M|Gx-9!ypV67!-KSwt=VI-*jej4FC~WiMTU+-",
"L<2+d_ew|{n~!VJuGlSJ0d_=`@?_jC3h)r_?|J_;aJ|`U^>F6N?3pcY069b8yy#(^>pP8L",
"*@Vg?IIMosq$u-00HX=q!s`G%_Rg?vBYQl0ssI200dcD"
],
"America/Santiago": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0fRZ<6QtM7KxBg84(fsEAUJ$J{f-TXlPEUec5Ee",
"n+hsD4lC(QYax=JdSpoyje8%VM`GW}<Unz6IOY4=y66tfqG2X4E8xIJQ(~?r{`L~T!sI~o",
"VBl7Ao!R1A76Y8P6Y<TfwVHf@sl@S-D4OuAy5mq0MKJZ>{bJ8@y$A8O&*$pw{(f~Os#}2w",
"eX6^Rgi$IT%n^V^85L>$_c7{cB^#ogV=rHBJGiz-RQNFGK?gdPi|q)j`&8)}KJ{qo6dixa",
"9@yYyVg+%lo0nO+Tw0-w2hJ%mafy<Co(;L+24CYl&?rN0mrh90nxG?%1&Ed@za`Yd>WL)|",
")<o0dZL-*?RFtH7dAv%G*O%l?qvq!0F5C?K#_ZoT{P$77IMoj3&8w3f&n36zquu~s`s0T)",
";>?W6Bi%FWuGPA1Dru$XR4SZANsAthU2EoKH<MU4wYvUTlZGcLIDR+hSik>F6oEtKq`rwP",
"(VNegnI_NI%;ma$)wj{k!@KFB30Yo)IOr<QX7IQ@TBq9d;e3QAtYU?$PS-WoaiqwFrg4PR",
"A->l>)$)D|+(5h&+%2vuwGuy^@S8FT^s21V5};>VA9Iu;?8bHz#r<;JtfZDI1(FT@edh0#",
"MYW$A1qkMGIwTZqqdYNE3gl#zp&NbL9Mp=voqN|;?gqR&4$)1`znddtEyuKS*^nMMD=0^>",
"7^z6-C4P67UWOXuMBubP>j6i~03aR@jD^-Y`JSYu#Yp0P8dLLJ0QOPE8=BoiuRX59YW7xg",
"WiexjHX%&0?`ZQCdxCdL^qd1v@kOjQKaWo2Y1++~LcA%FTq?5o<?(jL(_Uo}I}k_Fwflcr",
"aovwSR_(ILA6li<iBLPQ0#rEet;W-*54kj#sZEGK*tAF{)HNkn#&Hc5`#eaRF;N#$<xQU?",
"E%zm?2+b5Ho>%}fX1-RIvlB)1#iTNomGnUL=nM!>Ix|AGtON7!F1O?53kqlC2o-`ZGw*+s",
"NM$^9znsIJMwlgscE`|O3|;BRgsQMYm~`uv+nvuv`nigRa}X=BX=A5Sw$)WEklF7&c>_~$",
"zJ(m--bqXgiN^w-U=BJH9C0Qro(x90zo@rK;&TJ$nI@&k$ORgOb2<MjjIhYfr;pFUGdMd!",
"0d&bOvyq3AZPCez8E(XSg2hBu2A&^k?w|1u8v3JE>s%gWbc}ok_27)Eoku~Fq|B-Ps+4J_",
"HPJMLJ2^_)cOU$p&3kNAlrV!)%~6r$BJ>OOi~=-<6byle{?zd4J{NG}o8tw|+#ZNLcpNwk",
"TuPE~sbJB8_RZb2DopStO+Wwux~F#S59zm%00I98;S&G=b(j+6vBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0e+L++6@F7KxBg84(fsEAUJ$9Yj;p>Mq3>*@-",
"8>razI(EFyOtYzTyCY`3kVX~k`hSJt^$bT@V;iGRbT`JI3V)lBnui#lA$wIsfE8sL=RFVi",
"Wi5>;Y=+h5(#B8hW33CV<Q;A#rKAP&WQAy#{Hgd}k5cfcABuYPC#AID{sCG^k7cV;Yjn{S",
"|8$i(c-",
"ZWn+h)T(*sa4gX}q{a%>CYO92<z+UyE(W(N=OBKTA!5FCM+4etR}Gx{Khjr_hfVM-",
"3bY_3-f(4kMdWkgjQjaFM1%?zB^dnmPD^*OEjG@|L4T9oM==J-",
"1~cwbTkO+{<rZ1j%y$A#;`~jj_5>MtE<w1^%V}&!W~^`U#G#Kclvxg2eSK@Sb_pe)d8JOq",
"iIv8N?HvCOAa(Pe)%y{uRZ=iSffMAb29m~K3V!5$(+5c4`<&%}YBXxa{Nmro^{EpwXUg(M",
"d=g45)b1lm^o9O|j=$7W0%Fg!1vvZpZsS3gq(aYGRPC6r&`5zc<D8jH3uj3*Twj>mXQtd@",
"E+3y)$%o~XozOC|QgFl*-mp9+(`s0w4O4a$v!xt`G&ZI1uE4c`JE-t5WW%1*91J-",
"tr^_>U@@l^R_i@%gJ)q{kYX|w-",
"m{DbN=Z+Zey_(<=ZlU4e**~fkShDtX&9_W<ytT83B=B(s`~GrFf60my*fRSlCwvb~u-",
"3SYP<jcj2q)<&>>qj>RuJZ-ko&-nY}Eq_?SFX8N<9wT-TH*xbF0XT;L+siI1gPQrnKc|85",
"UZaq6@7JAVeu%)Aa6I>6D;F*N)|f6jhs01q_UU%gc_Ek#>SbTrf6i;OsAQ>8&urTeX|Ier",
"uSV{o||MjbXV?)AHzh1+v~odHV<_+-",
"M0v=QR6~y%&K00{%grZ=+5)JNlFg!9Q}2rv{$OC*D{_yiUd$SL!S)f@K#pe<#CBb&}BH0p",
"%w(SzGJjGX`q+ybM!e4GEP>b&S_4lnbBR!*5>ZIxYP?QK2DIny}eL_od7Ll|WdneS(sbh~",
"!}|r_VlzIOB|lsf+rf*<iF2;5mq&ws+~(vXucW*o<B(-B;$$S+_(`@_xgEw=t_P_S=K-",
"2*4@GPK8pEDecQUL_<_<IfI`6dpUo@#J|q(HK=)fN2`##&}B&C<HKnv-Zo68>B~wqYWzu*",
"k-",
"19B1stj0gO@Kd^97k>cVnNvEEaMwZ6=ObszLNnyDGMW0?9>PhVq$r$BCx^ZdJ#fO^fC1JG",
"oW>FS5oC!pr7=y}O@ecL6T+R$E20#GvlwVfswl?qnCEe|@!hI*Ooyk8~vfN}h@~NGp9$00",
"H<1(-Qyy=a@C3vBYQl0ssI200dcD"
],
"Asia/Tokyo": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~luMgIxeB7KxBg5R*;y?l4Rl4neXH3cv!OtfK@h",
"KZzauI)S!FSDREPhhBS6Fb$&Vv#7%;?Te|>pF^0HBr&z_Tk<%vMW_QqjevRZOp8XVFgP<8",
"TkT#`9H&0Ua;gT1#rZLV0HqbAKK;_z@nO;6t0L<i8TZ+%T<;ci2bYSG1u!mUSO5S3XcbN8",
"dIxbZ00Ex?wE_SDJu@vkvBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-",
"~luMgIxeB7KxBg5R*;y?l4Rl4neXH3cv!OtfK@hKZzauI)S!FSDREPhhBS6Fb$&Vv#7%;?",
"Te|>pF^0HBr&z_Tk<%vMW_QqjevRZOp8XVFgP<8TkT#`9H&0Ua;gT1#rZLV0HqbAKK;_z@",
"nO;6t0L<i8TZ+%T<;ci2bYSG1u!mUSO5S3XcbN8dIxbZ00Ex?wE_SDJu@vkvBYQl0ssI20",
"0dcD"
],
"Australia/Sydney": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0T)o7+nA=7KxBg5R*_t6jS5T`_Ull(nK1_YY;k%",
";_YdTuU3*!K)eKg@^kzjAtbo@Jd|KGai=Q%%sX5FI?*?LG!|m9cKH5~IEwI=PAr_Yc}w35",
">}hOdk<>TdUa07R(LPI6@!GU$ty4=mwqHG-XVe*n(Yvgdlr+FqIU18!osi)48t~eWX8)&L",
"G)Ud^0zz@*AF+2r7E}N<P$kOfo*88g)_bOO?7N1Jr|HJyg+HXc7f4}?%Dur3w|~JU?<x4K",
"%RRC~q_D87;UyN{nLRu!fEqKeRR*U$vs>f9Y72K~o-T%}D&z%}#7g<qim`EbfhF7ntyAiP",
"%LFNc&!$@Kv)Olyf&Y9%(#SkM+%yI}S%b+@ZM2dH7DpmndGMIda<(`#E9q|?H(HzClx+l;",
"M?IEz1eF}r?}ay!V9?9rKD^-ayjE@wUMD$2kC!iwH`n=eVrJPmJyNKaW`LdJ68&u;2nF1K",
"kZjKCY_A<>2br?oH6ZiYH^%>J3D)TPKV(JY*bwjuw5=DsPB@~CrR<E_U_fJTF9ufU%!cXK",
"_4uM#!%%Q1e1G~{E}~vGVE0{Kxecm^NjtJM`c8EFHFTiUIVl@YUD8F+s!u8jz~6hte@oa|",
"qayb*^Lwd(etNmBro;aXQjkY8g(*`_JQ0%{V3QP2l!GGQ7D+v&k_PK0F(?f{GziU5>OZeN",
"x>A*H&CHrWt0`EP`m!F%waepl#|w#&`XgVc?~2M3uw$fGX~tf_Il!q#Aa<*8xlzQ2+7r6Z",
"^;Laa9F(WB_O&Dy2r>~@kSi16W{=6+i5GV=Uq~KX*~&HUN4oz7*O(gXIr}sDVcD`Ikgw#|",
"50ssal8s)Qy;?YGCf;*UKKKN!T4!Kqy_G;7<gSrPK{)5#a>PfQapugqvVBKy12v3TVH^L2",
"0?#5*VP~MOYfe$h`*L!7@tiW|_^X1N%<}`7YahiUYtMu5XwmOf3?dr+@zXHwW`z}ZDqZlT",
"<2Cs(<1%M!i6o&VK89BY0J7HPIo;O62s=|IbV^@y$N&#<x=a876<(U>=>i^F00FcHoDl#3",
"Mdv&xvBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0TQa5M2N&7KxBg5R*_t3)o}P`_Ull(nK1~U=~g{",
"!2#v7ta-VPM|>+w;XP~5#XzS!mmq>y(@${PQsHdkBYiJixTuCQ2N?55>HhxKTEufgeVl}_",
"Sq_FUIXFF$kI6!-",
"eKVzudw1Zyg8;a%MynCz&_3Cl@%Zs*_JRo#I#Ss92V)!~{AwVung!|wK?4JFyFv#=nfhNI",
"6Czr|3VjINV7V6#eFr=Gl1UDdL9MQRWZ`RiZET+f<B?psO04A!^T@|OjD(g9%6r^uKWL~*",
"l5l((#kszU!MR&R^KmhB+uQG;V@@lt4iky54!7C3s3sgfut6UfX5na|U#Q8$>l#=kxZ_-",
"G@=#VCQEvCZVdj9;e?rY8ynpo&34D<xf3gdy0mso_&ACxT<X@=iK)(!)Cf=UJXS@mJ>Pa3",
"KI_Sc_0#?r^44F}Rzb_ChM7PPrq3@FkqqjWqgvOzPq+vkb;DT&2YMC|tC3lu6Ihz2prrFP",
"vOU@6I3$XU@68kFFWx8fx?jt@frXW`;AZso*plI73>*wENVoc+2cm`jgoe6(B`w`{Bdx<i",
"@MbN&WNDIQhD$&B-",
"uG<$!*}1d++*d~BtG=}tp$n^wQ7tVUh5HXx%pYRBr8eErtbe5x3d1_@!5Dfbr?|~LLM#@T",
"Jp{TC>VOWD1rqV<x%ZP)Pc1Wb+mhO{S8(-",
"3ndm1jWw?3!z_VRO<>TCN+Y4kRvHLYXlr)vU4G7S$p~Ia_n|G>=3N5dDB8cS6IGd-ckfc7",
"8#^71HtN_BO2#F%&%Ll+IhR1DlEUG@+#_Yl8AKa1US?{&+yOwA=WA=!z-",
"B9t2>Tn9Wu2*8Ydrbage(I4dkD_vcP(|c-",
"LhGRkSn{W*nk?RJQN!j1RSZ}L6e<&UGMisk>nLH5JiLNgOSC7c&Mn1+Pit*Gz;s=M?_q!v",
"DZ@dJctlzl2>W(rvs{(CERM)RT0xuqDAl{)$*ztEhe=?|;3u&!6O1c88W}SFHDBBHh%rrh",
"U6=f6kjYt!N19xk*s_9IpbeS;(cPoTPX!vr00FE9ju8L=r}(KdvBYQl0ssI200dcD"
],
"Europe/Dublin": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0>b$_+0=h7KxBg5R*;&J77#T_U2R5sleVWFDmK~",
"Kzj5oh@`<njquRZ&tJIS(cXp1>QKHvW^6V{jU-w>qg1tSt0c^vh;?qAqA0%t?;#S~6U8Qi",
"v&f1s9IH#g$m1k1a#3+lylw4mwT4QnEUUQdwg+xnEcBlgu31bAVabn41OMZVLGz6NDwG%X",
"uQar!b>GI{qSahE`AG}$kRWbuI~JCt;38)Xwbb~Qggs55t+MAHIxgDxzTJ;2xXx99+qCy4",
"45kC#v_l8fx|G&jlVvaciR<-wwf22l%4(t@S6tnX39#_K(4S0fu$FUs$isu<UOJYm|4)2i",
"aEpsajn@}B#rnY=Cg_TXsm-A)*adXV&$klNTn3n{XXlaquu}6m{k%oRmY0Yyhlj*<W{D5m",
"22}OiqnwHT!tnK`wPqx?wiF%v{ipTrOkcJ5P@7OC4(-l`*&SB$Wd4Vf8gn?>d<i@%mP*e*",
"ttDj`9M1;9$YV@dhT)DVcwdq(Ly~KDm_&KL?{_mFwwYtJqRZBk)i1FVQy!40w_KyAg?hIA",
"=_{(3#S0eWsF8f%_4Zza$4@$lSmov+Huyn$vP^zJ|8-<C3#q#0kEs9cNg^xUR(m?wEWt-D",
"GctAh2nIo~fz%$m$I41=b_WuJ6M9g#A9_Epwqw{d0B|vzmg#_y<=_>9IKzCXB<o`d)**5V",
"6g!<<Jw1n5TrN-$)aYz4cLsTmpsUf-6L7ix+kk>78NkARYq@9Dc0TGkhz);NtM_SSzEffN",
"l{2^*CKGdp52h!52A)6q9fUSltXF{T*Ehc9Q7u8!W7pE(Fv$D$cKUAt6wY=DA1mGgxC*VX",
"q_If3G#FY6-Voj`fIKk`0}Cc72_SD{v>468LV{pyBI33^p0E?}RwDA6Pkq--C~0jF&Z@Pv",
"!dx_1SN_)jwz@P$(oK%P!Tk9?fRjK88yxhxlcFtTjjZ$DYssSsa#ufYrR+}}nKS+r384o~",
"!Uw$nwTbF~qgRsgr0N#d@KIinx%<pnyQ!|>hQB(SJyjJtDtIy(%mDm}ZBGN}dV6K~om|=U",
"VGkbciQ=^$_14|gT21!YQ)@y*Rd0i_lS6gtPBE9+ah%WIJPwzUTjIr+J1XckkmA!6WE16%",
"CVAl{Dn&-)=G$Bjh?bh0$Xt1UDcgXJjXzzojuw0>paV~?Sa`VN3FysqF<S*L0RYSAY3jt(",
"8wCD04RfyEcP(RNT%x7k(7m-9H3{zuQ`RZy-Rz%*&dldDVFF+TwSAPO1wRX^5W5@xJ9{vW",
"w?rc^NH({%Ie<rxKqSVy!Le-_`U&@W_(D+>xTzfKVAu*ucq#+m=|KSSMvp_#@-lwd+q*ue",
"FQ^5<D+|jLr?k{O39i8AX2Qb^zi9A<7XD1y!-W2|0Hk8JVkN;gl><|<0R-u4qYMbRqzSn&",
"Q7jSuvc%b+EZc%>nI(+&0Tl1Y>a6v4`uNFD-7$QrhHgS7Wnv~rDgfH;rQw3+m`LJxoM4v#",
"gK@?|B{RHJ*VxZgk#!p<_&-sjxOda0YaiJ1UnG41VPv(Et%ElzKRMcO$AfgU+Xnwg5p2_+",
"NrnZ1WfEj^fmHd^sx@%JWKkh#zaK0ox%rdP)zUmGZZnqmZ_9L=%6R8ibJH0bOT$AGhDo6{",
"fJ?;_U;D|^>5by2ul@i4Zf()InfFN}00EQ=q#FPL>RM>svBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0>b$_gw%g7KxBg5R*;&J77#T_U2R5sleVWFDmK~",
"Kzj5oh@`<njquRZ&tJIS(cXp1>QKHvW^6V{jU-",
"w>qg1tSt0c^vh;?qAqA0%t?;#S~6U8Qiv&f1s9IH#g$m1k1a#3+lylw4mwT4QnEUUQdwg+",
"xnEcBlgu31bAVabn41OMZVLGz6NDwG%XuQar!b>GI{qSahE`AG}$kRWbuI~JCt;38)Xwbb",
"~Qggs55t+MAHIxgDxzTJ;2xXx99+qCy445kC#v_l8fx|G&jlVvaciR<-",
"wwf22l%4(t@S6tnX39#_K(4S0fu$FUs$isu<UOJYm|4)2iaEpsajn@}B#rnY=Cg_TXsm-",
"A)*adXV&$klNTn3n{XXlaquu}6m{k%oRmY0Yyhlj*<W{D5m22}OiqnwHT!tnK`wPqx?wiF",
"%v{ipTrOkcJ5P@7OC4(-",
"l`*&SB$Wd4Vf8gn?>d<i@%mP*e*ttDj`9M1;9$YV@dhT)DVcwdq(Ly~KDm_&KL?{_mFwwY",
"tJqRZBk)i1FVQy!40w_KyAg?hIA=_{(3#S0eWsF8f%_4Zza$4@$lSmov+Huyn$vP^zJ|8-",
"<C3#q#0kEs9cNg^xUR(m?wEWt-DGctAh2nIo~fz%$m$I41=b_WuJ6M9g#A9_Epwqw{d0B|",
"vzmg#_y<=_>9IKzCXB<o`d)**5V6g!<<Jw1n5TrN-$)aYz4cLsTmpsUf-",
"6L7ix+kk>78NkARYq@9Dc0TGkhz);NtM_SSzEffNl{2^*CKGdp52h!52A)6q9fUSltXF{T",
"*Ehc9Q7u8!W7pE(Fv$D$cKUAt6wY=DA1mGgxC*VXq_If3G#FY6-",
"Voj`fIKk`0}Cc72_SD{v>468LV{pyBI33^p0E?}RwDA6Pkq--C~0jF&Z@Pv!dx_1SN_)jw",
"z@P$(oK%P!Tk9?fRjK88yxhxlcFtTjjZ$DYssSsa#ufYrR+}}nKS+r384o~!Uw$nwTbF~q",
"gRsgr0N#d@KIinx%<pnyQ!|>hQB(SJyjJtDtIy(%mDm}ZBGN}dV6K~om|=UVGkbciQ=^$_",
"14|gT21!YQ)@y*Rd0i_lS6gtPBE9+ah%WIJPwzUTjIr+J1XckkmA!6WE16%CVAl{Dj@)nt",
"QW>5WCgG;uYjCg(j>qCs(*}h9<Z8uh9!SSPmJ9Us;ag{DE^LB{a>IWV8w>LXsNq+C>vLWc",
">tZt7asixk*XDd^+o!O=O52B7aPqt&HfgmRCd4-+ky?P-",
"XP?5Np6dxH29G;c3@torNWCs!ufT_DK<~j-",
"UgqAdf5G4ZdP`bECDkw8*?qvrtnMfoJT^Wq|8_~4;n!EAt=*dv)-",
"0yF8uKXAiZ~3d|7Z>ZO|t~YD2@;`6Qg{xlf8Q&;*xB-Ys_R2Cp{y((HBw7kN@@CtfA<<$B",
"fGc2BhK{XBpaQiJGWmO!j9XN7n0YhjD8vjw0{?{0zZm5js?lNS*8*{rU&d|`Rl_i(p2n<E",
"2E*;E@{_pkawrja2!Gd<d7B3gss7z!RY?4jLVEtA|Zd9xtHrRH@!`4#&7dlz(EmqJ50R7o",
"WFfxIQ=*$MQ;FXMyzYE7tuZ8vWD!}Q(QFk)FlkhmB%|FGo4d;`{0trwc>Hf}VB&&P+m6k^",
"y|9Wu}iApF2OdyoJC2VV0^9?>qd00EN<q#FPLQfh6$vBYQl0ssI200dcD"
],
"Europe/Lisbon": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0=rf*IfWA7KxBg5R*;*X|PN+G3LqthM?xgkNUN_",
")gCt1Sc%YT6^TTomk4yVHXeyvQj8}l<;q&s7K}#Vnc8lII1?)AHh$*>OKUU4S;*h>v*ep0",
"xTi1cK2{aY*|2D*-~K<;-{_W+r@NvZ7-|NZv($ek_C%VfP0xjWeZP#CPXD`IKkakjh(kUd",
"&H)m;^Q(jGjIyiyrcUMtOP)u3A>sw6ux;Bmp3x$4QvQKMx5TrCx_!$srWQuXNs&`9=^IY1",
"yc&C31!sQh7P=Mk*#6x8Z@5^%ehR8UW<EvzdWer9z;R6PrdUaWab3G>$OWw0KMw}P1ycI^",
"4eh12oBUOV?S>n*d!+EM@>x#9PZD12iD=zaC;7`8dTfkU_6d}OZvSFSbGgXeKw}XyX@D=(",
")D0!^DBGr8pXWBT$S-yhLP>Z3ys^VW<kSQr?{jhl<+{Fki;mTI=&Stgy$rttN?ulQM$lDr",
"G7))C7Dx=J6V-e^(Qk|r;f~TvIw1KqRIC{8f^jPy#blstV{-&2a}ZJe!Zr2c_R4NT)L@bs",
"+gRRm6Wn)VWVNHeK*TEV=f#2KZqu%y?mTx#EfRiK0)TG7$$~=LGxx@0D|lS2up|oCON{YQ",
"oN5-H$!_n-Kx2*=RO!epEX>3}RQ6{NGGVJG6vf*MH93vvNW6yLjie1;{4tVhg-KnSf|G`!",
"Z;j$7gJ1ows~RD=@n7I6aFd8rOR_7Y?E-$clI%1o5gA@O!KPa^(8^iFFeFykI-+z>E$mvp",
"E_h`vbHPjqkLs`Dn-0FV`R@z|h!S(Lb;M&|Exr<u8#s-T(>!biY`%bfp$6`hK;GDhdP|^Q",
"*Ty*}1d41K>H2B{jrjE9aFK>yAQJBX9CD%-384S;0fw`PlprHGS`^b$oS-`I4VH7ji8ou-",
"g|060jfb1XcxiInT0oO<S+<vh^)XY;lr@|IeXj}%k;}|kSlDGaYidk^zB|gEYaet~F%QYd",
"f7pbnQKLZ0o7=kso86doS;J@aQ>oeR7#%e5Ug5#KW)nV<Rc;|LjUDdhk8*dYJQwYN?hzH%",
"0<XB$!(rpf2nxaL22M`L4pKx>SRvLHNe$SQHM@2)`S9L7>RL@<XAlxVQfb2=%lcu!h+Um0",
"Q+Z=itevTFy}-Jl<g5crK55BF`VsoPH~qP3QrG%YtrD#s{=gA7p)QI<i=EwY(cel8`B=#u",
"Yq<K;4T(QBF_GvrYueSk*}gfrCSg22+YH-1N<WYkp|DA-P-&va<Xu<}^yafJKlzezB-lS{",
"a++P_^gYmgrc9FO-K3s~`jAcqVV!k?NV2IFV^86`cr>Qx%fmm7?3u7P5TywFQ}C@S(pq}|",
"eLPT{C^{<0Q?uU&kSVd%!~8q3;Z0s3OqzF`$HRkePL5Ywgiwn{R(<RY8ut&RJ;$?J*w*n)",
">zi+jmOBFrVpW;)@UsU#%$8BcV#h@}m$#!Fglo&bwb78aYqOG_W7h{eb(+39&-mk4EIXq_",
"_`30=8sfA3=!3TO_TyS5X22~?6nKngZ|bq=grdq=9X)3xAkA42L!~rmS)n3w-~;lgz%Fhn",
"(?rXdp2ho~9?wmVs2JwVt~?@FVD%`tN69{(i3oQa;O0<Hp#T5?$WIy3h`IlL00Hv}jT-;}",
"Z2tpNvBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0?wE>|FpV7KxBg5R*;+c_2(RlxFoAjepiN+t(&A",
"khpF;7sO(r>4o7M4zZVa$XY~v@-",
"@YI68ej+V4yBoGpi5ZYu;Zw>N;+Vkw)1TIVA4;Vn*JV=%f`o+fC9=H@gz8U6N-",
"<zlQBNWwuXloIY|=>M+jj4KAFBq5|dk<~RQE?jJV`tG;f@gAn7I{P&H39;;isJ!fMa`+wG",
"KkWXbi?P7&ivzm}BJ9guJ&D1=Ci_?iEC=T+}Ht)wPz7a<P1u#Hd12~xVtoKNVZZ9bK!~Gf",
";7HSY!7IC}gsZ4-",
"I$jT1i`pR(J9#tXon~s<`_^d}&`qr^@i)SF`Bb~U?H>zBV$vs`Fg0TYba5j@iBgmJ}w{Wg",
"C;WaVQ5{ZjAIYY-lC8|yfnqplNF0_6r6Jo}h*-",
"^e6n7IHY9#^nyJM($bLr;XKyoo4e=k4E_rXAXU|8;BGkQ^kNAtK@OYtPw+AVo@wAk+F{Xd",
"YfNlG<<-FnlW4;LdIej=F0WFr~-c^-",
"M)7hP!$diHGAim;v4<LV)m{cX4!QE}&4=e0*`q#JM6ECR9=km919W`%;z(u&c4)$k#`^5Q",
"%<auc|-%@#^72gOa!BcLl_-",
";@{3;XMzp%%Z(_TQo{SP(u0)+V(>c$Ui^z<`A!+F8RhS5DX<Qkb`v1Ppx<w)pQ`Oe9w2z$",
"W;R3APa$u$7+V=T0vX6Bq+}~Oi`T<CxP?En5_AWCA+WOr6Ba&T8?i(2iuLXc?c0Eb0#8fx",
"0;z)Qdw{&iu1p_}NcXrEZj<UU?o^>aZomO|PHR8B%eNWlI^Y;%XH23*BQ9Pv<U}>!0dNOS",
"JW<Ev&&4jcJ|ItzHY*}vkh#@p?GI~Z@92UqgCTvtS*M+R6>;D6iM<TKxRt(MO)fK0B`f-",
"Tgh%d%!X%ul%RzqJ&<cgV@U9}77Km>}OHVD9S;=_E<kAo67wo+uzcCI2-",
">GP=j^CP6mQ~WcEd%i!wnK!|`afKPHZ{nSFK{L!7$MM299!v3kGuq;oa;+;@@n1r@0<Jpc",
"%OKHC*qZY)V7YMh$GJ7uZJ*pknb#4Lndxc5t*p~=j{8%jTcs@J`sWW0;gyRp=j6{m!9jg8",
"yBY-uv0ppo?(LlG%MXywQB)0kk7!saH~{t8ok=ap8ckC*S;vkAK<RzcLxv+K_UDp%n=;eQ",
"$32d2IQTOvm&n1x4gxS+SH)m=#|D9u~O6QLYUFTM@z@V2^Z*27`WQlTcSk<fG)JHRfF0un",
"`wvtDD^{B6fp60>Kc%yl18x$nR#Kzi@<EMdmds2!rvB-",
"%G=Qs8_PTjcEFU+5%TqHsrny=`4g%T<w?<?*L7vocihTo#Uf$}3PtSt59Gp?gp!2>?+E(y",
"7o2rS3FClOOT`Amt}UyGfYZ7QWYqqCA(t;kD}FGa+vi>Zi)j}a2lrvx_};$<>-",
"h&pt_ePq>W5KHZPr_P0CVzjF(&G-",
"bL>Xi>@Jv$b)=hLcP>(ngUOdeI5{4_W6pSrkv%rnFAW~Xq0%BpP=^5An)E2@X9`)$o<{(F",
"VhGYMIW)@T&B7JZ06+RhK~>g?V;J{)+)M6xch^XnnI^Q0TC;zLSj(LMJ&sOV*Qj&~gGAP}",
"&ujCT8Fj?*_GQ!H)<ip+T(TLY1KvvUUC&|zS0?)a81xs}-pb-",
">00D>!#~T0u=fLlPvBYQl0ssI200dcD"
],
"Europe/London": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0`|pJ6!-O7KxBg5R*;$9DqzW!kQs3DZt(=0_!m1",
"4wvE`6N%Vj#u6PS_3S?~(2)&xn8}2}3Wr#kG8n2!x8>$E$lF&~Y#_H6bu6(BiwblJ>;-Fs",
"gA$Y$*?=X)n1pFkKn}F~`>=4)+LLQk?L*P!bhAm0;`N~z3QbUIyVrm%kOZ(n1JJsm0pyb8",
"!GV{d*C!9KXv;4v<seWRpo=ZZxGf)-5Qsn$3dw`uhF)+6#mgUoNF-Y2jN73pVhdTs*p0`Z",
"AbnT1puEtudB{Nul>D4Q>-k#+x(!V5L@w5M>v2V5<gcLskF+p`aGTSn{sY8^@MUc;2o{&V",
"R!$180N}BtfYKS)i9w=!<~&l?1Cv^PWs!&a9{s(35^yqGU$72DKX|IkRtDblB>a`B>t(|B",
"|Fqr4^-{S*%Ep~ojUtx_CRbSQ(uFwu2=KH)Q@EBs@ZqRXn4mU;B!68;;IQs3Ub=n&UU%*m",
"k&zwD36&JSwsN(%k&x?H+tN^6)23c`I0=5^N_R0~1>tsFZ`^`3z~rXSXT&qcwa#n!%+Z#P",
"PG}(D^_CCILXnF|GKwabBh*xFS?4rwGo2vtJUwzrbv_$5PO+`?$l{H-jGB@X%S!OAhw;D4",
"XFycN3!XqQ&EorJOD3>~^U%Luw!jF<;6_q-f-S|6<EHry?%{@fuyH`_+D%uTA@g0$5e!Yi",
"P1vQuevyS;jE(-R>{cQDfZ2(4Xf1MMLr1=SA=MwVf2%Pp%VP;jn)|5Tf!-DbUGn%I-r<KG",
"4jJ(Y#L-fJUpUb$yNfvhX*iqWZoG7T*WUfE6iQD9_^EWqExH`rc&jJ<o^E8-mM10WrZ_Vv",
"xx9nj<vMlEt*KfP*pyth!c_AKnrKtQTACX08#{pioAFnDq!53+h*hO^f*yrWjg0u2pUcgv",
"UlpEZ9G_dlhlW1J^h@gTt7{KPL2mRal;1juJ3Q8-!GXO#IPzT4ciJ-nB+nkphssM}Q7IAT",
"pM}AT%y(J!78F?>kYaH7?$$O!t)wwClAisr3eUoeB^~T=U*_P~Y2*KdnO87>B!19sV=xZ5",
"yApq26RxgqA|*tmsvtL#OhcF(C<0EGWHP)BF<g*iSWicU6k1<Ps?BQ$IWg-#s2uF-qXgJ_",
"!H_mZIMx*L%&a*_6;_trMCULk0ZYM<hfJlYBddHwRyYUDu3!C_lJZWTQ?c-R&@9054pj0k",
"kQ{Xi{A$&)&b#^G*}8w^qE5i<@aDxaJQs2E$W)AIqUXO{gQ;U8|FA%BD~sORzq44)AntUu",
"QHBO{{Pi<EpK!$x4(~7w)la!dN=M@L_j};6|5G&QfuO~2?Q7996z)78fqW<D#8tKNV(*qc",
"mfA>l?h)_*7!{LoJiv%RsOs!q->n+DcV%9~B@Rb<ISu%16c5H-7zQIq+SuS+s<lQOWK5+C",
"d*>C_1G_1g6`Yd~8|%-=2l~oGN!~TVv2Bnk>7wW8L@^?vX$f3AiT)(4nrCuTm9%(XC6Nai",
"E(;}7&=YZagjAN$O-cN;1u{dTkElmB0GT$|Wa)QMmKrx<|LCJ9qlUoFsUbD^H^6_8(w<0{",
"ftj&O1~p_%lh5z;zNV&sP<T$*OgK)_0B#JDtXOkhC;Bo7h)#RUy;vBiVLN-T$*7t*t9@ey",
"3Woa&24QZ_z38BQ@A(A<(9n@%R?}B`7%w2wowt~UU;bAlqCzr(H$M5t==jGIqMqCsE=Jwa",
"$3P+3^&|~i28@=d_u6Cgthe(Lq(wxKpdSDL|7X6Un<nrt00Gwuz#ISo`BbmvvBYQl0ssI2",
"00dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0{j(Ib8rM7KxBg5R*;%EAUJ=!kQs3DZt(=0_!m1",
"4wvE`6N%Vj#u6PS_3S?~(2)&xn8}2}3Wr#kG8n2!x8>$E$lF&~Y#_H6bu6(BiwblJ>;-",
"FsgA$Y$*?=X)n1pFkKn}F~`>=4)+LLQk?L*P!bhAm0;`N~z3QbUIyVrm%kOZ(n1JJsm0py",
"b8!GV{d*C!9KXv;4v<seWRpo=ZZxGf)-5Qsn$3dw`uhF)+6#mgUoNF-",
"Y2jN73pVhdTs*p0`ZAbnT1puEtudB{Nul>D4Q>-",
"k#+x(!V5L@w5M>v2V5<gcLskF+p`aGTSn{sY8^@MUc;2o{&VR!$180N}BtfYKS)i9w=!<~",
"&l?1Cv^PWs!&a9{s(35^yqGU$72DKX|IkRtDblB>a`B>t(|B|Fqr4^-",
"{S*%Ep~ojUtx_CRbSQ(uFwu2=KH)Q@EBs@ZqRXn4mU;B!68;;IQs3Ub=n&UU%*mk&zwD36",
"&JSwsN(%k&x?H+tN^6)23c`I0=5^N_R0~1>tsFZ`^`3z~rXSXT&qcwa#n#k^5sFT7HKtyC",
"9A0oluNf^P4yk-d_|mUd;T0;~y@2Cu@Xd;<hk`=GfF)t3<(W;^;#vS*2CIM0}lIP!ER~kM",
"z)8{sshyiGGP5>IRT#++Y*Z6*0%M4`z`3a2vnHc-",
"B(#xp*94c8t_y=lV~#Q=Whn&p<+Qzs`pL<^`L`BrF9ohSDa^SnosMt{zpFXpUjqumClb$D",
"^}s!&5JM`)L?H!Gzj6q@G0mVG?P#xu;u<zZ1><E)+3E=(_l2Q};6M&`<X7b>1DMoWN$14T",
"b>^F1?&ymdJ4TS`!aAO1#Myz0fU6<#?jJoc&X-",
"z&$D#Zj!Dg3w*<XVS=jFmPAR!(5h?6II3@of1!~K^$-",
"QX2G2lXMyPJhkXLlU>*{JNc(J;D0DrI1K1-",
"iv3~i7vFT?5yqP3rSI@jsz*k!0bE*<fCU<^XircA9v4*(~nJ^>~Pkn+g>u6Z;Se-",
")c$&;tI=8t;#10nr-)VFCx@^bf}Ak2a$g+MN&Pv7{W+wBYjvrBy)wwx-",
"|SlQynn<W$^Ik*J>=O3ULjr$jM*QQAezq5|DqO6<QT_YD!R$)!*RZ}#4L0{;;YiF!ta_vA",
"o_bRQz(yXVh(Pb?c@M0_Nw`OAh0wRSk&G;NwrYbla>3Zf+jql$4rQ2m|lPqGyA<>cV6bIA",
"o~*{`bgCh=A>WU}#b$t&5MPj$~|esT>8<kEOj;^~h1CXH8G(W#9e08Kd-",
"*F5|4A*&rbM;en?xL6l&Z{Ykm8CiF^r3y(3DvVoFpAb+ntAMvjr$pycbsU+9;c4roZ<eo^",
"PLSCW&pf@n2Q`ntKP;qNmw&SoHqbu1VHpCiJ*}hcmd((^642U>v-",
"C$_n!^iqI5;Q?X=YtG#wS0iS@Z+K+>Pw@;#vN~2NVaNcNiR~iPI$HEp0CZblbg!9944qJf",
"e^U%kyd74NW@4bR+8mK^RfSa}|2+8EsMDCnO<b%-",
"jeynUWspF(vCb3E!h#H}EYTD5FrIeRGbKg14?|O~2$j2xm;!0<!;WzH+-",
"i5d5tFbg7<Iby5Fk8J(*7&9_w84$GKMDrVHz;kCbn5G~;w8;xL!HKZvT=em^K46pU{o{_|",
"j^aXaZYS0#rN7ZS$)7F+7L39lWib5ukEMe<bXdIuFkbGR!az9hn%Ce>SZ@D)kcJx`Cx~hD",
"N7PV9xa~1y#{E$Hy#KX4zE=~k$7%Vm?00000LN&_Vg>>9w00Gqs&>R2&ZK8hovBYQl0ssI",
"200dcD"
],
"Pacific/Kiritimati": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~jCaVO;<!7KxBg5R*{K!`A|q%C5j6({{dSEy5>+",
"NF2>iK{8KMUf+)<-)VxXbLxD(alL}N$AT-ogNbJSMMYeX+Z{jS)b8TK^PB=FxyBxzfmFto",
"eo0R`a(%NO?#aEH9|?Cv00000NIsFh6BW2800DjO0RR918Pu^`vBYQl0ssI200dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~itMU0nbw7KxBg5R*{Ky3@w34e-",
"emlEe%!1&HoVz@2)g91$Qvf%e=}%J+k5^yTOu{Q*OrV;`agAU{YYC1>Bq#Lo)Ct}OFqAaP",
"SYvN>0#bjna^XrQl~da<70IbZ?+00000VC(mK^UzXX00DX60RR91ZYU6cvBYQl0ssI200d",
"cD"
],
"UTC": [
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~e#|9bEt_7KxBg5R*|3h1|xhHLji!C57qW6L*|H",
"pEErm00000ygu;I+>V)?00B92fhY-(AGY&-0RR9100dcD"
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-",
"~e#|9bEt_7KxBg5R*|3h1|xhHLji!C57qW6L*|HpEErm00000ygu;I+>V)?00B92fhY-",
"(AGY&-0RR9100dcD"
]
},
"metadata": {
"version": "2020a"
"version": "2025b"
}
}
+82 -8
View File
@@ -29,7 +29,7 @@ py_zoneinfo, c_zoneinfo = test_support.get_modules()
try:
importlib.metadata.metadata("tzdata")
HAS_TZDATA_PKG = True
HAS_TZDATA_PKG = (importlib.metadata.version("tzdata") == "2025.3")
except importlib.metadata.PackageNotFoundError:
HAS_TZDATA_PKG = False
@@ -316,6 +316,18 @@ class ZoneInfoTest(TzPathUserMixin, ZoneInfoTestBase):
self.assertEqual(dt.utcoffset(), offset.utcoffset, dt)
self.assertEqual(dt.dst(), offset.dst, dt)
def test_datetime_subclass_negative_components(self):
class MinusOneDateTime(datetime):
hour = minute = second = -1
zi = self.zone_from_key("UTC")
dt = MinusOneDateTime(2024, 1, 1, tzinfo=zi)
self.assertEqual(dt.utcoffset(), ZERO)
self.assertEqual(dt.dst(), ZERO)
self.assertEqual(dt.tzname(), "UTC")
self.assertEqual(zi.fromutc(dt), datetime(2024, 1, 1, tzinfo=zi))
def test_folds_and_gaps(self):
test_cases = []
for key in self.zones():
@@ -1009,14 +1021,14 @@ class TZStrTest(ZoneInfoTestBase):
cls._tzif_header = bytes(out)
def zone_from_tzstr(self, tzstr):
def zone_from_tzstr(self, tzstr, encoding="ascii"):
"""Creates a zoneinfo file following a POSIX rule."""
zonefile = io.BytesIO(self._tzif_header)
zonefile.seek(0, 2)
# Write the footer
zonefile.write(b"\x0A")
zonefile.write(tzstr.encode("ascii"))
zonefile.write(tzstr.encode(encoding))
zonefile.write(b"\x0A")
zonefile.seek(0)
@@ -1111,6 +1123,9 @@ class TZStrTest(ZoneInfoTestBase):
"AAA4BBB,J1/2,J1/14",
"AAA4BBB,J20/2,J365/2",
"AAA4BBB,J365/2,J365/14",
# Leading-zero day-of-year
"AAA4BBB,J001/2,J065/2",
"AAA4BBB,001/2,065/2",
# Extreme transition hour
"AAA4BBB,J60/167,J300/2",
"AAA4BBB,J60/+167,J300/2",
@@ -1142,9 +1157,21 @@ class TZStrTest(ZoneInfoTestBase):
def test_invalid_tzstr(self):
invalid_tzstrs = [
"PST8PDT", # DST but no transition specified
# gh-152212: the std offset is required (POSIX TZ grammar)
"AAA",
"A",
"AA",
"B",
"+11", # Unquoted alphanumeric
"GMT,M3.2.0/2,M11.1.0/3", # Transition rule but no DST
"GMT0+11,M3.2.0/2,M11.1.0/3", # Unquoted alphanumeric in DST
# Unquoted abbreviation with embedded or leading whitespace
"AB C3",
" A B 3",
"AAA4BB B,J60/2,J300/2", # Embedded whitespace in DST
# Empty quoted abbreviation
"<>5",
"AAA4<>,M3.2.0/2,M11.1.0/3",
"PST8PDT,M3.2.0/2", # Only one transition rule
# Invalid offset hours
"AAA168",
@@ -1184,6 +1211,11 @@ class TZStrTest(ZoneInfoTestBase):
# Invalid weekday
"AAA4BBB,M1.1.7/2,M2.1.1/2",
"AAA4BBB,M1.1.1/2,M2.1.7/2",
# Invalid Mm.w.d separator
"AAA4BBB,M3.2X0,M11.1.0",
"AAA4BBB,M3.2.0,M11.1X0",
"AAA4BBB,M3.2-0,M11.1.0/3",
"AAA4BBB,M3.2.0/2,M11.1:0",
# Invalid numeric offset
"AAA4BBB,-1/2,20/2",
"AAA4BBB,1/2,-1/2",
@@ -1192,6 +1224,15 @@ class TZStrTest(ZoneInfoTestBase):
# Invalid julian offset
"AAA4BBB,J0/2,J20/2",
"AAA4BBB,J20/2,J366/2",
# gh-152847: non-digit day-of-year
"AAA4BBB,J1_0,J300/2",
"AAA4BBB,J60/2,J30_0/2",
"AAA4BBB,1_0,J300/2",
"AAA4BBB,J+1,J300/2",
"AAA4BBB,J 1,J300/2",
"AAA4BBB, 1,J300/2",
"AAA4BBB,J0001,J300/2",
"AAA4BBB,0001,J300/2",
# Invalid transition time
"AAA4BBB,J60/2/3,J300/2",
"AAA4BBB,J60/2,J300/2/3",
@@ -1222,6 +1263,24 @@ class TZStrTest(ZoneInfoTestBase):
with self.assertRaisesRegex(ValueError, tzstr_regex):
self.zone_from_tzstr(invalid_tzstr)
def test_invalid_tzstr_non_ascii_abbr(self):
tzstr = "ABÀC3"
if self.module is py_zoneinfo:
expected = re.escape(tzstr)
else:
expected = re.escape(repr(tzstr.encode("utf-8")))
with self.assertRaisesRegex(ValueError, expected):
self.zone_from_tzstr(tzstr, encoding="utf-8")
def test_invalid_tzstr_non_ascii_dst_date(self):
tzstr = "AAA4BBB,J١,J300/2"
if self.module is py_zoneinfo:
expected = re.escape(tzstr)
else:
expected = re.escape(repr(tzstr.encode("utf-8")))
with self.assertRaisesRegex(ValueError, expected):
self.zone_from_tzstr(tzstr, encoding="utf-8")
@classmethod
def _populate_test_cases(cls):
# This method uses a somewhat unusual style in that it populates the
@@ -2055,6 +2114,21 @@ class TestModule(ZoneInfoTestBase):
actual = self.module.available_timezones()
self.assertEqual(actual, expected)
def test_exclude_localtime(self):
expected = {
"America/New_York",
"Europe/London",
}
tree = list(expected) + ["localtime"]
with tempfile.TemporaryDirectory() as td:
for key in tree:
self.touch_zone(key, td)
with self.tzpath_context([td]):
actual = self.module.available_timezones()
self.assertEqual(actual, expected)
class CTestModule(TestModule):
module = c_zoneinfo
@@ -2296,8 +2370,8 @@ class ZoneDumpData:
]
def _America_Santiago():
LMT = ZoneOffset("LMT", timedelta(seconds=-16966), ZERO)
SMT = ZoneOffset("SMT", timedelta(seconds=-16966), ZERO)
LMT = ZoneOffset("LMT", timedelta(seconds=-16965), ZERO)
SMT = ZoneOffset("SMT", timedelta(seconds=-16965), ZERO)
N05 = ZoneOffset("-05", timedelta(seconds=-18000), ZERO)
N04 = ZoneOffset("-04", timedelta(seconds=-14400), ZERO)
N03 = ZoneOffset("-03", timedelta(seconds=-10800), ONE_H)
@@ -2332,8 +2406,8 @@ class ZoneDumpData:
return [
ZoneTransition(datetime(1895, 2, 1), LMT, AEST),
ZoneTransition(datetime(1917, 1, 1, 0, 1), AEST, AEDT),
ZoneTransition(datetime(1917, 3, 25, 2), AEDT, AEST),
ZoneTransition(datetime(1917, 1, 1, 2), AEST, AEDT),
ZoneTransition(datetime(1917, 3, 25, 3), AEDT, AEST),
ZoneTransition(datetime(2012, 4, 1, 3), AEDT, AEST),
ZoneTransition(datetime(2012, 10, 7, 2), AEST, AEDT),
ZoneTransition(datetime(2040, 4, 1, 3), AEDT, AEST),
@@ -2341,7 +2415,7 @@ class ZoneDumpData:
]
def _Europe_Dublin():
LMT = ZoneOffset("LMT", timedelta(seconds=-1500), ZERO)
LMT = ZoneOffset("LMT", timedelta(seconds=-1521), ZERO)
DMT = ZoneOffset("DMT", timedelta(seconds=-1521), ZERO)
IST_0 = ZoneOffset("IST", timedelta(seconds=2079), ONE_H)
GMT_0 = ZoneOffset("GMT", ZERO, ZERO)
+3 -11
View File
@@ -2,29 +2,21 @@
%(heading)s:
%(prog)s: error: %(message)s\n
%(prog)s: warning: %(message)s\n
%r is not callable
'required' is an invalid argument for positionals
.__call__() not defined
ambiguous option: %(option)s could match %(matches)s
argument "-" with mode %r
argument %(argument_name)s: %(message)s
argument '%(argument_name)s' is deprecated
can't open '%(filename)s': %(error)s
cannot have multiple subparser arguments
cannot merge actions - two groups are named %r
command '%(parser_name)s' is deprecated
conflicting subparser alias: %s
conflicting subparser: %s
dest= is required for options like %r
conflicting option string: %s
expected %s argument
expected at least one argument
expected at most one argument
expected one argument
ignored explicit argument %r
invalid %(type)s value: %(value)r
invalid choice: %(value)r (choose from %(choices)s)
invalid conflict_resolution value: %r
invalid option string %(option)r: must start with a character %(prefix_chars)r
mutually exclusive arguments must be optional
invalid choice: %(value)r, maybe you meant %(closest)r? (choose from %(choices)s)
not allowed with argument %s
one of the arguments %s is required
option '%(option)s' is deprecated
+151 -98
View File
@@ -6,35 +6,6 @@ the Python Cookbook, published by O'Reilly.
Library usage: see the Timer class.
Command line usage:
python timeit.py [-n N] [-r N] [-s S] [-p] [-h] [--] [statement]
Options:
-n/--number N: how many times to execute 'statement' (default: see below)
-r/--repeat N: how many times to repeat the timer (default 5)
-s/--setup S: statement to be executed once initially (default 'pass').
Execution time of this setup statement is NOT timed.
-p/--process: use time.process_time() (default is time.perf_counter())
-v/--verbose: print raw timing results; repeat for more digits precision
-u/--unit: set the output time unit (nsec, usec, msec, or sec)
-h/--help: print this usage message and exit
--: separate options from statement, use when statement starts with -
statement: statement to be timed (default 'pass')
A multi-line statement may be given by specifying each line as a
separate argument; indented lines are possible by enclosing an
argument in quotes and using leading spaces. Multiple -s options are
treated similarly.
If -n is not given, a suitable number of loops is calculated by trying
increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the
total time is at least 0.2 seconds.
Note: there is a certain baseline overhead associated with executing a
pass statement. It differs between versions. The code here doesn't try
to hide it, but you should be aware of it. The baseline overhead can be
measured by invoking the program without arguments.
Classes:
Timer
@@ -57,6 +28,7 @@ dummy_src_name = "<timeit-src>"
default_number = 1000000
default_repeat = 5
default_timer = time.perf_counter
default_target_time = 0.2
_globals = globals
@@ -133,7 +105,7 @@ class Timer:
exec(code, global_ns, local_ns)
self.inner = local_ns["inner"]
def print_exc(self, file=None):
def print_exc(self, file=None, **kwargs):
"""Helper to print a traceback from the timed code.
Typical use:
@@ -149,6 +121,11 @@ class Timer:
The optional file argument directs where the traceback is
sent; it defaults to sys.stderr.
The optional colorize keyword argument controls whether the
traceback is colorized; it defaults to False for programmatic
usage. When used from the command line, this is automatically
set based on terminal capabilities.
"""
import linecache, traceback
if self.src is not None:
@@ -158,7 +135,8 @@ class Timer:
dummy_src_name)
# else the source is already stored somewhere else
traceback.print_exc(file=file)
kwargs['colorize'] = kwargs.get('colorize', False)
traceback.print_exc(file=file, **kwargs)
def timeit(self, number=default_number):
"""Time 'number' executions of the main statement.
@@ -206,12 +184,13 @@ class Timer:
r.append(t)
return r
def autorange(self, callback=None):
"""Return the number of loops and time taken so that total time >= 0.2.
def autorange(self, callback=None, target_time=default_target_time):
"""Return the number of loops and time taken so that
total time >= target_time (default is 0.2 seconds).
Calls the timeit method with increasing numbers from the sequence
1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2
second. Returns (number, time_taken).
1, 2, 5, 10, 20, 50, ... until the target time is reached.
Returns (number, time_taken).
If *callback* is given and is not None, it will be called after
each trial with two arguments: ``callback(number, time_taken)``.
@@ -223,7 +202,7 @@ class Timer:
time_taken = self.timeit(number)
if callback:
callback(number, time_taken)
if time_taken >= 0.2:
if time_taken >= target_time:
return (number, time_taken)
i *= 10
@@ -257,53 +236,114 @@ def main(args=None, *, _wrap_timer=None):
is not None, it must be a callable that accepts a timer function
and returns another timer function (used for unit testing).
"""
import argparse
if args is None:
args = sys.argv[1:]
import getopt
try:
opts, args = getopt.getopt(args, "n:u:s:r:pvh",
["number=", "setup=", "repeat=",
"process", "verbose", "unit=", "help"])
except getopt.error as err:
print(err)
print("use -h/--help for command line help")
return 2
import _colorize
colorize = _colorize.can_colorize()
theme = _colorize.get_theme(force_color=colorize).timeit
reset = theme.reset
timer = default_timer
stmt = "\n".join(args) or "pass"
number = 0 # auto-determine
setup = []
repeat = default_repeat
verbose = 0
time_unit = None
epilog = """\
A multi-line statement may be given by specifying each line as a
separate argument; indented lines are possible by enclosing an
argument in quotes and using leading spaces. Multiple `-s` options are
treated similarly.
If `-n` is not given, a suitable number of loops is calculated by trying
increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the
total time is at least `--target-time` seconds.
Note: there is a certain baseline overhead associated with executing a
pass statement. It differs between versions. The code here doesn't try
to hide it, but you should be aware of it. The baseline overhead can be
measured by invoking the program without arguments."""
parser = argparse.ArgumentParser(
prog="python -m timeit",
description="""\
Tool for measuring execution time of small code snippets.
This module avoids a number of common traps for measuring execution
times. See also Tim Peters' introduction to the Algorithms chapter in
the Python Cookbook, published by O'Reilly.
Library usage: see the `Timer` class.""",
epilog=epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-n",
"--number",
type=int,
default=0,
help="how many times to execute 'statement' (default: see below)",
)
parser.add_argument(
"-r",
"--repeat",
type=int,
default=default_repeat,
help="how many times to repeat the timer (default %(default)s)",
)
parser.add_argument(
"-s",
"--setup",
action="append",
default=[],
help="statement to be executed once initially. "
"Execution time of this setup statement is NOT timed. "
"(default 'pass')",
)
parser.add_argument(
"-p",
"--process",
action="store_true",
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 "
"at least this many seconds (default %(default)s)",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="print raw timing results; repeat for more digits precision",
)
parser.add_argument(
"-u",
"--unit",
default=None,
choices=["nsec", "usec", "msec", "sec"],
help="set the output time unit",
)
parser.add_argument(
"statement",
nargs="*",
default=["pass"],
help="statement to be timed (default 'pass')",
)
try:
ns = parser.parse_args(args)
except SystemExit as e:
return e.code
timer = time.process_time if ns.process else default_timer
stmt = "\n".join(ns.statement) or "pass"
number = ns.number
target_time = ns.target_time
setup = "\n".join(ns.setup) or "pass"
repeat = max(ns.repeat, 1)
verbose = ns.verbose
time_unit = ns.unit
units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0}
precision = 3
for o, a in opts:
if o in ("-n", "--number"):
number = int(a)
if o in ("-s", "--setup"):
setup.append(a)
if o in ("-u", "--unit"):
if a in units:
time_unit = a
else:
print("Unrecognized unit. Please select nsec, usec, msec, or sec.",
file=sys.stderr)
return 2
if o in ("-r", "--repeat"):
repeat = int(a)
if repeat <= 0:
repeat = 1
if o in ("-p", "--process"):
timer = time.process_time
if o in ("-v", "--verbose"):
if verbose:
precision += 1
verbose += 1
if o in ("-h", "--help"):
print(__doc__, end="")
return 0
setup = "\n".join(setup) or "pass"
precision = 3 + max(verbose - 1, 0)
# Include the current directory, so that local imports work (sys.path
# contains the directory of this script, rather than the current
@@ -315,18 +355,21 @@ def main(args=None, *, _wrap_timer=None):
t = Timer(stmt, setup, timer)
if number == 0:
# determine number so that 0.2 <= total time < 2.0
# determine number so that total time >= target_time
callback = None
if verbose:
def callback(number, time_taken):
msg = "{num} loop{s} -> {secs:.{prec}g} sec"
plural = (number != 1)
print(msg.format(num=number, s='s' if plural else '',
secs=time_taken, prec=precision))
s = "" if number == 1 else "s"
print(
f"{number} loop{s} "
f"{theme.punctuation}-> "
f"{theme.timing}{time_taken:.{precision}g} sec{reset}"
)
try:
number, _ = t.autorange(callback)
number, _ = t.autorange(callback, target_time)
except:
t.print_exc()
t.print_exc(colorize=colorize)
return 1
if verbose:
@@ -335,7 +378,7 @@ def main(args=None, *, _wrap_timer=None):
try:
raw_timings = t.repeat(repeat, number)
except:
t.print_exc()
t.print_exc(colorize=colorize)
return 1
def format_time(dt):
@@ -353,24 +396,34 @@ def main(args=None, *, _wrap_timer=None):
return "%.*g %s" % (precision, dt / scale, unit)
if verbose:
print("raw times: %s" % ", ".join(map(format_time, raw_timings)))
raw = f"{theme.punctuation}, ".join(
f"{theme.timing}{t}" for t in map(format_time, raw_timings)
)
print(f"raw times: {raw}{reset}")
print()
timings = [dt / number for dt in raw_timings]
best = min(timings)
print("%d loop%s, best of %d: %s per loop"
% (number, 's' if number != 1 else '',
repeat, format_time(best)))
best = min(timings)
worst = max(timings)
s = "" if number == 1 else "s"
print(
f"{number} loop{s}, best of {repeat}: "
f"{theme.best}{format_time(best)}{reset} "
f"{theme.per_loop}per loop{reset}"
)
if worst >= best * 4:
import warnings
warnings.warn_explicit("The test results are likely unreliable. "
"The worst time (%s) was more than four times "
"slower than the best time (%s)."
% (format_time(worst), format_time(best)),
UserWarning, '', 0)
print(file=sys.stderr)
warnings.warn_explicit(
f"{theme.warning}The test results are likely unreliable. "
f"The {theme.warning_worst}worst time ({format_time(worst)})"
f"{theme.warning} was more than four times slower than the "
f"{theme.warning_best}best time ({format_time(best)})"
f"{theme.warning}.{reset}",
UserWarning, "", 0,
)
return None
+38 -27
View File
@@ -4,8 +4,11 @@
from __future__ import annotations
# Defer loading regular expressions until we actually need them in
# parse_value().
__lazy_modules__ = ["tomllib._re"]
import sys
from types import MappingProxyType
from ._re import (
RE_DATETIME,
@@ -16,6 +19,9 @@ from ._re import (
match_to_number,
)
if sys.version_info < (3, 15):
from types import MappingProxyType as frozendict
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterable
@@ -30,35 +36,36 @@ if TYPE_CHECKING:
# and RecursionError.
MAX_KEY_PARTS: Final = sys.getrecursionlimit()
ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
ASCII_CTRL: Final = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
# Neither of these sets include quotation mark or backslash. They are
# currently handled as separate cases in the parser functions.
ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t")
ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n")
ILLEGAL_BASIC_STR_CHARS: Final = ASCII_CTRL - frozenset("\t")
ILLEGAL_MULTILINE_BASIC_STR_CHARS: Final = ASCII_CTRL - frozenset("\t\n")
ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
ILLEGAL_LITERAL_STR_CHARS: Final = ILLEGAL_BASIC_STR_CHARS
ILLEGAL_MULTILINE_LITERAL_STR_CHARS: Final = ILLEGAL_MULTILINE_BASIC_STR_CHARS
ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
ILLEGAL_COMMENT_CHARS: Final = ILLEGAL_BASIC_STR_CHARS
TOML_WS = frozenset(" \t")
TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n")
BARE_KEY_CHARS = frozenset(
TOML_WS: Final = frozenset(" \t")
TOML_WS_AND_NEWLINE: Final = TOML_WS | frozenset("\n")
BARE_KEY_CHARS: Final = frozenset(
"abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "-_"
)
KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
HEXDIGIT_CHARS = frozenset("abcdef" "ABCDEF" "0123456789")
KEY_INITIAL_CHARS: Final = BARE_KEY_CHARS | frozenset("\"'")
HEXDIGIT_CHARS: Final = frozenset("abcdef" "ABCDEF" "0123456789")
BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
BASIC_STR_ESCAPE_REPLACEMENTS: Final = frozendict(
{
"\\b": "\u0008", # backspace
"\\t": "\u0009", # tab
"\\n": "\u000A", # linefeed
"\\f": "\u000C", # form feed
"\\r": "\u000D", # carriage return
"\\n": "\u000a", # linefeed
"\\f": "\u000c", # form feed
"\\r": "\u000d", # carriage return
"\\e": "\u001b", # escape
'\\"': "\u0022", # quote
"\\\\": "\u005C", # backslash
"\\\\": "\u005c", # backslash
}
)
@@ -141,7 +148,7 @@ def load(fp: IO[bytes], /, *, parse_float: ParseFloat = float) -> dict[str, Any]
return loads(s, parse_float=parse_float)
def loads(s: str, /, *, parse_float: ParseFloat = float) -> dict[str, Any]: # noqa: C901
def loads(s: str, /, *, parse_float: ParseFloat = float) -> dict[str, Any]:
"""Parse TOML from a string."""
# The spec allows converting "\r\n" to "\n", even in string
@@ -216,10 +223,10 @@ class Flags:
"""Flags that map to parsed keys/namespaces."""
# Marks an immutable namespace (inline array or inline table).
FROZEN = 0
FROZEN: Final = 0
# Marks a nest that has been explicitly created and can no longer
# be opened using the "[table]" syntax.
EXPLICIT_NEST = 1
EXPLICIT_NEST: Final = 1
def __init__(self) -> None:
self._flags: dict[str, dict[Any, Any]] = {}
@@ -265,8 +272,8 @@ class Flags:
cont = inner_cont["nested"]
key_stem = key[-1]
if key_stem in cont:
cont = cont[key_stem]
return flag in cont["flags"] or flag in cont["recursive_flags"]
inner_cont = cont[key_stem]
return flag in inner_cont["flags"] or flag in inner_cont["recursive_flags"]
return False
@@ -527,7 +534,7 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos
nested_dict = NestedDict()
flags = Flags()
pos = skip_chars(src, pos, TOML_WS)
pos = skip_comments_and_array_ws(src, pos)
if src.startswith("}", pos):
return pos + 1, nested_dict.dict
while True:
@@ -542,16 +549,18 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos
if key_stem in nest:
raise TOMLDecodeError(f"Duplicate inline table key {key_stem!r}", src, pos)
nest[key_stem] = value
pos = skip_chars(src, pos, TOML_WS)
pos = skip_comments_and_array_ws(src, pos)
c = src[pos : pos + 1]
if c == "}":
return pos + 1, nested_dict.dict
if c != ",":
raise TOMLDecodeError("Unclosed inline table", src, pos)
pos += 1
pos = skip_comments_and_array_ws(src, pos)
if src.startswith("}", pos):
return pos + 1, nested_dict.dict
if isinstance(value, (dict, list)):
flags.set(key, Flags.FROZEN, recursive=True)
pos += 1
pos = skip_chars(src, pos, TOML_WS)
def parse_basic_str_escape(
@@ -573,6 +582,8 @@ def parse_basic_str_escape(
pos += 1
pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
return pos, ""
if escape_id == "\\x":
return parse_hex_char(src, pos, 2)
if escape_id == "\\u":
return parse_hex_char(src, pos, 4)
if escape_id == "\\U":
@@ -672,7 +683,7 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
pos += 1
def parse_value( # noqa: C901
def parse_value(
src: str, pos: Pos, parse_float: ParseFloat
) -> tuple[Pos, Any]:
try:
+16 -10
View File
@@ -10,16 +10,20 @@ import re
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
from typing import Any, Final
from ._types import ParseFloat
# E.g.
# - 00:32:00.999999
# - 00:32:00
_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?"
_TIME_RE_STR: Final = r"""
([01][0-9]|2[0-3]) # hours
:([0-5][0-9]) # minutes
(?:
:([0-5][0-9]) # optional seconds
(?:\.([0-9]{1,6})[0-9]*)? # optional fractions of a second
)?
"""
RE_NUMBER = re.compile(
RE_NUMBER: Final = re.compile(
r"""
0
(?:
@@ -38,8 +42,8 @@ RE_NUMBER = re.compile(
""",
flags=re.VERBOSE,
)
RE_LOCALTIME = re.compile(_TIME_RE_STR)
RE_DATETIME = re.compile(
RE_LOCALTIME: Final = re.compile(_TIME_RE_STR, flags=re.VERBOSE)
RE_DATETIME: Final = re.compile(
rf"""
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
(?:
@@ -74,7 +78,8 @@ def match_to_datetime(match: re.Match[str]) -> datetime | date:
year, month, day = int(year_str), int(month_str), int(day_str)
if hour_str is None:
return date(year, month, day)
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
hour, minute = int(hour_str), int(minute_str)
sec = int(sec_str) if sec_str else 0
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
if offset_sign_str:
tz: tzinfo | None = cached_tz(
@@ -103,8 +108,9 @@ def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
def match_to_localtime(match: re.Match[str]) -> time:
hour_str, minute_str, sec_str, micros_str = match.groups()
sec = int(sec_str) if sec_str else 0
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
return time(int(hour_str), int(minute_str), int(sec_str), micros)
return time(int(hour_str), int(minute_str), sec, micros)
def match_to_number(match: re.Match[str], parse_float: ParseFloat) -> Any:
+4
View File
@@ -177,6 +177,10 @@ def available_timezones():
# posixrules is a special symlink-only time zone where it exists, it
# should not be included in the output
valid_zones.remove("posixrules")
if "localtime" in valid_zones:
# localtime is a special symlink-only time zone where it exists, it
# should not be included in the output
valid_zones.remove("localtime")
return valid_zones
+7 -4
View File
@@ -640,11 +640,11 @@ def _parse_tz_str(tz_str):
parser_re = re.compile(
r"""
(?P<std>[^<0-9:.+-]+|<[a-zA-Z0-9+-]+>)
(?P<std>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)
(?:
(?P<stdoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)
(?:
(?P<dst>[^0-9:.+-]+|<[a-zA-Z0-9+-]+>)
(?P<dst>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)
(?P<dstoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)?
)? # dst
)? # stdoff
@@ -672,7 +672,8 @@ def _parse_tz_str(tz_str):
except ValueError as e:
raise ValueError(f"Invalid STD offset in {tz_str}") from e
else:
std_offset = 0
# The STD offset is required
raise ValueError(f"Invalid STD offset in {tz_str}")
if dst_abbr is not None:
if dst_offset := m.group("dstoff"):
@@ -707,7 +708,7 @@ def _parse_dst_start_end(dststr):
type = date[:1]
if type == "M":
n_is_julian = False
m = re.fullmatch(r"M(\d{1,2})\.(\d).(\d)", date, re.ASCII)
m = re.fullmatch(r"M(\d{1,2})\.(\d)\.(\d)", date, re.ASCII)
if m is None:
raise ValueError(f"Invalid dst start/end date: {dststr}")
date_offset = tuple(map(int, m.groups()))
@@ -719,6 +720,8 @@ def _parse_dst_start_end(dststr):
else:
n_is_julian = False
if re.fullmatch(r"\d{1,3}", date, re.ASCII) is None:
raise ValueError(f"Invalid dst start/end date: {dststr}")
doy = int(date)
offset = _DayOffset(doy, n_is_julian)
+1 -2
View File
@@ -645,8 +645,7 @@ fn call_show_warning(
module: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let Some(show_fn) = get_warnings_attr(vm, identifier!(vm, _showwarnmsg), source.is_some())?
else {
let Some(show_fn) = get_warnings_attr(vm, identifier!(vm, _showwarnmsg), true)? else {
show_warning(&filename, lineno, text, &category, source_line, vm);
return Ok(());
};
+9 -3
View File
@@ -316,9 +316,15 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> u32 {
fn pymain_run_python(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<u32> {
let scope = vm.new_scope_with_main()?;
// Initialize warnings module to process sys.warnoptions
// _PyWarnings_Init()
if vm.import("warnings", 0).is_err() {
// The native warnings state already holds the default filters. Import the
// Python module only when startup options require processing (-W, -b, dev).
let warnoptions = vm.sys_module.dict().get_item_opt("warnoptions", vm)?;
if warnoptions
.as_ref()
.and_then(|options| options.downcast_ref::<rustpython_vm::builtins::PyList>())
.is_some_and(|options| options.__len__() > 0)
&& vm.import("warnings", 0).is_err()
{
warn!("Failed to import warnings module");
}