Skip to content

Commit

Permalink
cli: Switched pyinstaller CLI from argparse to click.
Browse files Browse the repository at this point in the history
   
Switched the CLI to improve the user experience.
Fixes issue pyinstaller#5579
  • Loading branch information
Riz committed Feb 20, 2021
1 parent cad1d0f commit cd1001b
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 52 deletions.
36 changes: 20 additions & 16 deletions PyInstaller/__main__.py
Expand Up @@ -18,6 +18,8 @@
import argparse
import platform

import click


from . import __version__
from . import log as logging
Expand Down Expand Up @@ -65,10 +67,11 @@ def run_build(pyi_config, spec_file, **kwargs):
PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)


def __add_options(parser):
parser.add_argument('-v', '--version', action='version',
def __add_options(func):
return click.version_option('-v', '--version',
version=__version__,
help='Show program version info and exit.')
help='Show program version info and exit.')(func)


def run(pyi_args=None, pyi_config=None):
"""
Expand All @@ -82,19 +85,20 @@ def run(pyi_args=None, pyi_config=None):
import PyInstaller.log

try:
parser = argparse.ArgumentParser(formatter_class=_SmartFormatter)
__add_options(parser)
PyInstaller.building.makespec.__add_options(parser)
PyInstaller.building.build_main.__add_options(parser)
PyInstaller.log.__add_options(parser)
parser.add_argument('filenames', metavar='scriptname', nargs='+',
help=("name of scriptfiles to be processed or "
"exactly one .spec-file. If a .spec-file is "
"specified, most options are unnecessary "
"and are ignored."))

args = parser.parse_args(pyi_args)
PyInstaller.log.__process_options(parser, args)
options = PyInstaller.building.makespec.__add_options(run_makespec)
options = __add_options(options)
# carry on from here - wrap run_makespec in all the click.option decorators.
options = PyInstaller.building.build_main.__add_options(options)
options = PyInstaller.log.__add_options(options)
options = click.option('filenames', metavar='scriptname',
required=True,
help=("name of scriptfiles to be processed or"
" exactly one .spec-file. If a .spec-file is"
" specified, most options are unnecessary"
" and are ignored."))(options)

args = click.parse_args(pyi_args)
PyInstaller.log.__process_options(args)

# Print PyInstaller version, Python version and platform
# as the first line to stdout.
Expand Down
54 changes: 32 additions & 22 deletions PyInstaller/building/build_main.py
Expand Up @@ -25,6 +25,8 @@

import pkg_resources

import click


# Relative imports to PyInstaller modules.
from .. import HOMEPATH, DEFAULT_DISTPATH, DEFAULT_WORKPATH
Expand Down Expand Up @@ -671,28 +673,36 @@ def build(spec, distpath, workpath, clean_build):
raise SystemExit('spec "{}" not found'.format(spec))
exec(code, spec_namespace)

def __add_options(parser):
parser.add_argument("--distpath", metavar="DIR",
default=DEFAULT_DISTPATH,
help=('Where to put the bundled app (default: %s)' %
os.path.join(os.curdir, 'dist')))
parser.add_argument('--workpath', default=DEFAULT_WORKPATH,
help=('Where to put all the temporary work files, '
'.log, .pyz and etc. (default: %s)' %
os.path.join(os.curdir, 'build')))
parser.add_argument('-y', '--noconfirm',
action="store_true", default=False,
help='Replace output directory (default: %s) without '
'asking for confirmation' % os.path.join('SPECPATH', 'dist', 'SPECNAME'))
parser.add_argument('--upx-dir', default=None,
help='Path to UPX utility (default: search the execution path)')
parser.add_argument("-a", "--ascii", action="store_true",
help="Do not include unicode encoding support "
"(default: included if available)")
parser.add_argument('--clean', dest='clean_build', action='store_true',
default=False,
help='Clean PyInstaller cache and remove temporary '
'files before building.')

def __add_options(func):
options = [
click.option("--distpath", default=DEFAULT_DISTPATH,
help=(f"Where to put the bundled app"
f" (default:"
f" {os.path.join(os.curdir, 'dist')})")),
click.option('--workpath', default=DEFAULT_WORKPATH,
help=(f"Where to put all the temporary work files,"
f" .log, .pyz and etc. (default: "
f" {os.path.join(os.curdir, 'build')})")),
click.option('-y', '--noconfirm', is_flag=True,
help=f"Replace output directory (default: "
f" {os.path.join('SPECPATH', 'dist', 'SPECNAME')})"
f" without asking for confirmation"),
click.option('--upx-dir', default=None,
help="Path to UPX utility (default: search the"
" execution path)"),
click.option("-a", "--ascii", is_flag=True,
help="Do not include unicode encoding support"
" (default: included if available)"),
click.option('--clean', is_flag=True,
help='Clean PyInstaller cache and remove temporary '
'files before building.')
]
for option in options:
func = option(func)
return func




def main(pyi_config, specfile, noconfirm, ascii=False, **kw):
Expand Down
25 changes: 13 additions & 12 deletions PyInstaller/log.py
@@ -1,4 +1,4 @@
#-----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Copyright (c) 2013-2021, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
Expand All @@ -7,7 +7,7 @@
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
# -----------------------------------------------------------------------------


"""
Expand All @@ -19,6 +19,8 @@
import logging
from logging import getLogger, INFO, WARN, DEBUG, ERROR, FATAL

import click

TRACE = logging.TRACE = DEBUG - 5
logging.addLevelName(TRACE, 'TRACE')

Expand All @@ -27,22 +29,21 @@
logger = getLogger('PyInstaller')


def __add_options(parser):
def __add_options(func):
levels = ('TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL')
parser.add_argument('--log-level',
choices=levels, metavar="LEVEL",
return click.option('--log-level',
type=click.Choice(levels),
default='INFO',
dest='loglevel',
help=('Amount of detail in build-time console messages. '
'LEVEL may be one of %s (default: %%(default)s).'
% ', '.join(levels))
)
help=f"Amount of detail in build-time console "
f"messages. LEVEL may be one of "
f"{', '.join(levels)} (default: %%(default)s)."
)(func)


def __process_options(parser, opts):
def __process_options(opts):
try:
level = getattr(logging, opts.loglevel.upper())
except AttributeError:
parser.error('Unknown log level `%s`' % opts.loglevel)
raise click.ClickException('Unknown log level `%s`' % opts.loglevel)
else:
logger.setLevel(level)
2 changes: 1 addition & 1 deletion PyInstaller/utils/cliutils/archive_viewer.py
Expand Up @@ -259,7 +259,7 @@ def run():
help="pyinstaller archive to show content of")

args = parser.parse_args()
PyInstaller.log.__process_options(parser, args)
PyInstaller.log.__process_options(args)

try:
raise SystemExit(main(**vars(args)))
Expand Down
2 changes: 1 addition & 1 deletion PyInstaller/utils/cliutils/bindepend.py
Expand Up @@ -32,7 +32,7 @@ def run():
"the dependencies should be shown"))

args = parser.parse_args()
PyInstaller.log.__process_options(parser, args)
PyInstaller.log.__process_options(args)

# Suppress all informative messages from the dependency code.
PyInstaller.log.getLogger('PyInstaller.build.bindepend').setLevel(
Expand Down

0 comments on commit cd1001b

Please sign in to comment.