[git-cl] Lint and clean-up git-cl, test, and related modules
In this CL:
- Clarify some comments.
- Remove some unused imports.
- Make some style more consistent (e.g. quotes, whitespace)
Tools used: pyflakes, flake8 (most warnings ignored)
Change-Id: Ibfb6733c8d844b3c75a7f50b4f3c1d43afabb0ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1773856
Reviewed-by: Andrii Shyshkalov <tandrii@google.com>
Commit-Queue: Quinten Yearsley <qyearsley@chromium.org>
diff --git a/git_cl.py b/git_cl.py
index f8b1596..ba5c54f 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -15,7 +15,6 @@
import collections
import contextlib
import datetime
-import fnmatch
import httplib
import itertools
import json
@@ -42,11 +41,9 @@
import auth
import clang_format
import dart_format
-import setup_color
import fix_encoding
import gclient_utils
import gerrit_util
-import git_cache
import git_common
import git_footers
import metrics
@@ -55,6 +52,7 @@
import owners_finder
import presubmit_support
import scm
+import setup_color
import split_cl
import subcommand
import subprocess2
@@ -112,10 +110,10 @@
# File name for yapf style config files.
YAPF_CONFIG_FILENAME = '.style.yapf'
-# Buildbucket master name prefix.
+# Buildbucket master name prefix for Buildbot masters.
MASTER_PREFIX = 'master.'
-# Shortcut since it quickly becomes redundant.
+# Shortcut since it quickly becomes repetitive.
Fore = colorama.Fore
# Initialized in main()
@@ -194,7 +192,7 @@
prefix = 'git version '
version = RunGit(['--version']).strip()
return (version.startswith(prefix) and
- LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
+ LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
def BranchExists(branch):
@@ -276,7 +274,7 @@
args = ['config']
if value_type == bool:
args.append('--bool')
- # git config also has --int, but apparently git config suffers from integer
+ # `git config` also has --int, but apparently git config suffers from integer
# overflows (http://crbug.com/640115), so don't use it.
args.append(_git_branch_config_key(branch, key))
code, out = RunGitWithCode(args)
@@ -307,8 +305,8 @@
args.append('--bool')
value = str(value).lower()
else:
- # git config also has --int, but apparently git config suffers from integer
- # overflows (http://crbug.com/640115), so don't use it.
+ # `git config` also has --int, but apparently git config suffers from
+ # integer overflows (http://crbug.com/640115), so don't use it.
value = str(value)
args.append(_git_branch_config_key(branch, key))
if value is not None:
@@ -321,7 +319,7 @@
Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
"""
- # Git also stores timezone offset, but it only affects visual display,
+ # Git also stores timezone offset, but it only affects visual display;
# actual point in time is defined by this timestamp only.
return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
@@ -395,9 +393,9 @@
if response.status == 200:
if content_json is None:
raise BuildbucketResponseException(
- 'Buildbucket returns invalid json content: %s.\n'
+ 'Buildbucket returned invalid JSON content: %s.\n'
'Please file bugs at http://crbug.com, '
- 'component "Infra>Platform>BuildBucket".' %
+ 'component "Infra>Platform>Buildbucket".' %
content)
return content_json
if response.status < 500 or try_count >= 2:
@@ -405,14 +403,14 @@
# status >= 500 means transient failures.
logging.debug('Transient errors when %s. Will retry.', operation_name)
- time_sleep(0.5 + 1.5*try_count)
+ time_sleep(0.5 + (1.5 * try_count))
try_count += 1
assert False, 'unreachable'
def _get_bucket_map(changelist, options, option_parser):
"""Returns a dict mapping bucket names to builders and tests,
- for triggering try jobs.
+ for triggering tryjobs.
"""
# If no bots are listed, we try to get a set of builders and tests based
# on GetPreferredTryMasters functions in PRESUBMIT.py files.
@@ -443,11 +441,11 @@
def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
- """Sends a request to Buildbucket to trigger try jobs for a changelist.
+ """Sends a request to Buildbucket to trigger tryjobs for a changelist.
Args:
auth_config: AuthConfig for Buildbucket.
- changelist: Changelist that the try jobs are associated with.
+ changelist: Changelist that the tryjobs are associated with.
buckets: A nested dict mapping bucket names to builders to tests.
options: Command-line options.
"""
@@ -522,7 +520,7 @@
)
_buildbucket_retry(
- 'triggering try jobs',
+ 'triggering tryjobs',
http,
buildbucket_put_url,
'PUT',
@@ -536,7 +534,7 @@
def fetch_try_jobs(auth_config, changelist, buildbucket_host,
patchset=None):
- """Fetches try jobs from buildbucket.
+ """Fetches tryjobs from buildbucket.
Returns a map from build id to build info as a dictionary.
"""
@@ -570,7 +568,7 @@
url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
hostname=buildbucket_host,
params=urllib.urlencode(params))
- content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
+ content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
for build in content.get('builds', []):
builds[build['id']] = build
if 'next_cursor' in content:
@@ -583,7 +581,7 @@
def print_try_jobs(options, builds):
"""Prints nicely result of fetch_try_jobs."""
if not builds:
- print('No try jobs scheduled.')
+ print('No tryjobs scheduled.')
return
# Make a copy, because we'll be modifying builds dictionary.
@@ -676,7 +674,7 @@
pop(title='Other:',
f=lambda b: (get_name(b), 'id=%s' % b['id']))
assert len(builds) == 0
- print('Total: %d try jobs' % total)
+ print('Total: %d tryjobs' % total)
def _ComputeDiffLineRanges(files, upstream_commit):
@@ -734,8 +732,8 @@
"""Checks if a yapf file is in any parent directory of fpath until top_dir.
Recursively checks parent directories to find yapf file and if no yapf file
- is found returns None. Uses yapf_config_cache as a cache for
- previously found configs.
+ is found returns None. Uses yapf_config_cache as a cache for previously found
+ configs.
"""
fpath = os.path.abspath(fpath)
# Return result if we've already computed it.
@@ -873,14 +871,14 @@
return self._GetConfig('rietveld.cc', error_ok=True)
def GetIsGerrit(self):
- """Return true if this repo is associated with gerrit code review system."""
+ """Returns True if this repo is associated with Gerrit."""
if self.is_gerrit is None:
self.is_gerrit = (
self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
return self.is_gerrit
def GetSquashGerritUploads(self):
- """Return true if uploads to Gerrit should be squashed by default."""
+ """Returns True if uploads to Gerrit should be squashed by default."""
if self.squash_gerrit_uploads is None:
self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
if self.squash_gerrit_uploads is None:
@@ -914,7 +912,7 @@
return self.gerrit_skip_ensure_authenticated
def GetGitEditor(self):
- """Return the editor specified in the git config, or None if none is."""
+ """Returns the editor specified in the git config, or None if none is."""
if self.git_editor is None:
# Git requires single quotes for paths with spaces. We need to replace
# them with double quotes for Windows to treat such paths as a single
@@ -1357,8 +1355,8 @@
self._cached_remote_url = (True, url)
return url
- # If it cannot be parsed as an url, assume it is a local directory, probably
- # a git cache.
+ # If it cannot be parsed as an url, assume it is a local directory,
+ # probably a git cache.
logging.warning('"%s" doesn\'t appear to point to a git host. '
'Interpreting it as a local directory.', url)
if not os.path.isdir(url):
@@ -1428,7 +1426,7 @@
raw_description = self.GetDescription()
msg_lines, _, footers = git_footers.split_footers(raw_description)
if footers:
- msg_lines = msg_lines[:len(msg_lines)-1]
+ msg_lines = msg_lines[:len(msg_lines) - 1]
return msg_lines, footers
def GetPatchset(self):
@@ -1601,10 +1599,9 @@
base_branch = self.GetCommonAncestorWithUpstream()
git_diff_args = [base_branch, 'HEAD']
-
- # Fast best-effort checks to abort before running potentially
- # expensive hooks if uploading is likely to fail anyway. Passing these
- # checks does not guarantee that uploading will not fail.
+ # Fast best-effort checks to abort before running potentially expensive
+ # hooks if uploading is likely to fail anyway. Passing these checks does
+ # not guarantee that uploading will not fail.
self._codereview_impl.EnsureAuthenticated(force=options.force)
self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
@@ -1718,11 +1715,11 @@
return self._codereview_impl.GetMostRecentPatchset()
def CannotTriggerTryJobReason(self):
- """Returns reason (str) if unable trigger try jobs on this CL or None."""
+ """Returns reason (str) if unable trigger tryjobs on this CL or None."""
return self._codereview_impl.CannotTriggerTryJobReason()
def GetTryJobProperties(self, patchset=None):
- """Returns dictionary of properties to launch try job."""
+ """Returns dictionary of properties to launch tryjob."""
return self._codereview_impl.GetTryJobProperties(patchset=patchset)
def __getattr__(self, attr):
@@ -1861,7 +1858,7 @@
raise NotImplementedError()
def CannotTriggerTryJobReason(self):
- """Returns reason (str) if unable trigger try jobs on this CL or None."""
+ """Returns reason (str) if unable trigger tryjobs on this CL or None."""
raise NotImplementedError()
def GetIssueOwner(self):
@@ -1899,7 +1896,7 @@
parsed = urlparse.urlparse(self.GetRemoteUrl())
if parsed.scheme == 'sso':
print('WARNING: using non-https URLs for remote is likely broken\n'
- ' Your current remote is: %s' % self.GetRemoteUrl())
+ ' Your current remote is: %s' % self.GetRemoteUrl())
self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
self._gerrit_server = 'https://%s' % self._gerrit_host
return self._gerrit_host
@@ -2075,7 +2072,7 @@
return presubmit_support.GerritAccessor(self._GetGerritHost())
def GetStatus(self):
- """Apply a rough heuristic to give a simple summary of an issue's review
+ """Applies a rough heuristic to give a simple summary of an issue's review
or CQ status, assuming adherence to a common workflow.
Returns None if no issue for this branch, or one of the following keywords:
@@ -2327,7 +2324,7 @@
def _IsCqConfigured(self):
detail = self._GetChangeDetail(['LABELS'])
- if not u'Commit-Queue' in detail.get('labels', {}):
+ if u'Commit-Queue' not in detail.get('labels', {}):
return False
# TODO(crbug/753213): Remove temporary hack
if ('https://chromium.googlesource.com/chromium/src' ==
@@ -2482,8 +2479,8 @@
hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
if not os.path.exists(hook):
return
- # Crude attempt to distinguish Gerrit Codereview hook from potentially
- # custom developer made one.
+ # Crude attempt to distinguish Gerrit Codereview hook from a potentially
+ # custom developer-made one.
data = gclient_utils.FileRead(hook)
if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
return
@@ -2978,9 +2975,9 @@
def SetCQState(self, new_state):
"""Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
vote_map = {
- _CQState.NONE: 0,
- _CQState.DRY_RUN: 1,
- _CQState.COMMIT: 2,
+ _CQState.NONE: 0,
+ _CQState.DRY_RUN: 1,
+ _CQState.COMMIT: 2,
}
labels = {'Commit-Queue': vote_map[new_state]}
notify = False if new_state == _CQState.DRY_RUN else None
@@ -2998,7 +2995,7 @@
return 'CL %s is closed' % self.GetIssue()
def GetTryJobProperties(self, patchset=None):
- """Returns dictionary of properties to launch try job."""
+ """Returns dictionary of properties to launch a tryjob."""
data = self._GetChangeDetail(['ALL_REVISIONS'])
patchset = int(patchset or self.GetPatchset())
assert patchset
@@ -3420,7 +3417,7 @@
def LoadCodereviewSettingsFromFile(fileobj):
- """Parse a codereview.settings file and updates hooks."""
+ """Parses a codereview.settings file and updates hooks."""
keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
def SetProperty(name, setting, unset_error_ok=False):
@@ -3463,8 +3460,10 @@
def urlretrieve(source, destination):
- """urllib is broken for SSL connections via a proxy therefore we
- can't use urllib.urlretrieve()."""
+ """Downloads a network object to a local file, like urllib.urlretrieve.
+
+ This is necessary because urllib is broken for SSL connections via a proxy.
+ """
with open(destination, 'w') as f:
f.write(urllib2.urlopen(source).read())
@@ -3481,7 +3480,7 @@
def DownloadGerritHook(force):
- """Download and install Gerrit commit-msg hook.
+ """Downloads and installs a Gerrit commit-msg hook.
Args:
force: True to update hooks. False to install hooks if not present.
@@ -3769,7 +3768,7 @@
found = True
print('\n\n.gitcookies problem report:\n')
bad_hosts.update(hosts or [])
- print(' %s%s' % (title , (':' if sublines else '')))
+ print(' %s%s' % (title, (':' if sublines else '')))
if sublines:
print()
print(' %s' % '\n '.join(sublines))
@@ -3833,6 +3832,7 @@
return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
error_ok=False).strip()
+
def color_for_status(status):
"""Maps a Changelist status to color, for CMDstatus and other tools."""
return {
@@ -3910,18 +3910,19 @@
"""Uploads CLs of local branches that are dependents of the current branch.
If the local branch dependency tree looks like:
- test1 -> test2.1 -> test3.1
- -> test3.2
- -> test2.2 -> test3.3
+
+ test1 -> test2.1 -> test3.1
+ -> test3.2
+ -> test2.2 -> test3.3
and you run "git cl upload --dependencies" from test1 then "git cl upload" is
run on the dependent branches in this order:
test2.1, test3.1, test3.2, test2.2, test3.3
- Note: This function does not rebase your local dependent branches. Use it when
- you make a change to the parent branch that will not conflict with its
- dependent branches, and you would like their dependencies updated in
- Rietveld.
+ Note: This function does not rebase your local dependent branches. Use it
+ when you make a change to the parent branch that will not conflict
+ with its dependent branches, and you would like their dependencies
+ updated in Rietveld.
"""
if git_common.is_dirty_git_tree('upload-branch-deps'):
return 1
@@ -3941,8 +3942,8 @@
print('No local branches found.')
return 0
- # Create a dictionary of all local branches to the branches that are dependent
- # on it.
+ # Create a dictionary of all local branches to the branches that are
+ # dependent on it.
tracked_to_dependents = collections.defaultdict(list)
for b in branches.splitlines():
tokens = b.split()
@@ -3953,6 +3954,7 @@
print()
print('The dependent local branches of %s are:' % root_branch)
dependents = []
+
def traverse_dependents_preorder(branch, padding=''):
dependents_to_process = tracked_to_dependents.get(branch, [])
padding += ' '
@@ -3960,6 +3962,7 @@
print('%s%s' % (padding, dependent))
dependents.append(dependent)
traverse_dependents_preorder(dependent, padding)
+
traverse_dependents_preorder(root_branch)
print()
@@ -4557,14 +4560,14 @@
def GenerateGerritChangeId(message):
- """Returns Ixxxxxx...xxx change id.
+ """Returns the Change ID footer value (Ixxxxxx...xxx).
Works the same way as
https://gerrit-review.googlesource.com/tools/hooks/commit-msg
but can be called on demand on all platforms.
- The basic idea is to generate git hash of a state of the tree, original commit
- message, author/committer info and timestamps.
+ The basic idea is to generate git hash of a state of the tree, original
+ commit message, author/committer info and timestamps.
"""
lines = []
tree_hash = RunGitSilent(['write-tree'])
@@ -4656,16 +4659,16 @@
Can skip dependency patchset uploads for a branch by running:
git config branch.branch_name.skip-deps-uploads True
- To unset run:
+ To unset, run:
git config --unset branch.branch_name.skip-deps-uploads
Can also set the above globally by using the --global flag.
- If the name of the checked out branch starts with "bug-" or "fix-" followed by
- a bug number, this bug number is automatically populated in the CL
+ If the name of the checked out branch starts with "bug-" or "fix-" followed
+ by a bug number, this bug number is automatically populated in the CL
description.
If subject contains text in square brackets or has "<text>: " prefix, such
- text(s) is treated as Gerrit hashtags. For example, CLs with subjects
+ text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
[git-cl] add support for hashtags
Foo bar: implement foo
will be hashtagged with "git-cl" and "foo-bar" respectively.
@@ -4797,22 +4800,22 @@
comment, the string '$directory', is replaced with the directory containing
the shared OWNERS file.
"""
- parser.add_option("-d", "--description", dest="description_file",
- help="A text file containing a CL description in which "
- "$directory will be replaced by each CL's directory.")
- parser.add_option("-c", "--comment", dest="comment_file",
- help="A text file containing a CL comment.")
- parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
+ parser.add_option('-d', '--description', dest='description_file',
+ help='A text file containing a CL description in which '
+ '$directory will be replaced by each CL\'s directory.')
+ parser.add_option('-c', '--comment', dest='comment_file',
+ help='A text file containing a CL comment.')
+ parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
default=False,
- help="List the files and reviewers for each CL that would "
- "be created, but don't create branches or CLs.")
- parser.add_option("--cq-dry-run", action='store_true',
- help="If set, will do a cq dry run for each uploaded CL. "
- "Please be careful when doing this; more than ~10 CLs "
- "has the potential to overload our build "
- "infrastructure. Try to upload these not during high "
- "load times (usually 11-3 Mountain View time). Email "
- "infra-dev@chromium.org with any questions.")
+ help='List the files and reviewers for each CL that would '
+ 'be created, but don\'t create branches or CLs.')
+ parser.add_option('--cq-dry-run', action='store_true',
+ help='If set, will do a cq dry run for each uploaded CL. '
+ 'Please be careful when doing this; more than ~10 CLs '
+ 'has the potential to overload our build '
+ 'infrastructure. Try to upload these not during high '
+ 'load times (usually 11-3 Mountain View time). Email '
+ 'infra-dev@chromium.org with any questions.')
parser.add_option('-a', '--enable-auto-submit', action='store_true',
default=True,
help='Sends your change to the CQ after an approval. Only '
@@ -4895,7 +4898,6 @@
parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
help='don\'t commit after patch applies. Rietveld only.')
-
group = optparse.OptionGroup(
parser,
'Options for continuing work on the current issue uploaded from a '
@@ -5029,8 +5031,8 @@
@metrics.collector.collect_metrics('git cl try')
def CMDtry(parser, args):
- """Triggers tryjobs using either BuildBucket or CQ dry run."""
- group = optparse.OptionGroup(parser, 'Try job options')
+ """Triggers tryjobs using either Buildbucket or CQ dry run."""
+ group = optparse.OptionGroup(parser, 'Tryjob options')
group.add_option(
'-b', '--bot', action='append',
help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
@@ -5046,7 +5048,7 @@
help=('DEPRECATED, use -B. The try master where to run the builds.'))
group.add_option(
'-r', '--revision',
- help='Revision to use for the try job; default: the revision will '
+ help='Revision to use for the tryjob; default: the revision will '
'be determined by the try recipe that builder runs, which usually '
'defaults to HEAD of origin/master')
group.add_option(
@@ -5065,8 +5067,8 @@
help='Specify generic properties in the form -p key1=value1 -p '
'key2=value2 etc. The value will be treated as '
'json if decodable, or as string otherwise. '
- 'NOTE: using this may make your try job not usable for CQ, '
- 'which will then schedule another try job with default properties')
+ 'NOTE: using this may make your tryjob not usable for CQ, '
+ 'which will then schedule another tryjob with default properties')
group.add_option(
'--buildbucket-host', default='cr-buildbucket.appspot.com',
help='Host of buildbucket. The default host is %default.')
@@ -5099,7 +5101,7 @@
error_message = cl.CannotTriggerTryJobReason()
if error_message:
- parser.error('Can\'t trigger try jobs: %s' % error_message)
+ parser.error('Can\'t trigger tryjobs: %s' % error_message)
if options.bucket and options.master:
parser.error('Only one of --bucket and --master may be used.')
@@ -5134,7 +5136,7 @@
@metrics.collector.collect_metrics('git cl try-results')
def CMDtry_results(parser, args):
"""Prints info about results for tryjobs associated with the current CL."""
- group = optparse.OptionGroup(parser, 'Try job results options')
+ group = optparse.OptionGroup(parser, 'Tryjob results options')
group.add_option(
'-p', '--patchset', type=int, help='patchset number if not current.')
group.add_option(
@@ -5146,7 +5148,7 @@
'--buildbucket-host', default='cr-buildbucket.appspot.com',
help='Host of buildbucket. The default host is %default.')
group.add_option(
- '--json', help=('Path of JSON output file to write try job results to,'
+ '--json', help=('Path of JSON output file to write tryjob results to,'
'or "-" for stdout.'))
parser.add_option_group(group)
auth.add_auth_options(parser)
@@ -5220,8 +5222,8 @@
return 1
# Redirect I/O before invoking browser to hide its output. For example, this
- # allows to hide "Created new window in existing browser session." message
- # from Chrome. Based on https://stackoverflow.com/a/2323563.
+ # allows us to hide the "Created new window in existing browser session."
+ # message from Chrome. Based on https://stackoverflow.com/a/2323563.
saved_stdout = os.dup(1)
saved_stderr = os.dup(2)
os.close(1)
@@ -5638,13 +5640,13 @@
return_value = 2 # Not formatted.
elif opts.diff and gn_ret == 2:
# TODO this should compute and print the actual diff.
- print("This change has GN build file diff for " + gn_diff_file)
+ print('This change has GN build file diff for ' + gn_diff_file)
elif gn_ret != 0:
# For non-dry run cases (and non-2 return values for dry-run), a
# nonzero error code indicates a failure, probably because the file
# doesn't parse.
- DieWithError("gn format failed on " + gn_diff_file +
- "\nTry running 'gn format' on this file manually.")
+ DieWithError('gn format failed on ' + gn_diff_file +
+ '\nTry running `gn format` on this file manually.')
# Skip the metrics formatting from the global presubmit hook. These files have
# a separate presubmit hook that issues an error if the files need formatting,
@@ -5664,13 +5666,15 @@
return return_value
+
def GetDirtyMetricsDirs(diff_files):
xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
metrics_xml_dirs = [
os.path.join('tools', 'metrics', 'actions'),
os.path.join('tools', 'metrics', 'histograms'),
os.path.join('tools', 'metrics', 'rappor'),
- os.path.join('tools', 'metrics', 'ukm')]
+ os.path.join('tools', 'metrics', 'ukm'),
+ ]
for xml_dir in metrics_xml_dirs:
if any(file.startswith(xml_dir) for file in xml_diff_files):
yield xml_dir
@@ -5782,7 +5786,7 @@
def main(argv):
if sys.hexversion < 0x02060000:
- print('\nYour python version %s is unsupported, please upgrade.\n' %
+ print('\nYour Python version %s is unsupported, please upgrade.\n' %
(sys.version.split(' ', 1)[0],), file=sys.stderr)
return 2
@@ -5796,14 +5800,14 @@
if e.code != 500:
raise
DieWithError(
- ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
+ ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
return 0
if __name__ == '__main__':
- # These affect sys.stdout so do it outside of main() to simplify mocks in
- # unit testing.
+ # These affect sys.stdout, so do it outside of main() to simplify mocks in
+ # the unit tests.
fix_encoding.fix_encoding()
setup_color.init()
with metrics.collector.print_notice_and_exit():