mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
c434bfdacd
commit
75e77c30e3
35 files changed
+2286
-649
No files matched your search
Vendored
+235
-50
@@ -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}")
|
||||
Vendored
+137
-66
@@ -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"> </td>' % self.cssclass_noday
|
||||
return f'<td class="{self.cssclass_noday}"> </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)
|
||||
|
||||
Vendored
+223
-29
@@ -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():
|
||||
|
||||
Vendored
+38
-22
@@ -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}")
|
||||
Vendored
+7
-2
@@ -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."""
|
||||
|
||||
Vendored
+613
-26
@@ -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.
|
||||
|
||||
Vendored
+172
-28
@@ -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"> </td><td class="noday"> </td><td class="noday"> </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"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </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"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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"> </td><td class="noday"> </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"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </td><td class="noday"> </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):
|
||||
|
||||
Vendored
+143
-25
@@ -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):
|
||||
|
||||
Vendored
+39
-13
@@ -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()
|
||||
Vendored
+20
-5
@@ -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):
|
||||
|
||||
Vendored
+66
-16
@@ -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()
|
||||
Vendored
+7
-24
@@ -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
|
||||
File renamed without changes.
File renamed without changes.
@@ -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"
|
||||
Vendored
+1
-18
@@ -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)
|
||||
|
||||
Vendored
+19
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Vendored
+151
-98
@@ -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
|
||||
|
||||
|
||||
|
||||
Vendored
+38
-27
@@ -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:
|
||||
|
||||
Vendored
+16
-10
@@ -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:
|
||||
|
||||
Vendored
+4
@@ -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
|
||||
|
||||
|
||||
Vendored
+7
-4
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user