diff --git a/bazarr/main.py b/bazarr/main.py index c16a3288e..2725be325 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -145,12 +145,6 @@ def check_credentials(user, pw): return False -def authorize(): - if login_auth == 'form': - aaa = Cork(os.path.normpath(os.path.join(args.config_dir, 'config'))) - aaa.require(fail_redirect=(base_url + 'login')) - - def api_authorize(): if 'apikey' in request.GET.dict: if request.GET.dict['apikey'][0] == settings.auth.apikey: @@ -198,14 +192,14 @@ def logout(): # @app.route('/') # # @custom_auth_basic(check_credentials) # def redirect_root(): -# authorize() +# # redirect(base_url) @app.route(base_url + 'shutdown/') # @custom_auth_basic(check_credentials) def shutdown(): - authorize() + try: server.stop() except: @@ -225,7 +219,7 @@ def shutdown(): @app.route(base_url + 'restart/') # @custom_auth_basic(check_credentials) def restart(): - authorize() + try: server.stop() except: @@ -246,7 +240,7 @@ def restart(): @app.route(base_url + 'wizard/') # @custom_auth_basic(check_credentials) def wizard(): - authorize() + # Get languages list settings_languages = database.execute("SELECT * FROM table_settings_languages ORDER BY name") @@ -261,7 +255,7 @@ def wizard(): @app.route(base_url + 'save_wizard', methods=['POST']) # @custom_auth_basic(check_credentials) def save_wizard(): - authorize() + settings_general_ip = request.form.get('settings_general_ip') settings_general_port = request.form.get('settings_general_port') @@ -477,7 +471,7 @@ def save_wizard(): @app.route(base_url + 'emptylog') # @custom_auth_basic(check_credentials) def emptylog(): - authorize() + ref = request.environ['HTTP_REFERER'] empty_log() @@ -489,14 +483,14 @@ def emptylog(): @app.route(base_url + 'bazarr.log') # @custom_auth_basic(check_credentials) def download_log(): - authorize() + return static_file('bazarr.log', root=os.path.join(args.config_dir, 'log/'), download='bazarr.log') @app.route(base_url + 'image_proxy/', methods=['GET']) # @custom_auth_basic(check_credentials) def image_proxy(url): - authorize() + apikey = settings.sonarr.apikey url_image = url_sonarr_short() + '/' + url + '?apikey=' + apikey try: @@ -514,7 +508,7 @@ def image_proxy(url): @app.route(base_url + 'image_proxy_movies/', methods=['GET']) # @custom_auth_basic(check_credentials) def image_proxy_movies(url): - authorize() + apikey = settings.radarr.apikey try: url_image = (url_radarr_short() + '/' + url + '?apikey=' + apikey).replace('/fanart.jpg', '/banner.jpg') @@ -534,7 +528,7 @@ def image_proxy_movies(url): @app.route(base_url) # @custom_auth_basic(check_credentials) def redirect_root(): - authorize() + if settings.general.getboolean('use_sonarr'): return redirect(base_url + 'series') elif settings.general.getboolean('use_radarr'): @@ -548,7 +542,7 @@ def redirect_root(): @app.route(base_url + 'series/') # @custom_auth_basic(check_credentials) def series(): - authorize() + series_count = database.execute("SELECT COUNT(*) as count FROM table_shows", only_one=True)['count'] page = request.data @@ -605,7 +599,7 @@ def series(): @app.route(base_url + 'serieseditor/') # @custom_auth_basic(check_credentials) def serieseditor(): - authorize() + # Get missing count missing_count = database.execute("SELECT COUNT(*) as count FROM table_shows", only_one=True)['count'] @@ -627,7 +621,7 @@ def serieseditor(): @app.route(base_url + 'search_json/', methods=['GET']) # @custom_auth_basic(check_credentials) def search_json(query): - authorize() + query = '%' + query + '%' search_list = [] @@ -655,7 +649,7 @@ def search_json(query): @app.route(base_url + 'edit_series/', methods=['POST']) # @custom_auth_basic(check_credentials) def edit_series(no): - authorize() + ref = request.environ['HTTP_REFERER'] lang = request.form.getlist('languages') @@ -693,7 +687,7 @@ def edit_series(no): @app.route(base_url + 'edit_serieseditor', methods=['POST']) # @custom_auth_basic(check_credentials) def edit_serieseditor(): - authorize() + ref = request.environ['HTTP_REFERER'] series = request.form.get('series') @@ -723,7 +717,7 @@ def edit_serieseditor(): @app.route(base_url + 'episodes/', methods=['GET']) # @custom_auth_basic(check_credentials) def episodes(no): - authorize() + series_details = database.execute("SELECT title, overview, poster, fanart, hearing_impaired, tvdbId, " "audio_language, languages, path, forced FROM table_shows WHERE " @@ -755,7 +749,7 @@ def episodes(no): @app.route(base_url + 'movies') # @custom_auth_basic(check_credentials) def movies(): - authorize() + missing_count = database.execute("SELECT COUNT(*) as count FROM table_movies", only_one=True)['count'] page = request.data @@ -782,7 +776,7 @@ def movies(): @app.route(base_url + 'movieseditor') # @custom_auth_basic(check_credentials) def movieseditor(): - authorize() + missing_count = database.execute("SELECT COUNT(*) as count FROM table_movies", only_one=True)['count'] @@ -801,7 +795,7 @@ def movieseditor(): @app.route(base_url + 'edit_movieseditor', methods=['POST']) # @custom_auth_basic(check_credentials) def edit_movieseditor(): - authorize() + ref = request.environ['HTTP_REFERER'] movies = request.form.get('movies') @@ -831,7 +825,7 @@ def edit_movieseditor(): @app.route(base_url + 'edit_movie/', methods=['POST']) # @custom_auth_basic(check_credentials) def edit_movie(no): - authorize() + ref = request.environ['HTTP_REFERER'] lang = request.form.getlist('languages') @@ -869,7 +863,7 @@ def edit_movie(no): @app.route(base_url + 'movie/', methods=['GET']) # @custom_auth_basic(check_credentials) def movie(no): - authorize() + movies_details = database.execute("SELECT title, overview, poster, fanart, hearing_impaired, tmdbId, " "audio_language, languages, path, subtitles, radarrId, missing_subtitles, " @@ -890,7 +884,7 @@ def movie(no): @app.route(base_url + 'scan_disk/', methods=['GET']) # @custom_auth_basic(check_credentials) def scan_disk(no): - authorize() + ref = request.environ['HTTP_REFERER'] series_scan_subtitles(no) @@ -901,7 +895,7 @@ def scan_disk(no): @app.route(base_url + 'scan_disk_movie/', methods=['GET']) # @custom_auth_basic(check_credentials) def scan_disk_movie(no): - authorize() + ref = request.environ['HTTP_REFERER'] movies_scan_subtitles(no) @@ -912,7 +906,7 @@ def scan_disk_movie(no): @app.route(base_url + 'search_missing_subtitles/', methods=['GET']) # @custom_auth_basic(check_credentials) def search_missing_subtitles(no): - authorize() + ref = request.environ['HTTP_REFERER'] add_job(series_download_subtitles, args=[no], name=('search_missing_subtitles_' + str(no))) @@ -923,7 +917,7 @@ def search_missing_subtitles(no): @app.route(base_url + 'search_missing_subtitles_movie/', methods=['GET']) # @custom_auth_basic(check_credentials) def search_missing_subtitles_movie(no): - authorize() + ref = request.environ['HTTP_REFERER'] add_job(movies_download_subtitles, args=[no], name=('movies_download_subtitles_' + str(no))) @@ -934,14 +928,14 @@ def search_missing_subtitles_movie(no): @app.route(base_url + 'history') # @custom_auth_basic(check_credentials) def history(): - authorize() + return render_template('history', bazarr_version=bazarr_version, base_url=base_url, current_port=settings.general.port) @app.route(base_url + 'historyseries') # @custom_auth_basic(check_credentials) def historyseries(): - authorize() + row_count = database.execute("SELECT COUNT(*) as count FROM table_history LEFT JOIN table_shows on " "table_history.sonarrSeriesId = table_shows.sonarrSeriesId WHERE " @@ -1020,7 +1014,7 @@ def historyseries(): @app.route(base_url + 'historymovies') # @custom_auth_basic(check_credentials) def historymovies(): - authorize() + row_count = database.execute("SELECT COUNT(*) as count FROM table_history_movie LEFT JOIN table_movies ON " "table_history_movie.radarrId=table_movies.radarrId " @@ -1097,14 +1091,14 @@ def historymovies(): @app.route(base_url + 'wanted') # @custom_auth_basic(check_credentials) def wanted(): - authorize() + return render_template('wanted', bazarr_version=bazarr_version, base_url=base_url, current_port=settings.general.port) @app.route(base_url + 'wantedseries') # @custom_auth_basic(check_credentials) def wantedseries(): - authorize() + if settings.sonarr.getboolean('only_monitored'): monitored_only_query_string = " AND monitored='True'" @@ -1138,7 +1132,7 @@ def wantedseries(): @app.route(base_url + 'wantedmovies') # @custom_auth_basic(check_credentials) def wantedmovies(): - authorize() + if settings.radarr.getboolean('only_monitored'): monitored_only_query_string = " AND monitored='True'" @@ -1169,7 +1163,7 @@ def wantedmovies(): @app.route(base_url + 'wanted_search_missing_subtitles') # @custom_auth_basic(check_credentials) def wanted_search_missing_subtitles_list(): - authorize() + ref = request.environ['HTTP_REFERER'] add_job(wanted_search_missing_subtitles, name='manual_wanted_search_missing_subtitles') @@ -1180,7 +1174,7 @@ def wanted_search_missing_subtitles_list(): @app.route(base_url + 'settings/') # @custom_auth_basic(check_credentials) def _settings(): - authorize() + settings_languages = database.execute("SELECT * FROM table_settings_languages ORDER BY name") settings_providers = sorted(provider_manager.names()) @@ -1194,7 +1188,7 @@ def _settings(): @app.route(base_url + 'save_settings', methods=['POST']) # @custom_auth_basic(check_credentials) def save_settings(): - authorize() + ref = request.environ['HTTP_REFERER'] settings_general_ip = request.form.get('settings_general_ip') @@ -1624,7 +1618,7 @@ def save_settings(): @app.route(base_url + 'check_update') # @custom_auth_basic(check_credentials) def check_update(): - authorize() + ref = request.environ['HTTP_REFERER'] if not args.no_update: @@ -1636,7 +1630,7 @@ def check_update(): @app.route(base_url + 'system') # @custom_auth_basic(check_credentials) def system(): - authorize() + def get_time_from_interval(td_object): seconds = int(td_object.total_seconds()) @@ -1720,7 +1714,7 @@ def system(): @app.route(base_url + 'logs') # @custom_auth_basic(check_credentials) def get_logs(): - authorize() + logs = [] with open(os.path.join(args.config_dir, 'log', 'bazarr.log')) as file: for line in file.readlines(): @@ -1735,7 +1729,7 @@ def get_logs(): @app.route(base_url + 'execute/') # @custom_auth_basic(check_credentials) def execute_task(taskid): - authorize() + ref = request.environ['HTTP_REFERER'] execute_now(taskid) @@ -1746,7 +1740,7 @@ def execute_task(taskid): @app.route(base_url + 'remove_subtitles', methods=['POST']) # @custom_auth_basic(check_credentials) def remove_subtitles(): - authorize() + episodePath = request.form.get('episodePath') language = request.form.get('language') subtitlesPath = request.form.get('subtitlesPath') @@ -1765,7 +1759,7 @@ def remove_subtitles(): @app.route(base_url + 'remove_subtitles_movie', methods=['POST']) # @custom_auth_basic(check_credentials) def remove_subtitles_movie(): - authorize() + moviePath = request.form.get('moviePath') language = request.form.get('language') subtitlesPath = request.form.get('subtitlesPath') @@ -1783,7 +1777,7 @@ def remove_subtitles_movie(): @app.route(base_url + 'get_subtitle', methods=['POST']) # @custom_auth_basic(check_credentials) def get_subtitle(): - authorize() + ref = request.environ['HTTP_REFERER'] episodePath = request.form.get('episodePath') @@ -1819,7 +1813,7 @@ def get_subtitle(): @app.route(base_url + 'manual_search', methods=['POST']) # @custom_auth_basic(check_credentials) def manual_search_json(): - authorize() + episodePath = request.form.get('episodePath') sceneName = request.form.get('sceneName') @@ -1838,7 +1832,7 @@ def manual_search_json(): @app.route(base_url + 'manual_get_subtitle', methods=['POST']) # @custom_auth_basic(check_credentials) def manual_get_subtitle(): - authorize() + ref = request.environ['HTTP_REFERER'] episodePath = request.form.get('episodePath') @@ -1876,7 +1870,7 @@ def manual_get_subtitle(): @app.route(base_url + 'manual_upload_subtitle', methods=['POST']) # @custom_auth_basic(check_credentials) def perform_manual_upload_subtitle(): - authorize() + ref = request.environ['HTTP_REFERER'] episodePath = request.form.get('episodePath') @@ -1920,7 +1914,7 @@ def perform_manual_upload_subtitle(): @app.route(base_url + 'get_subtitle_movie', methods=['POST']) # @custom_auth_basic(check_credentials) def get_subtitle_movie(): - authorize() + ref = request.environ['HTTP_REFERER'] moviePath = request.form.get('moviePath') @@ -1955,7 +1949,7 @@ def get_subtitle_movie(): @app.route(base_url + 'manual_search_movie', methods=['POST']) # @custom_auth_basic(check_credentials) def manual_search_movie_json(): - authorize() + moviePath = request.form.get('moviePath') sceneName = request.form.get('sceneName') @@ -1974,7 +1968,7 @@ def manual_search_movie_json(): @app.route(base_url + 'manual_get_subtitle_movie', methods=['POST']) # @custom_auth_basic(check_credentials) def manual_get_subtitle_movie(): - authorize() + ref = request.environ['HTTP_REFERER'] moviePath = request.form.get('moviePath') @@ -2010,7 +2004,7 @@ def manual_get_subtitle_movie(): @app.route(base_url + 'manual_upload_subtitle_movie', methods=['POST']) # @custom_auth_basic(check_credentials) def perform_manual_upload_subtitle_movie(): - authorize() + ref = request.environ['HTTP_REFERER'] moviePath = request.form.get('moviePath') @@ -2117,7 +2111,7 @@ def api_movies_history(): @app.route(base_url + 'test_url//', methods=['GET']) # @custom_auth_basic(check_credentials) def test_url(protocol, url): - authorize() + url = six.moves.urllib.parse.unquote(url) try: result = requests.get(protocol + "://" + url, allow_redirects=False, verify=False).json()['version'] @@ -2130,7 +2124,7 @@ def test_url(protocol, url): @app.route(base_url + 'test_notification//', methods=['GET']) # @custom_auth_basic(check_credentials) def test_notification(protocol, provider): - authorize() + provider = six.moves.urllib.parse.unquote(provider) apobj = apprise.Apprise() apobj.add(protocol + "://" + provider) @@ -2144,7 +2138,7 @@ def test_notification(protocol, provider): @app.route(base_url + 'notifications') # @custom_auth_basic(check_credentials) def notifications(): - authorize() + if queueconfig.notifications: return queueconfig.notifications.read() else: @@ -2154,14 +2148,14 @@ def notifications(): @app.route(base_url + 'running_tasks') # @custom_auth_basic(check_credentials) def running_tasks_list(): - authorize() + return dict(tasks=running_tasks) @app.route(base_url + 'episode_history/') # @custom_auth_basic(check_credentials) def episode_history(no): - authorize() + episode_history = database.execute("SELECT action, timestamp, language, provider, score FROM table_history " "WHERE sonarrEpisodeId=? ORDER BY timestamp DESC", (no,)) for item in episode_history: @@ -2200,7 +2194,7 @@ def episode_history(no): @app.route(base_url + 'movie_history/') # @custom_auth_basic(check_credentials) def movie_history(no): - authorize() + movie_history = database.execute("SELECT action, timestamp, language, provider, score FROM table_history_movie " "WHERE radarrId=? ORDER BY timestamp DESC", (no,)) for item in movie_history: diff --git a/libs/click/_bashcomplete.py b/libs/click/_bashcomplete.py index d9d26d28b..a5f1084c9 100644 --- a/libs/click/_bashcomplete.py +++ b/libs/click/_bashcomplete.py @@ -1,27 +1,81 @@ +import copy import os import re + from .utils import echo from .parser import split_arg_string -from .core import MultiCommand, Option +from .core import MultiCommand, Option, Argument +from .types import Choice +try: + from collections import abc +except ImportError: + import collections as abc -COMPLETION_SCRIPT = ''' +WORDBREAK = '=' + +# Note, only BASH version 4.4 and later have the nosort option. +COMPLETION_SCRIPT_BASH = ''' %(complete_func)s() { + local IFS=$'\n' COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ COMP_CWORD=$COMP_CWORD \\ %(autocomplete_var)s=complete $1 ) ) return 0 } -complete -F %(complete_func)s -o default %(script_names)s +%(complete_func)setup() { + local COMPLETION_OPTIONS="" + local BASH_VERSION_ARR=(${BASH_VERSION//./ }) + # Only BASH version 4.4 and later have the nosort option. + if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then + COMPLETION_OPTIONS="-o nosort" + fi + + complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s +} + +%(complete_func)setup +''' + +COMPLETION_SCRIPT_ZSH = ''' +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ + COMP_CWORD=$((CURRENT-1)) \\ + %(autocomplete_var)s=\"complete_zsh\" \\ + %(script_names)s )}") + + for key descr in ${(kv)response}; do + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U -Q + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -Q -a completions + fi + compstate[insert]="automenu" +} + +compdef %(complete_func)s %(script_names)s ''' _invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]') -def get_completion_script(prog_name, complete_var): +def get_completion_script(prog_name, complete_var, shell): cf_name = _invalid_ident_char_re.sub('', prog_name.replace('-', '_')) - return (COMPLETION_SCRIPT % { + script = COMPLETION_SCRIPT_ZSH if shell == 'zsh' else COMPLETION_SCRIPT_BASH + return (script % { 'complete_func': '_%s_completion' % cf_name, 'script_names': prog_name, 'autocomplete_var': complete_var, @@ -29,37 +83,189 @@ def get_completion_script(prog_name, complete_var): def resolve_ctx(cli, prog_name, args): + """ + Parse into a hierarchy of contexts. Contexts are connected through the parent variable. + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :return: the final context/command parsed + """ ctx = cli.make_context(prog_name, args, resilient_parsing=True) - while ctx.protected_args + ctx.args and isinstance(ctx.command, MultiCommand): - a = ctx.protected_args + ctx.args - cmd = ctx.command.get_command(ctx, a[0]) - if cmd is None: - return None - ctx = cmd.make_context(a[0], a[1:], parent=ctx, resilient_parsing=True) + args = ctx.protected_args + ctx.args + while args: + if isinstance(ctx.command, MultiCommand): + if not ctx.command.chain: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + ctx = cmd.make_context(cmd_name, args, parent=ctx, + resilient_parsing=True) + args = ctx.protected_args + ctx.args + else: + # Walk chained subcommand contexts saving the last one. + while args: + cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) + if cmd is None: + return ctx + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True) + args = sub_ctx.args + ctx = sub_ctx + args = sub_ctx.protected_args + sub_ctx.args + else: + break return ctx +def start_of_option(param_str): + """ + :param param_str: param_str to check + :return: whether or not this is the start of an option declaration (i.e. starts "-" or "--") + """ + return param_str and param_str[:1] == '-' + + +def is_incomplete_option(all_args, cmd_param): + """ + :param all_args: the full original list of args supplied + :param cmd_param: the current command paramter + :return: whether or not the last option declaration (i.e. starts "-" or "--") is incomplete and + corresponds to this cmd_param. In other words whether this cmd_param option can still accept + values + """ + if not isinstance(cmd_param, Option): + return False + if cmd_param.is_flag: + return False + last_option = None + for index, arg_str in enumerate(reversed([arg for arg in all_args if arg != WORDBREAK])): + if index + 1 > cmd_param.nargs: + break + if start_of_option(arg_str): + last_option = arg_str + + return True if last_option and last_option in cmd_param.opts else False + + +def is_incomplete_argument(current_params, cmd_param): + """ + :param current_params: the current params and values for this argument as already entered + :param cmd_param: the current command parameter + :return: whether or not the last argument is incomplete and corresponds to this cmd_param. In + other words whether or not the this cmd_param argument can still accept values + """ + if not isinstance(cmd_param, Argument): + return False + current_param_values = current_params[cmd_param.name] + if current_param_values is None: + return True + if cmd_param.nargs == -1: + return True + if isinstance(current_param_values, abc.Iterable) \ + and cmd_param.nargs > 1 and len(current_param_values) < cmd_param.nargs: + return True + return False + + +def get_user_autocompletions(ctx, args, incomplete, cmd_param): + """ + :param ctx: context associated with the parsed command + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :param cmd_param: command definition + :return: all the possible user-specified completions for the param + """ + results = [] + if isinstance(cmd_param.type, Choice): + # Choices don't support descriptions. + results = [(c, None) + for c in cmd_param.type.choices if str(c).startswith(incomplete)] + elif cmd_param.autocompletion is not None: + dynamic_completions = cmd_param.autocompletion(ctx=ctx, + args=args, + incomplete=incomplete) + results = [c if isinstance(c, tuple) else (c, None) + for c in dynamic_completions] + return results + + +def get_visible_commands_starting_with(ctx, starts_with): + """ + :param ctx: context associated with the parsed command + :starts_with: string that visible commands must start with. + :return: all visible (not hidden) commands that start with starts_with. + """ + for c in ctx.command.list_commands(ctx): + if c.startswith(starts_with): + command = ctx.command.get_command(ctx, c) + if not command.hidden: + yield command + + +def add_subcommand_completions(ctx, incomplete, completions_out): + # Add subcommand completions. + if isinstance(ctx.command, MultiCommand): + completions_out.extend( + [(c.name, c.get_short_help_str()) for c in get_visible_commands_starting_with(ctx, incomplete)]) + + # Walk up the context list and add any other completion possibilities from chained commands + while ctx.parent is not None: + ctx = ctx.parent + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + remaining_commands = [c for c in get_visible_commands_starting_with(ctx, incomplete) + if c.name not in ctx.protected_args] + completions_out.extend([(c.name, c.get_short_help_str()) for c in remaining_commands]) + + def get_choices(cli, prog_name, args, incomplete): + """ + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :return: all the possible completions for the incomplete + """ + all_args = copy.deepcopy(args) + ctx = resolve_ctx(cli, prog_name, args) if ctx is None: - return + return [] - choices = [] - if incomplete and not incomplete[:1].isalnum(): + # In newer versions of bash long opts with '='s are partitioned, but it's easier to parse + # without the '=' + if start_of_option(incomplete) and WORDBREAK in incomplete: + partition_incomplete = incomplete.partition(WORDBREAK) + all_args.append(partition_incomplete[0]) + incomplete = partition_incomplete[2] + elif incomplete == WORDBREAK: + incomplete = '' + + completions = [] + if start_of_option(incomplete): + # completions for partial options for param in ctx.command.params: - if not isinstance(param, Option): - continue - choices.extend(param.opts) - choices.extend(param.secondary_opts) - elif isinstance(ctx.command, MultiCommand): - choices.extend(ctx.command.list_commands(ctx)) + if isinstance(param, Option) and not param.hidden: + param_opts = [param_opt for param_opt in param.opts + + param.secondary_opts if param_opt not in all_args or param.multiple] + completions.extend([(o, param.help) for o in param_opts if o.startswith(incomplete)]) + return completions + # completion for option values from user supplied values + for param in ctx.command.params: + if is_incomplete_option(all_args, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + # completion for argument values from user supplied values + for param in ctx.command.params: + if is_incomplete_argument(ctx.params, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) - for item in choices: - if item.startswith(incomplete): - yield item + add_subcommand_completions(ctx, incomplete, completions) + # Sort before returning so that proper ordering can be enforced in custom types. + return sorted(completions) -def do_complete(cli, prog_name): +def do_complete(cli, prog_name, include_descriptions): cwords = split_arg_string(os.environ['COMP_WORDS']) cword = int(os.environ['COMP_CWORD']) args = cwords[1:cword] @@ -69,15 +275,19 @@ def do_complete(cli, prog_name): incomplete = '' for item in get_choices(cli, prog_name, args, incomplete): - echo(item) + echo(item[0]) + if include_descriptions: + # ZSH has trouble dealing with empty array parameters when returned from commands, so use a well defined character '_' to indicate no description is present. + echo(item[1] if item[1] else '_') return True def bashcomplete(cli, prog_name, complete_var, complete_instr): - if complete_instr == 'source': - echo(get_completion_script(prog_name, complete_var)) + if complete_instr.startswith('source'): + shell = 'zsh' if complete_instr == 'source_zsh' else 'bash' + echo(get_completion_script(prog_name, complete_var, shell)) return True - elif complete_instr == 'complete': - return do_complete(cli, prog_name) + elif complete_instr == 'complete' or complete_instr == 'complete_zsh': + return do_complete(cli, prog_name, complete_instr == 'complete_zsh') return False diff --git a/libs/click/_compat.py b/libs/click/_compat.py index 2b43412c4..937e2301d 100644 --- a/libs/click/_compat.py +++ b/libs/click/_compat.py @@ -7,24 +7,31 @@ from weakref import WeakKeyDictionary PY2 = sys.version_info[0] == 2 -WIN = sys.platform.startswith('win') +CYGWIN = sys.platform.startswith('cygwin') +# Determine local App Engine environment, per Google's own suggestion +APP_ENGINE = ('APPENGINE_RUNTIME' in os.environ and + 'Development/' in os.environ['SERVER_SOFTWARE']) +WIN = sys.platform.startswith('win') and not APP_ENGINE DEFAULT_COLUMNS = 80 -_ansi_re = re.compile('\033\[((?:\d|;)*)([a-zA-Z])') +_ansi_re = re.compile(r'\033\[((?:\d|;)*)([a-zA-Z])') def get_filesystem_encoding(): return sys.getfilesystemencoding() or sys.getdefaultencoding() -def _make_text_stream(stream, encoding, errors): +def _make_text_stream(stream, encoding, errors, + force_readable=False, force_writable=False): if encoding is None: encoding = get_best_encoding(stream) if errors is None: errors = 'replace' return _NonClosingTextIOWrapper(stream, encoding, errors, - line_buffering=True) + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable) def is_ascii_encoding(encoding): @@ -45,8 +52,10 @@ def get_best_encoding(stream): class _NonClosingTextIOWrapper(io.TextIOWrapper): - def __init__(self, stream, encoding, errors, **extra): - self._stream = stream = _FixupStream(stream) + def __init__(self, stream, encoding, errors, + force_readable=False, force_writable=False, **extra): + self._stream = stream = _FixupStream(stream, force_readable, + force_writable) io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) # The io module is a place where the Python 3 text behavior @@ -81,10 +90,16 @@ class _FixupStream(object): """The new io interface needs more from streams than streams traditionally implement. As such, this fix-up code is necessary in some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). """ - def __init__(self, stream): + def __init__(self, stream, force_readable=False, force_writable=False): self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable def __getattr__(self, name): return getattr(self._stream, name) @@ -101,6 +116,8 @@ class _FixupStream(object): return self._stream.read(size) def readable(self): + if self._force_readable: + return True x = getattr(self._stream, 'readable', None) if x is not None: return x() @@ -111,6 +128,8 @@ class _FixupStream(object): return True def writable(self): + if self._force_writable: + return True x = getattr(self._stream, 'writable', None) if x is not None: return x() @@ -139,6 +158,7 @@ if PY2: bytes = str raw_input = raw_input string_types = (str, unicode) + int_types = (int, long) iteritems = lambda x: x.iteritems() range_type = xrange @@ -165,10 +185,13 @@ if PY2: # available (which is why we use try-catch instead of the WIN variable # here), such as the Google App Engine development server on Windows. In # those cases there is just nothing we can do. + def set_binary_mode(f): + return f + try: import msvcrt except ImportError: - set_binary_mode = lambda x: x + pass else: def set_binary_mode(f): try: @@ -179,6 +202,21 @@ if PY2: msvcrt.setmode(fileno, os.O_BINARY) return f + try: + import fcntl + except ImportError: + pass + else: + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + flags = fcntl.fcntl(fileno, fcntl.F_GETFL) + fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + return f + def isidentifier(x): return _identifier_re.search(x) is not None @@ -186,28 +224,35 @@ if PY2: return set_binary_mode(sys.stdin) def get_binary_stdout(): + _wrap_std_stream('stdout') return set_binary_mode(sys.stdout) def get_binary_stderr(): + _wrap_std_stream('stderr') return set_binary_mode(sys.stderr) def get_text_stdin(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stdin, encoding, errors) if rv is not None: return rv - return _make_text_stream(sys.stdin, encoding, errors) + return _make_text_stream(sys.stdin, encoding, errors, + force_readable=True) def get_text_stdout(encoding=None, errors=None): + _wrap_std_stream('stdout') rv = _get_windows_console_stream(sys.stdout, encoding, errors) if rv is not None: return rv - return _make_text_stream(sys.stdout, encoding, errors) + return _make_text_stream(sys.stdout, encoding, errors, + force_writable=True) def get_text_stderr(encoding=None, errors=None): + _wrap_std_stream('stderr') rv = _get_windows_console_stream(sys.stderr, encoding, errors) if rv is not None: return rv - return _make_text_stream(sys.stderr, encoding, errors) + return _make_text_stream(sys.stderr, encoding, errors, + force_writable=True) def filename_to_ui(value): if isinstance(value, bytes): @@ -218,6 +263,7 @@ else: text_type = str raw_input = input string_types = (str,) + int_types = (int,) range_type = range isidentifier = lambda x: x.isidentifier() iteritems = lambda x: iter(x.items()) @@ -298,7 +344,8 @@ else: return False - def _force_correct_text_reader(text_reader, encoding, errors): + def _force_correct_text_reader(text_reader, encoding, errors, + force_readable=False): if _is_binary_reader(text_reader, False): binary_reader = text_reader else: @@ -324,9 +371,11 @@ else: # we're so fundamentally fucked that nothing can repair it. if errors is None: errors = 'replace' - return _make_text_stream(binary_reader, encoding, errors) + return _make_text_stream(binary_reader, encoding, errors, + force_readable=force_readable) - def _force_correct_text_writer(text_writer, encoding, errors): + def _force_correct_text_writer(text_writer, encoding, errors, + force_writable=False): if _is_binary_writer(text_writer, False): binary_writer = text_writer else: @@ -352,7 +401,8 @@ else: # we're so fundamentally fucked that nothing can repair it. if errors is None: errors = 'replace' - return _make_text_stream(binary_writer, encoding, errors) + return _make_text_stream(binary_writer, encoding, errors, + force_writable=force_writable) def get_binary_stdin(): reader = _find_binary_reader(sys.stdin) @@ -379,19 +429,22 @@ else: rv = _get_windows_console_stream(sys.stdin, encoding, errors) if rv is not None: return rv - return _force_correct_text_reader(sys.stdin, encoding, errors) + return _force_correct_text_reader(sys.stdin, encoding, errors, + force_readable=True) def get_text_stdout(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stdout, encoding, errors) if rv is not None: return rv - return _force_correct_text_writer(sys.stdout, encoding, errors) + return _force_correct_text_writer(sys.stdout, encoding, errors, + force_writable=True) def get_text_stderr(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stderr, encoding, errors) if rv is not None: return rv - return _force_correct_text_writer(sys.stderr, encoding, errors) + return _force_correct_text_writer(sys.stderr, encoding, errors, + force_writable=True) def filename_to_ui(value): if isinstance(value, bytes): @@ -420,7 +473,7 @@ def open_stream(filename, mode='r', encoding=None, errors='strict', # Standard streams first. These are simple because they don't need # special handling for the atomic flag. It's entirely ignored. if filename == '-': - if 'w' in mode: + if any(m in mode for m in ['w', 'a', 'x']): if 'b' in mode: return get_binary_stdout(), False return get_text_stdout(encoding=encoding, errors=errors), False @@ -460,7 +513,7 @@ def open_stream(filename, mode='r', encoding=None, errors='strict', else: f = os.fdopen(fd, mode) - return _AtomicFile(f, tmp_filename, filename), True + return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True # Used in a destructor call, needs extra protection from interpreter cleanup. @@ -533,7 +586,7 @@ if WIN: # Windows has a smaller terminal DEFAULT_COLUMNS = 79 - from ._winconsole import _get_windows_console_stream + from ._winconsole import _get_windows_console_stream, _wrap_std_stream def _get_argv_encoding(): import locale @@ -595,6 +648,7 @@ else: return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() _get_windows_console_stream = lambda *x: None + _wrap_std_stream = lambda *x: None def term_len(x): @@ -620,6 +674,7 @@ def _make_cached_stream_func(src_func, wrapper_func): return rv rv = wrapper_func() try: + stream = src_func() # In case wrapper_func() modified the stream cache[stream] = rv except Exception: pass diff --git a/libs/click/_termui_impl.py b/libs/click/_termui_impl.py index 7cfd3d5c4..00a8e5ef1 100644 --- a/libs/click/_termui_impl.py +++ b/libs/click/_termui_impl.py @@ -1,20 +1,24 @@ +# -*- coding: utf-8 -*- """ - click._termui_impl - ~~~~~~~~~~~~~~~~~~ +click._termui_impl +~~~~~~~~~~~~~~~~~~ - This module contains implementations for the termui module. To keep the - import time of Click down, some infrequently used functionality is placed - in this module and only imported as needed. +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. - :copyright: (c) 2014 by Armin Ronacher. - :license: BSD, see LICENSE for more details. +:copyright: © 2014 by the Pallets team. +:license: BSD, see LICENSE.rst for more details. """ + import os import sys import time import math +import contextlib from ._compat import _default_text_stdout, range_type, PY2, isatty, \ - open_stream, strip_ansi, term_len, get_best_encoding, WIN + open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \ + CYGWIN from .utils import echo from .exceptions import ClickException @@ -41,7 +45,7 @@ def _length_hint(obj): except TypeError: return None if hint is NotImplemented or \ - not isinstance(hint, (int, long)) or \ + not isinstance(hint, int_types) or \ hint < 0: return None return hint @@ -88,6 +92,7 @@ class ProgressBar(object): self.current_item = None self.is_hidden = not isatty(self.file) self._last_line = None + self.short_limit = 0.5 def __enter__(self): self.entered = True @@ -101,10 +106,13 @@ class ProgressBar(object): if not self.entered: raise RuntimeError('You need to use progress bars in a with block.') self.render_progress() - return self + return self.generator() + + def is_fast(self): + return time.time() - self.start <= self.short_limit def render_finish(self): - if self.is_hidden: + if self.is_hidden or self.is_fast(): return self.file.write(AFTER_BAR) self.file.flush() @@ -129,13 +137,13 @@ class ProgressBar(object): def format_eta(self): if self.eta_known: - t = self.eta + 1 + t = int(self.eta) seconds = t % 60 - t /= 60 + t //= 60 minutes = t % 60 - t /= 60 + t //= 60 hours = t % 24 - t /= 24 + t //= 24 if t > 0: days = t return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) @@ -152,25 +160,27 @@ class ProgressBar(object): def format_pct(self): return ('% 4d%%' % int(self.pct * 100))[1:] - def format_progress_line(self): - show_percent = self.show_percent - - info_bits = [] + def format_bar(self): if self.length_known: bar_length = int(self.pct * self.width) bar = self.fill_char * bar_length bar += self.empty_char * (self.width - bar_length) - if show_percent is None: - show_percent = not self.show_pos + elif self.finished: + bar = self.fill_char * self.width else: - if self.finished: - bar = self.fill_char * self.width - else: - bar = list(self.empty_char * (self.width or 1)) - if self.time_per_iteration != 0: - bar[int((math.cos(self.pos * self.time_per_iteration) - / 2.0 + 0.5) * self.width)] = self.fill_char - bar = ''.join(bar) + bar = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + bar[int((math.cos(self.pos * self.time_per_iteration) + / 2.0 + 0.5) * self.width)] = self.fill_char + bar = ''.join(bar) + return bar + + def format_progress_line(self): + show_percent = self.show_percent + + info_bits = [] + if self.length_known and show_percent is None: + show_percent = not self.show_pos if self.show_pos: info_bits.append(self.format_pos()) @@ -185,49 +195,47 @@ class ProgressBar(object): return (self.bar_template % { 'label': self.label, - 'bar': bar, + 'bar': self.format_bar(), 'info': self.info_sep.join(info_bits) }).rstrip() def render_progress(self): from .termui import get_terminal_size - nl = False if self.is_hidden: - buf = [self.label] - nl = True - else: - buf = [] - # Update width in case the terminal has been resized - if self.autowidth: - old_width = self.width - self.width = 0 - clutter_length = term_len(self.format_progress_line()) - new_width = max(0, get_terminal_size()[0] - clutter_length) - if new_width < old_width: - buf.append(BEFORE_BAR) - buf.append(' ' * self.max_width) - self.max_width = new_width - self.width = new_width + return - clear_width = self.width - if self.max_width is not None: - clear_width = self.max_width + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, get_terminal_size()[0] - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(' ' * self.max_width) + self.max_width = new_width + self.width = new_width - buf.append(BEFORE_BAR) - line = self.format_progress_line() - line_len = term_len(line) - if self.max_width is None or self.max_width < line_len: - self.max_width = line_len - buf.append(line) + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width - buf.append(' ' * (clear_width - line_len)) + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(' ' * (clear_width - line_len)) line = ''.join(buf) - # Render the line only if it changed. - if line != self._last_line: + + if line != self._last_line and not self.is_fast(): self._last_line = line - echo(line, file=self.file, color=self.color, nl=nl) + echo(line, file=self.file, color=self.color, nl=False) self.file.flush() def make_step(self, n_steps): @@ -239,7 +247,16 @@ class ProgressBar(object): return self.last_eta = time.time() - self.avg = self.avg[-6:] + [-(self.start - time.time()) / (self.pos)] + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] self.eta_known = self.length_known @@ -252,54 +269,56 @@ class ProgressBar(object): self.current_item = None self.finished = True - def next(self): + def generator(self): + """ + Returns a generator which yields the items added to the bar during + construction, and updates the progress bar *after* the yielded block + returns. + """ + if not self.entered: + raise RuntimeError('You need to use progress bars in a with block.') + if self.is_hidden: - return next(self.iter) - try: - rv = next(self.iter) - self.current_item = rv - except StopIteration: + for rv in self.iter: + yield rv + else: + for rv in self.iter: + self.current_item = rv + yield rv + self.update(1) self.finish() self.render_progress() - raise StopIteration() - else: - self.update(1) - return rv - - if not PY2: - __next__ = next - del next -def pager(text, color=None): +def pager(generator, color=None): """Decide what method to use for paging through text.""" stdout = _default_text_stdout() if not isatty(sys.stdin) or not isatty(stdout): - return _nullpager(stdout, text, color) + return _nullpager(stdout, generator, color) pager_cmd = (os.environ.get('PAGER', None) or '').strip() if pager_cmd: if WIN: - return _tempfilepager(text, pager_cmd, color) - return _pipepager(text, pager_cmd, color) + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) if os.environ.get('TERM') in ('dumb', 'emacs'): - return _nullpager(stdout, text, color) + return _nullpager(stdout, generator, color) if WIN or sys.platform.startswith('os2'): - return _tempfilepager(text, 'more <', color) + return _tempfilepager(generator, 'more <', color) if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: - return _pipepager(text, 'less', color) + return _pipepager(generator, 'less', color) import tempfile fd, filename = tempfile.mkstemp() os.close(fd) try: if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: - return _pipepager(text, 'more', color) - return _nullpager(stdout, text, color) + return _pipepager(generator, 'more', color) + return _nullpager(stdout, generator, color) finally: os.unlink(filename) -def _pipepager(text, cmd, color): +def _pipepager(generator, cmd, color): """Page through text by feeding it to another program. Invoking a pager through this might support colors. """ @@ -317,17 +336,19 @@ def _pipepager(text, cmd, color): elif 'r' in less_flags or 'R' in less_flags: color = True - if not color: - text = strip_ansi(text) - c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) encoding = get_best_encoding(c.stdin) try: - c.stdin.write(text.encode(encoding, 'replace')) - c.stdin.close() + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text.encode(encoding, 'replace')) except (IOError, KeyboardInterrupt): pass + else: + c.stdin.close() # Less doesn't respect ^C, but catches it for its own UI purposes (aborting # search or other commands inside less). @@ -346,10 +367,12 @@ def _pipepager(text, cmd, color): break -def _tempfilepager(text, cmd, color): +def _tempfilepager(generator, cmd, color): """Page through text by invoking a program on a temporary file.""" import tempfile filename = tempfile.mktemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) if not color: text = strip_ansi(text) encoding = get_best_encoding(sys.stdout) @@ -361,11 +384,12 @@ def _tempfilepager(text, cmd, color): os.unlink(filename) -def _nullpager(stream, text, color): +def _nullpager(stream, generator, color): """Simply print unformatted text. This is the ultimate fallback.""" - if not color: - text = strip_ansi(text) - stream.write(text) + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) class Editor(object): @@ -478,6 +502,14 @@ def open_url(url, wait=False, locate=False): args = 'start %s "" "%s"' % ( wait and '/WAIT' or '', url.replace('"', '')) return os.system(args) + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = 'cygstart "%s"' % (os.path.dirname(url).replace('"', '')) + else: + args = 'cygstart %s "%s"' % ( + wait and '-w' or '', url.replace('"', '')) + return os.system(args) try: if locate: @@ -497,32 +529,69 @@ def open_url(url, wait=False, locate=False): def _translate_ch_to_exc(ch): - if ch == '\x03': + if ch == u'\x03': raise KeyboardInterrupt() - if ch == '\x04': + if ch == u'\x04' and not WIN: # Unix-like, Ctrl+D + raise EOFError() + if ch == u'\x1a' and WIN: # Windows, Ctrl+Z raise EOFError() if WIN: import msvcrt + @contextlib.contextmanager + def raw_terminal(): + yield + def getchar(echo): - rv = msvcrt.getch() + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. if echo: - msvcrt.putchar(rv) + func = msvcrt.getwche + else: + func = msvcrt.getwch + + rv = func() + if rv in (u'\x00', u'\xe0'): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() _translate_ch_to_exc(rv) - if PY2: - enc = getattr(sys.stdin, 'encoding', None) - if enc is not None: - rv = rv.decode(enc, 'replace') - else: - rv = rv.decode('cp1252', 'replace') return rv else: import tty import termios - def getchar(echo): + @contextlib.contextmanager + def raw_terminal(): if not isatty(sys.stdin): f = open('/dev/tty') fd = f.fileno() @@ -533,9 +602,7 @@ else: old_settings = termios.tcgetattr(fd) try: tty.setraw(fd) - ch = os.read(fd, 32) - if echo and isatty(sys.stdout): - sys.stdout.write(ch) + yield fd finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) sys.stdout.flush() @@ -543,5 +610,12 @@ else: f.close() except termios.error: pass - _translate_ch_to_exc(ch) - return ch.decode(get_best_encoding(sys.stdin), 'replace') + + def getchar(echo): + with raw_terminal() as fd: + ch = os.read(fd, 32) + ch = ch.decode(get_best_encoding(sys.stdin), 'replace') + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + _translate_ch_to_exc(ch) + return ch diff --git a/libs/click/_unicodefun.py b/libs/click/_unicodefun.py index 9e17a384e..620edff37 100644 --- a/libs/click/_unicodefun.py +++ b/libs/click/_unicodefun.py @@ -14,6 +14,8 @@ click = sys.modules[__name__.rsplit('.', 1)[0]] def _find_unicode_literals_frame(): import __future__ + if not hasattr(sys, '_getframe'): # not all Python implementations have it + return 0 frm = sys._getframe(1) idx = 1 while frm is not None: @@ -41,7 +43,7 @@ def _check_for_unicode_literals(): 'because it can introduce subtle bugs in your ' 'code. You should instead use explicit u"" literals ' 'for your unicode strings. For more information see ' - 'http://click.pocoo.org/python3/'), + 'https://click.palletsprojects.com/python3/'), stacklevel=bad_frame) @@ -60,8 +62,11 @@ def _verify_python3_env(): extra = '' if os.name == 'posix': import subprocess - rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE).communicate()[0] + try: + rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate()[0] + except OSError: + rv = b'' good_locales = set() has_c_utf8 = False @@ -94,7 +99,7 @@ def _verify_python3_env(): else: extra += ( 'This system lists a couple of UTF-8 supporting locales that\n' - 'you can pick from. The following suitable locales where\n' + 'you can pick from. The following suitable locales were\n' 'discovered: %s' ) % ', '.join(sorted(good_locales)) @@ -112,7 +117,9 @@ def _verify_python3_env(): 'is not supported' ) % bad_locale - raise RuntimeError('Click will abort further execution because Python 3 ' - 'was configured to use ASCII as encoding for the ' - 'environment. Consult http://click.pocoo.org/python3/' - 'for mitigation steps.' + extra) + raise RuntimeError( + 'Click will abort further execution because Python 3 was' + ' configured to use ASCII as encoding for the environment.' + ' Consult https://click.palletsprojects.com/en/7.x/python3/ for' + ' mitigation steps.' + extra + ) diff --git a/libs/click/_winconsole.py b/libs/click/_winconsole.py index 9aed94216..bbb080dda 100644 --- a/libs/click/_winconsole.py +++ b/libs/click/_winconsole.py @@ -15,7 +15,7 @@ import zlib import time import ctypes import msvcrt -from click._compat import _NonClosingTextIOWrapper, text_type, PY2 +from ._compat import _NonClosingTextIOWrapper, text_type, PY2 from ctypes import byref, POINTER, c_int, c_char, c_char_p, \ c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE try: @@ -201,6 +201,40 @@ class ConsoleStream(object): ) +class WindowsChunkedWriter(object): + """ + Wraps a stream (such as stdout), acting as a transparent proxy for all + attribute access apart from method 'write()' which we wrap to write in + limited chunks due to a Windows limitation on binary console streams. + """ + def __init__(self, wrapped): + # double-underscore everything to prevent clashes with names of + # attributes on the wrapped stream object. + self.__wrapped = wrapped + + def __getattr__(self, name): + return getattr(self.__wrapped, name) + + def write(self, text): + total_to_write = len(text) + written = 0 + + while written < total_to_write: + to_write = min(total_to_write - written, MAX_BYTES_WRITTEN) + self.__wrapped.write(text[written:written+to_write]) + written += to_write + + +_wrapped_std_streams = set() + + +def _wrap_std_stream(name): + # Python 2 & Windows 7 and below + if PY2 and sys.getwindowsversion()[:2] <= (6, 1) and name not in _wrapped_std_streams: + setattr(sys, name, WindowsChunkedWriter(getattr(sys, name))) + _wrapped_std_streams.add(name) + + def _get_text_stdin(buffer_stream): text_stream = _NonClosingTextIOWrapper( io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), @@ -210,14 +244,14 @@ def _get_text_stdin(buffer_stream): def _get_text_stdout(buffer_stream): text_stream = _NonClosingTextIOWrapper( - _WindowsConsoleWriter(STDOUT_HANDLE), + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), 'utf-16-le', 'strict', line_buffering=True) return ConsoleStream(text_stream, buffer_stream) def _get_text_stderr(buffer_stream): text_stream = _NonClosingTextIOWrapper( - _WindowsConsoleWriter(STDERR_HANDLE), + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), 'utf-16-le', 'strict', line_buffering=True) return ConsoleStream(text_stream, buffer_stream) @@ -261,7 +295,7 @@ def _get_windows_console_stream(f, encoding, errors): func = _stream_factories.get(f.fileno()) if func is not None: if not PY2: - f = getattr(f, 'buffer') + f = getattr(f, 'buffer', None) if f is None: return None else: diff --git a/libs/click/core.py b/libs/click/core.py index 745645147..7a1e3422b 100644 --- a/libs/click/core.py +++ b/libs/click/core.py @@ -1,4 +1,5 @@ import errno +import inspect import os import sys from contextlib import contextmanager @@ -6,15 +7,16 @@ from itertools import repeat from functools import update_wrapper from .types import convert_type, IntRange, BOOL -from .utils import make_str, make_default_short_help, echo, get_os_args +from .utils import PacifyFlushWrapper, make_str, make_default_short_help, \ + echo, get_os_args from .exceptions import ClickException, UsageError, BadParameter, Abort, \ - MissingParameter -from .termui import prompt, confirm + MissingParameter, Exit +from .termui import prompt, confirm, style from .formatting import HelpFormatter, join_options from .parser import OptionParser, split_opt from .globals import push_context, pop_context -from ._compat import PY2, isidentifier, iteritems +from ._compat import PY2, isidentifier, iteritems, string_types from ._unicodefun import _check_for_unicode_literals, _verify_python3_env @@ -24,6 +26,24 @@ _missing = object() SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' +DEPRECATED_HELP_NOTICE = ' (DEPRECATED)' +DEPRECATED_INVOKE_NOTICE = 'DeprecationWarning: ' + \ + 'The command %(name)s is deprecated.' + + +def _maybe_show_deprecated_notice(cmd): + if cmd.deprecated: + echo(style(DEPRECATED_INVOKE_NOTICE % {'name': cmd.name}, fg='red'), err=True) + + +def fast_exit(code): + """Exit without garbage collection, this speeds up exit by about 10ms for + things like bash completion. + """ + sys.stdout.flush() + sys.stderr.flush() + os._exit(code) + def _bashcomplete(cmd, prog_name, complete_var=None): """Internal handler for the bash completion support.""" @@ -35,7 +55,7 @@ def _bashcomplete(cmd, prog_name, complete_var=None): from ._bashcomplete import bashcomplete if bashcomplete(cmd, prog_name, complete_var, complete_instr): - sys.exit(1) + fast_exit(1) def _check_multicommand(base_command, cmd_name, cmd, register=False): @@ -50,9 +70,7 @@ def _check_multicommand(base_command, cmd_name, cmd, register=False): raise RuntimeError('%s. Command "%s" is set to chain and "%s" was ' 'added as subcommand but it in itself is a ' 'multi command. ("%s" is a %s within a chained ' - '%s named "%s"). This restriction was supposed to ' - 'be lifted in 6.0 but the fix was flawed. This ' - 'will be fixed in Click 7.0' % ( + '%s named "%s").' % ( hint, base_command.name, cmd_name, cmd_name, cmd.__class__.__name__, base_command.__class__.__name__, @@ -165,7 +183,8 @@ class Context(object): add some safety mapping on the right. :param resilient_parsing: if this flag is enabled then Click will parse without any interactivity or callback - invocation. This is useful for implementing + invocation. Default values will also be + ignored. This is useful for implementing things such as completion support. :param allow_extra_args: if this is set to `True` then extra arguments at the end will not raise an error and will be @@ -295,7 +314,8 @@ class Context(object): self.token_normalize_func = token_normalize_func #: Indicates if resilient parsing is enabled. In that case Click - #: will do its best to not cause any failures. + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. self.resilient_parsing = resilient_parsing # If there is no envvar prefix yet, but the parent has one and @@ -308,7 +328,7 @@ class Context(object): auto_envvar_prefix = '%s_%s' % (parent.auto_envvar_prefix, self.info_name.upper()) else: - self.auto_envvar_prefix = auto_envvar_prefix.upper() + auto_envvar_prefix = auto_envvar_prefix.upper() self.auto_envvar_prefix = auto_envvar_prefix if color is None and parent is not None: @@ -372,7 +392,7 @@ class Context(object): @property def meta(self): """This is a dictionary which is shared with all the contexts - that are nested. It exists so that click utiltiies can store some + that are nested. It exists so that click utilities can store some state here if they need to. It is however the responsibility of that code to manage this dictionary well. @@ -481,7 +501,7 @@ class Context(object): def exit(self, code=0): """Exits the application with a given exit code.""" - sys.exit(code) + raise Exit(code) def get_usage(self): """Helper method to get formatted usage string for the current @@ -655,7 +675,7 @@ class BaseCommand(object): name from ``sys.argv[0]``. :param complete_var: the environment variable that controls the bash completion support. The default is - ``"__COMPLETE"`` with prog name in + ``"__COMPLETE"`` with prog_name in uppercase. :param standalone_mode: the default behavior is to invoke the script in standalone mode. Click will then @@ -670,7 +690,7 @@ class BaseCommand(object): constructor. See :class:`Context` for more information. """ # If we are in Python 3, we will verify that the environment is - # sane at this point of reject further execution to avoid a + # sane at this point or reject further execution to avoid a # broken script. if not PY2: _verify_python3_env() @@ -697,6 +717,13 @@ class BaseCommand(object): rv = self.invoke(ctx) if not standalone_mode: return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness ctx.exit() except (EOFError, KeyboardInterrupt): echo(file=sys.stderr) @@ -708,9 +735,24 @@ class BaseCommand(object): sys.exit(e.exit_code) except IOError as e: if e.errno == errno.EPIPE: + sys.stdout = PacifyFlushWrapper(sys.stdout) + sys.stderr = PacifyFlushWrapper(sys.stderr) sys.exit(1) else: raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code except Abort: if not standalone_mode: raise @@ -743,11 +785,16 @@ class Command(BaseCommand): shown on the command listing of the parent command. :param add_help_option: by default each command registers a ``--help`` option. This can be disabled by this parameter. + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. """ def __init__(self, name, context_settings=None, callback=None, params=None, help=None, epilog=None, short_help=None, - options_metavar='[OPTIONS]', add_help_option=True): + options_metavar='[OPTIONS]', add_help_option=True, + hidden=False, deprecated=False): BaseCommand.__init__(self, name, context_settings) #: the callback to execute when the command fires. This might be #: `None` in which case nothing happens. @@ -756,13 +803,17 @@ class Command(BaseCommand): #: should show up in the help page and execute. Eager parameters #: will automatically be handled before non eager ones. self.params = params or [] + # if a form feed (page break) is found in the help text, truncate help + # text to the content preceding the first form feed + if help and '\f' in help: + help = help.split('\f', 1)[0] self.help = help self.epilog = epilog self.options_metavar = options_metavar - if short_help is None and help: - short_help = make_default_short_help(help) self.short_help = short_help self.add_help_option = add_help_option + self.hidden = hidden + self.deprecated = deprecated def get_usage(self, ctx): formatter = ctx.make_formatter() @@ -816,8 +867,6 @@ class Command(BaseCommand): def make_parser(self, ctx): """Creates the underlying option parser for this command.""" parser = OptionParser(ctx) - parser.allow_interspersed_args = ctx.allow_interspersed_args - parser.ignore_unknown_options = ctx.ignore_unknown_options for param in self.get_params(ctx): param.add_to_parser(parser, ctx) return parser @@ -830,6 +879,10 @@ class Command(BaseCommand): self.format_help(ctx, formatter) return formatter.getvalue().rstrip('\n') + def get_short_help_str(self, limit=45): + """Gets short help for the command or makes it by shortening the long help string.""" + return self.short_help or self.help and make_default_short_help(self.help, limit) or '' + def format_help(self, ctx, formatter): """Writes the help into the formatter if it exists. @@ -850,7 +903,14 @@ class Command(BaseCommand): if self.help: formatter.write_paragraph() with formatter.indentation(): - formatter.write_text(self.help) + help_text = self.help + if self.deprecated: + help_text += DEPRECATED_HELP_NOTICE + formatter.write_text(help_text) + elif self.deprecated: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(DEPRECATED_HELP_NOTICE) def format_options(self, ctx, formatter): """Writes all the options into the formatter if they exist.""" @@ -891,6 +951,7 @@ class Command(BaseCommand): """Given a context, this invokes the attached callback (if it exists) in the right way. """ + _maybe_show_deprecated_notice(self) if self.callback is not None: return ctx.invoke(self.callback, **ctx.params) @@ -996,19 +1057,29 @@ class MultiCommand(Command): """Extra format methods for multi methods that adds all the commands after the options. """ - rows = [] + commands = [] for subcommand in self.list_commands(ctx): cmd = self.get_command(ctx, subcommand) # What is this, the tool lied about a command. Ignore it if cmd is None: continue + if cmd.hidden: + continue - help = cmd.short_help or '' - rows.append((subcommand, help)) + commands.append((subcommand, cmd)) - if rows: - with formatter.section('Commands'): - formatter.write_dl(rows) + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section('Commands'): + formatter.write_dl(rows) def parse_args(self, ctx, args): if not args and self.no_args_is_help and not ctx.resilient_parsing: @@ -1111,7 +1182,7 @@ class MultiCommand(Command): # an option we want to kick off parsing again for arguments to # resolve things like --help which now should go to the main # place. - if cmd is None: + if cmd is None and not ctx.resilient_parsing: if split_opt(cmd_name)[0]: self.parse_args(ctx, ctx.args) ctx.fail('No such command "%s".' % original_cmd_name) @@ -1216,7 +1287,7 @@ class CommandCollection(MultiCommand): class Parameter(object): - """A parameter to a command comes in two versions: they are either + r"""A parameter to a command comes in two versions: they are either :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently not supported by design as some of the internals for parsing are intentionally not finalized. @@ -1261,7 +1332,8 @@ class Parameter(object): def __init__(self, param_decls=None, type=None, required=False, default=None, callback=None, nargs=None, metavar=None, - expose_value=True, is_eager=False, envvar=None): + expose_value=True, is_eager=False, envvar=None, + autocompletion=None): self.name, self.opts, self.secondary_opts = \ self._parse_decls(param_decls or (), expose_value) @@ -1284,6 +1356,7 @@ class Parameter(object): self.is_eager = is_eager self.metavar = metavar self.envvar = envvar + self.autocompletion = autocompletion @property def human_readable_name(self): @@ -1316,10 +1389,10 @@ class Parameter(object): def consume_value(self, ctx, opts): value = opts.get(self.name) - if value is None: - value = ctx.lookup_default(self.name) if value is None: value = self.value_from_envvar(ctx) + if value is None: + value = ctx.lookup_default(self.name) return value def type_cast_value(self, ctx, value): @@ -1364,7 +1437,7 @@ class Parameter(object): def full_process_value(self, ctx, value): value = self.process_value(ctx, value) - if value is None: + if value is None and not ctx.resilient_parsing: value = self.get_default(ctx) if self.required and self.value_is_missing(value): @@ -1416,6 +1489,13 @@ class Parameter(object): def get_usage_pieces(self, ctx): return [] + def get_error_hint(self, ctx): + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return ' / '.join('"%s"' % x for x in hint_list) + class Option(Parameter): """Options are usually optional values on the command line and @@ -1424,10 +1504,15 @@ class Option(Parameter): All other parameters are passed onwards to the parameter constructor. :param show_default: controls if the default value should be shown on the - help page. Normally, defaults are not shown. - :param prompt: if set to `True` or a non empty string then the user will - be prompted for input if not set. If set to `True` the - prompt will be the option name capitalized. + help page. Normally, defaults are not shown. If this + value is a string, it shows the string instead of the + value. This is particularly useful for dynamic options. + :param show_envvar: controls if an environment variable should be shown on + the help page. Normally, environment variables + are not shown. + :param prompt: if set to `True` or a non empty string then the user will be + prompted for input. If set to `True` the prompt will be the + option name capitalized. :param confirmation_prompt: if set then the value will need to be confirmed if it was prompted for. :param hide_input: if this is `True` then the input on the prompt will be @@ -1448,6 +1533,7 @@ class Option(Parameter): variable in case a prefix is defined on the context. :param help: the help string. + :param hidden: hide this option from help outputs. """ param_type_name = 'option' @@ -1455,7 +1541,8 @@ class Option(Parameter): prompt=False, confirmation_prompt=False, hide_input=False, is_flag=None, flag_value=None, multiple=False, count=False, allow_from_autoenv=True, - type=None, help=None, **attrs): + type=None, help=None, hidden=False, show_choices=True, + show_envvar=False, **attrs): default_is_missing = attrs.get('default', _missing) is _missing Parameter.__init__(self, param_decls, type=type, **attrs) @@ -1468,6 +1555,7 @@ class Option(Parameter): self.prompt = prompt_text self.confirmation_prompt = confirmation_prompt self.hide_input = hide_input + self.hidden = hidden # Flags if is_flag is None: @@ -1500,6 +1588,8 @@ class Option(Parameter): self.allow_from_autoenv = allow_from_autoenv self.help = help self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar # Sanity check for stuff we don't support if __debug__: @@ -1548,8 +1638,8 @@ class Option(Parameter): opts.append(decl) if name is None and possible_names: - possible_names.sort(key=lambda x: len(x[0])) - name = possible_names[-1][1].replace('-', '_').lower() + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace('-', '_').lower() if not isidentifier(name): name = None @@ -1595,6 +1685,8 @@ class Option(Parameter): parser.add_option(self.opts, **kwargs) def get_help_record(self, ctx): + if self.hidden: + return any_prefix_is_slash = [] def _write_opts(opts): @@ -1611,11 +1703,28 @@ class Option(Parameter): help = self.help or '' extra = [] + if self.show_envvar: + envvar = self.envvar + if envvar is None: + if self.allow_from_autoenv and \ + ctx.auto_envvar_prefix is not None: + envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper()) + if envvar is not None: + extra.append('env var: %s' % ( + ', '.join('%s' % d for d in envvar) + if isinstance(envvar, (list, tuple)) + else envvar, )) if self.default is not None and self.show_default: - extra.append('default: %s' % ( - ', '.join('%s' % d for d in self.default) - if isinstance(self.default, (list, tuple)) - else self.default, )) + if isinstance(self.show_default, string_types): + default_string = '({})'.format(self.show_default) + elif isinstance(self.default, (list, tuple)): + default_string = ', '.join('%s' % d for d in self.default) + elif inspect.isfunction(self.default): + default_string = "(dynamic)" + else: + default_string = self.default + extra.append('default: {}'.format(default_string)) + if self.required: extra.append('required') if extra: @@ -1649,8 +1758,8 @@ class Option(Parameter): if self.is_bool_flag: return confirm(self.prompt, default) - return prompt(self.prompt, default=default, - hide_input=self.hide_input, + return prompt(self.prompt, default=default, type=self.type, + hide_input=self.hide_input, show_choices=self.show_choices, confirmation_prompt=self.confirmation_prompt, value_proc=lambda x: self.process_value(ctx, x)) @@ -1710,7 +1819,9 @@ class Argument(Parameter): def make_metavar(self): if self.metavar is not None: return self.metavar - var = self.name.upper() + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() if not self.required: var = '[%s]' % var if self.nargs != 1: @@ -1725,16 +1836,17 @@ class Argument(Parameter): if len(decls) == 1: name = arg = decls[0] name = name.replace('-', '_').lower() - elif len(decls) == 2: - name, arg = decls else: - raise TypeError('Arguments take exactly one or two ' - 'parameter declarations, got %d' % len(decls)) + raise TypeError('Arguments take exactly one ' + 'parameter declaration, got %d' % len(decls)) return name, [arg], [] def get_usage_pieces(self, ctx): return [self.make_metavar()] + def get_error_hint(self, ctx): + return '"%s"' % self.make_metavar() + def add_to_parser(self, parser, ctx): parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/libs/click/decorators.py b/libs/click/decorators.py index 989345265..c57c53086 100644 --- a/libs/click/decorators.py +++ b/libs/click/decorators.py @@ -61,7 +61,7 @@ def make_pass_decorator(object_type, ensure=False): raise RuntimeError('Managed to invoke callback without a ' 'context object of type %r existing' % object_type.__name__) - return ctx.invoke(f, obj, *args[1:], **kwargs) + return ctx.invoke(f, obj, *args, **kwargs) return update_wrapper(new_func, f) return decorator @@ -85,12 +85,12 @@ def _make_command(f, name, attrs, cls): help = inspect.cleandoc(help) attrs['help'] = help _check_for_unicode_literals() - return cls(name=name or f.__name__.lower(), + return cls(name=name or f.__name__.lower().replace('_', '-'), callback=f, params=params, **attrs) def command(name=None, cls=None, **attrs): - """Creates a new :class:`Command` and uses the decorated function as + r"""Creates a new :class:`Command` and uses the decorated function as callback. This will also automatically attach all decorated :func:`option`\s and :func:`argument`\s as parameters to the command. @@ -105,7 +105,7 @@ def command(name=None, cls=None, **attrs): command :class:`Group`. :param name: the name of the command. This defaults to the function - name. + name with underscores replaced by dashes. :param cls: the command class to instantiate. This defaults to :class:`Command`. """ @@ -164,10 +164,13 @@ def option(*param_decls, **attrs): :class:`Option`. """ def decorator(f): - if 'help' in attrs: - attrs['help'] = inspect.cleandoc(attrs['help']) - OptionClass = attrs.pop('cls', Option) - _param_memo(f, OptionClass(param_decls, **attrs)) + # Issue 926, copy attrs, so pre-defined options can re-use the same cls= + option_attrs = attrs.copy() + + if 'help' in option_attrs: + option_attrs['help'] = inspect.cleandoc(option_attrs['help']) + OptionClass = option_attrs.pop('cls', Option) + _param_memo(f, OptionClass(param_decls, **option_attrs)) return f return decorator @@ -235,7 +238,11 @@ def version_option(version=None, *param_decls, **attrs): :param others: everything else is forwarded to :func:`option`. """ if version is None: - module = sys._getframe(1).f_globals.get('__name__') + if hasattr(sys, '_getframe'): + module = sys._getframe(1).f_globals.get('__name__') + else: + module = '' + def decorator(f): prog_name = attrs.pop('prog_name', None) message = attrs.pop('message', '%(prog)s, version %(version)s') diff --git a/libs/click/exceptions.py b/libs/click/exceptions.py index 74a4542bb..6fa17658c 100644 --- a/libs/click/exceptions.py +++ b/libs/click/exceptions.py @@ -2,6 +2,12 @@ from ._compat import PY2, filename_to_ui, get_text_stderr from .utils import echo +def _join_param_hints(param_hint): + if isinstance(param_hint, (tuple, list)): + return ' / '.join('"%s"' % x for x in param_hint) + return param_hint + + class ClickException(Exception): """An exception that Click can handle and show to the user.""" @@ -9,15 +15,25 @@ class ClickException(Exception): exit_code = 1 def __init__(self, message): + ctor_msg = message if PY2: - if message is not None: - message = message.encode('utf-8') - Exception.__init__(self, message) + if ctor_msg is not None: + ctor_msg = ctor_msg.encode('utf-8') + Exception.__init__(self, ctor_msg) self.message = message def format_message(self): return self.message + def __str__(self): + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.message.encode('utf-8') + def show(self, file=None): if file is None: file = get_text_stderr() @@ -37,14 +53,20 @@ class UsageError(ClickException): def __init__(self, message, ctx=None): ClickException.__init__(self, message) self.ctx = ctx + self.cmd = self.ctx and self.ctx.command or None def show(self, file=None): if file is None: file = get_text_stderr() color = None + hint = '' + if (self.cmd is not None and + self.cmd.get_help_option(self.ctx) is not None): + hint = ('Try "%s %s" for help.\n' + % (self.ctx.command_path, self.ctx.help_option_names[0])) if self.ctx is not None: color = self.ctx.color - echo(self.ctx.get_usage() + '\n', file=file, color=color) + echo(self.ctx.get_usage() + '\n%s' % hint, file=file, color=color) echo('Error: %s' % self.format_message(), file=file, color=color) @@ -76,11 +98,11 @@ class BadParameter(UsageError): if self.param_hint is not None: param_hint = self.param_hint elif self.param is not None: - param_hint = self.param.opts or [self.param.human_readable_name] + param_hint = self.param.get_error_hint(self.ctx) else: return 'Invalid value: %s' % self.message - if isinstance(param_hint, (tuple, list)): - param_hint = ' / '.join('"%s"' % x for x in param_hint) + param_hint = _join_param_hints(param_hint) + return 'Invalid value for %s: %s' % (param_hint, self.message) @@ -105,11 +127,10 @@ class MissingParameter(BadParameter): if self.param_hint is not None: param_hint = self.param_hint elif self.param is not None: - param_hint = self.param.opts or [self.param.human_readable_name] + param_hint = self.param.get_error_hint(self.ctx) else: param_hint = None - if isinstance(param_hint, (tuple, list)): - param_hint = ' / '.join('"%s"' % x for x in param_hint) + param_hint = _join_param_hints(param_hint) param_type = self.param_type if param_type is None and self.param is not None: @@ -164,10 +185,13 @@ class BadOptionUsage(UsageError): for an option is not correct. .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. """ - def __init__(self, message, ctx=None): + def __init__(self, option_name, message, ctx=None): UsageError.__init__(self, message, ctx) + self.option_name = option_name class BadArgumentUsage(UsageError): @@ -199,3 +223,13 @@ class FileError(ClickException): class Abort(RuntimeError): """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + def __init__(self, code=0): + self.exit_code = code diff --git a/libs/click/globals.py b/libs/click/globals.py index 14338e6bb..843b594ab 100644 --- a/libs/click/globals.py +++ b/libs/click/globals.py @@ -9,7 +9,7 @@ def get_current_context(silent=False): access the current context object from anywhere. This is a more implicit alternative to the :func:`pass_context` decorator. This function is primarily useful for helpers such as :func:`echo` which might be - interested in changing it's behavior based on the current context. + interested in changing its behavior based on the current context. To push the current context, :meth:`Context.scope` can be used. diff --git a/libs/click/parser.py b/libs/click/parser.py index 9775c9ff9..1c3ae9c8e 100644 --- a/libs/click/parser.py +++ b/libs/click/parser.py @@ -1,20 +1,21 @@ # -*- coding: utf-8 -*- """ - click.parser - ~~~~~~~~~~~~ +click.parser +~~~~~~~~~~~~ - This module started out as largely a copy paste from the stdlib's - optparse module with the features removed that we do not need from - optparse because we implement them in Click on a higher level (for - instance type handling, help formatting and a lot more). +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). - The plan is to remove more and more from here over time. +The plan is to remove more and more from here over time. - The reason this is a different module and not optparse from the stdlib - is that there are differences in 2.x and 3.x about the error messages - generated and optparse in the stdlib uses gettext for no good reason - and might cause us issues. +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. """ + import re from collections import deque from .exceptions import UsageError, NoSuchOption, BadOptionUsage, \ @@ -74,8 +75,8 @@ def _unpack_args(args, nargs_spec): def _error_opt_args(nargs, opt): if nargs == 1: - raise BadOptionUsage('%s option requires an argument' % opt) - raise BadOptionUsage('%s option requires %d arguments' % (opt, nargs)) + raise BadOptionUsage(opt, '%s option requires an argument' % opt) + raise BadOptionUsage(opt, '%s option requires %d arguments' % (opt, nargs)) def split_opt(opt): @@ -321,7 +322,7 @@ class OptionParser(object): if opt not in self._long_opt: possibilities = [word for word in self._long_opt if word.startswith(opt)] - raise NoSuchOption(opt, possibilities=possibilities) + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) option = self._long_opt[opt] if option.takes_value: @@ -342,7 +343,7 @@ class OptionParser(object): del state.rargs[:nargs] elif explicit_value is not None: - raise BadOptionUsage('%s option does not take a value' % opt) + raise BadOptionUsage(opt, '%s option does not take a value' % opt) else: value = None @@ -364,7 +365,7 @@ class OptionParser(object): if self.ignore_unknown_options: unknown_options.append(ch) continue - raise NoSuchOption(opt) + raise NoSuchOption(opt, ctx=self.ctx) if option.takes_value: # Any characters left in arg? Pretend they're the # next arg, and stop consuming characters of arg. diff --git a/libs/click/termui.py b/libs/click/termui.py index d9fba5232..bf9a3aa16 100644 --- a/libs/click/termui.py +++ b/libs/click/termui.py @@ -1,12 +1,14 @@ import os import sys import struct +import inspect +import itertools from ._compat import raw_input, text_type, string_types, \ isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN from .utils import echo from .exceptions import Abort, UsageError -from .types import convert_type +from .types import convert_type, Choice, Path from .globals import resolve_color_default @@ -14,8 +16,25 @@ from .globals import resolve_color_default # functions to customize how they work. visible_prompt_func = raw_input -_ansi_colors = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', - 'cyan', 'white', 'reset') +_ansi_colors = { + 'black': 30, + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'white': 37, + 'reset': 39, + 'bright_black': 90, + 'bright_red': 91, + 'bright_green': 92, + 'bright_yellow': 93, + 'bright_blue': 94, + 'bright_magenta': 95, + 'bright_cyan': 96, + 'bright_white': 97, +} _ansi_reset_all = '\033[0m' @@ -24,23 +43,27 @@ def hidden_prompt_func(prompt): return getpass.getpass(prompt) -def _build_prompt(text, suffix, show_default=False, default=None): +def _build_prompt(text, suffix, show_default=False, default=None, show_choices=True, type=None): prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += ' (' + ", ".join(map(str, type.choices)) + ')' if default is not None and show_default: prompt = '%s [%s]' % (prompt, default) return prompt + suffix -def prompt(text, default=None, hide_input=False, - confirmation_prompt=False, type=None, - value_proc=None, prompt_suffix=': ', - show_default=True, err=False): +def prompt(text, default=None, hide_input=False, confirmation_prompt=False, + type=None, value_proc=None, prompt_suffix=': ', show_default=True, + err=False, show_choices=True): """Prompts a user for input. This is a convenience function that can be used to prompt a user for input later. If the user aborts the input by sending a interrupt signal, this function will catch it and raise a :exc:`Abort` exception. + .. versionadded:: 7.0 + Added the show_choices parameter. + .. versionadded:: 6.0 Added unicode support for cmd.exe on Windows. @@ -61,6 +84,10 @@ def prompt(text, default=None, hide_input=False, :param show_default: shows or hides the default value in the prompt. :param err: if set to true the file defaults to ``stderr`` instead of ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". """ result = None @@ -82,17 +109,18 @@ def prompt(text, default=None, hide_input=False, if value_proc is None: value_proc = convert_type(type, default) - prompt = _build_prompt(text, prompt_suffix, show_default, default) + prompt = _build_prompt(text, prompt_suffix, show_default, default, show_choices, type) while 1: while 1: value = prompt_func(prompt) if value: break - # If a default is set and used, then the confirmation - # prompt is always skipped because that's the only thing - # that really makes sense. elif default is not None: + if isinstance(value_proc, Path): + # validate Path default value(exists, dir_okay etc.) + value = default + break return default try: result = value_proc(value) @@ -166,8 +194,14 @@ def get_terminal_size(): sz = shutil_get_terminal_size() return sz.columns, sz.lines + # We provide a sensible default for get_winterm_size() when being invoked + # inside a subprocess. Without this, it would not provide a useful input. if get_winterm_size is not None: - return get_winterm_size() + size = get_winterm_size() + if size == (0, 0): + return (79, 24) + else: + return size def ioctl_gwinsz(fd): try: @@ -195,22 +229,33 @@ def get_terminal_size(): return int(cr[1]), int(cr[0]) -def echo_via_pager(text, color=None): +def echo_via_pager(text_or_generator, color=None): """This function takes a text and shows it via an environment specific pager on stdout. .. versionchanged:: 3.0 Added the `color` flag. - :param text: the text to page. + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. :param color: controls if the pager supports ANSI colors or not. The default is autodetection. """ color = resolve_color_default(color) - if not isinstance(text, string_types): - text = text_type(text) + + if inspect.isgeneratorfunction(text_or_generator): + i = text_or_generator() + elif isinstance(text_or_generator, string_types): + i = [text_or_generator] + else: + i = iter(text_or_generator) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, string_types) else text_type(el) + for el in i) + from ._termui_impl import pager - return pager(text + '\n', color) + return pager(itertools.chain(text_generator, "\n"), color) def progressbar(iterable=None, length=None, label=None, show_eta=True, @@ -347,10 +392,21 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, * ``magenta`` * ``cyan`` * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` * ``reset`` (reset the color code only) .. versionadded:: 2.0 + .. versionadded:: 7.0 + Added support for bright colors. + :param text: the string to style with ansi codes. :param fg: if provided this will become the foreground color. :param bg: if provided this will become the background color. @@ -369,13 +425,13 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, bits = [] if fg: try: - bits.append('\033[%dm' % (_ansi_colors.index(fg) + 30)) - except ValueError: + bits.append('\033[%dm' % (_ansi_colors[fg])) + except KeyError: raise TypeError('Unknown color %r' % fg) if bg: try: - bits.append('\033[%dm' % (_ansi_colors.index(bg) + 40)) - except ValueError: + bits.append('\033[%dm' % (_ansi_colors[bg] + 10)) + except KeyError: raise TypeError('Unknown color %r' % bg) if bold is not None: bits.append('\033[%dm' % (1 if bold else 22)) @@ -405,7 +461,7 @@ def unstyle(text): return strip_ansi(text) -def secho(text, file=None, nl=True, err=False, color=None, **styles): +def secho(message=None, file=None, nl=True, err=False, color=None, **styles): """This function combines :func:`echo` and :func:`style` into one call. As such the following two calls are the same:: @@ -417,7 +473,9 @@ def secho(text, file=None, nl=True, err=False, color=None, **styles): .. versionadded:: 2.0 """ - return echo(style(text, **styles), file=file, nl=nl, err=err, color=color) + if message is not None: + message = style(message, **styles) + return echo(message, file=file, nl=nl, err=err, color=color) def edit(text=None, editor=None, env=None, require_save=True, @@ -466,7 +524,7 @@ def launch(url, wait=False, locate=False): Examples:: - click.launch('http://click.pocoo.org/') + click.launch('https://click.palletsprojects.com/') click.launch('/my/downloaded/file', locate=True) .. versionadded:: 2.0 @@ -499,6 +557,10 @@ def getchar(echo=False): Note that this will always read from the terminal, even if something is piped into the standard input. + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + .. versionadded:: 2.0 :param echo: if set to `True`, the character read will also show up on @@ -510,6 +572,11 @@ def getchar(echo=False): return f(echo) +def raw_terminal(): + from ._termui_impl import raw_terminal as f + return f() + + def pause(info='Press any key to continue ...', err=False): """This command stops execution and waits for the user to press any key to continue. This is similar to the Windows batch "pause" diff --git a/libs/click/testing.py b/libs/click/testing.py index 4416c7741..1b2924e0b 100644 --- a/libs/click/testing.py +++ b/libs/click/testing.py @@ -3,8 +3,9 @@ import sys import shutil import tempfile import contextlib +import shlex -from ._compat import iteritems, PY2 +from ._compat import iteritems, PY2, string_types # If someone wants to vendor click, we want to ensure the @@ -72,27 +73,44 @@ def make_input_stream(input, charset): class Result(object): """Holds the captured result of an invoked CLI script.""" - def __init__(self, runner, output_bytes, exit_code, exception, - exc_info=None): + def __init__(self, runner, stdout_bytes, stderr_bytes, exit_code, + exception, exc_info=None): #: The runner that created the result self.runner = runner - #: The output as bytes. - self.output_bytes = output_bytes + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or False(y) if not available + self.stderr_bytes = stderr_bytes #: The exit code as integer. self.exit_code = exit_code - #: The exception that happend if one did. + #: The exception that happened if one did. self.exception = exception #: The traceback self.exc_info = exc_info @property def output(self): - """The output as unicode string.""" - return self.output_bytes.decode(self.runner.charset, 'replace') \ + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self): + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, 'replace') \ .replace('\r\n', '\n') + @property + def stderr(self): + """The standard error as unicode string.""" + if not self.stderr_bytes: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, 'replace') \ + .replace('\r\n', '\n') + + def __repr__(self): - return '' % ( + return '<%s %s>' % ( + type(self).__name__, self.exception and repr(self.exception) or 'okay', ) @@ -111,14 +129,21 @@ class CliRunner(object): to stdout. This is useful for showing examples in some circumstances. Note that regular prompts will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently """ - def __init__(self, charset=None, env=None, echo_stdin=False): + def __init__(self, charset=None, env=None, echo_stdin=False, + mix_stderr=True): if charset is None: charset = 'utf-8' self.charset = charset self.env = env or {} self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr def get_default_prog_name(self, cli): """Given a command object it will return the default program name @@ -163,16 +188,27 @@ class CliRunner(object): env = self.make_env(env) if PY2: - sys.stdout = sys.stderr = bytes_output = StringIO() + bytes_output = StringIO() if self.echo_stdin: input = EchoingStdin(input, bytes_output) + sys.stdout = bytes_output + if not self.mix_stderr: + bytes_error = StringIO() + sys.stderr = bytes_error else: bytes_output = io.BytesIO() if self.echo_stdin: input = EchoingStdin(input, bytes_output) input = io.TextIOWrapper(input, encoding=self.charset) - sys.stdout = sys.stderr = io.TextIOWrapper( + sys.stdout = io.TextIOWrapper( bytes_output, encoding=self.charset) + if not self.mix_stderr: + bytes_error = io.BytesIO() + sys.stderr = io.TextIOWrapper( + bytes_error, encoding=self.charset) + + if self.mix_stderr: + sys.stderr = sys.stdout sys.stdin = input @@ -196,6 +232,7 @@ class CliRunner(object): return char default_color = color + def should_strip_ansi(stream=None, color=None): if color is None: return not default_color @@ -221,7 +258,7 @@ class CliRunner(object): pass else: os.environ[key] = value - yield bytes_output + yield (bytes_output, not self.mix_stderr and bytes_error) finally: for key, value in iteritems(old_env): if value is None: @@ -241,7 +278,7 @@ class CliRunner(object): clickpkg.formatting.FORCED_WIDTH = old_forced_width def invoke(self, cli, args=None, input=None, env=None, - catch_exceptions=True, color=False, **extra): + catch_exceptions=True, color=False, mix_stderr=False, **extra): """Invokes a command in an isolated environment. The arguments are forwarded directly to the command line script, the `extra` keyword arguments are passed to the :meth:`~clickpkg.Command.main` function of @@ -260,7 +297,10 @@ class CliRunner(object): The ``color`` parameter was added. :param cli: the command to invoke - :param args: the arguments to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. :param input: the input data for `sys.stdin`. :param env: the environment overrides. :param catch_exceptions: Whether to catch any other exceptions than @@ -270,36 +310,48 @@ class CliRunner(object): application can still override this explicitly. """ exc_info = None - with self.isolation(input=input, env=env, color=color) as out: + with self.isolation(input=input, env=env, color=color) as outstreams: exception = None exit_code = 0 + if isinstance(args, string_types): + args = shlex.split(args) + try: - cli.main(args=args or (), - prog_name=self.get_default_prog_name(cli), **extra) + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + cli.main(args=args or (), prog_name=prog_name, **extra) except SystemExit as e: - if e.code != 0: + exc_info = sys.exc_info() + exit_code = e.code + if exit_code is None: + exit_code = 0 + + if exit_code != 0: exception = e - exc_info = sys.exc_info() - - exit_code = e.code if not isinstance(exit_code, int): sys.stdout.write(str(exit_code)) sys.stdout.write('\n') exit_code = 1 + except Exception as e: if not catch_exceptions: raise exception = e - exit_code = -1 + exit_code = 1 exc_info = sys.exc_info() finally: sys.stdout.flush() - output = out.getvalue() + stdout = outstreams[0].getvalue() + stderr = outstreams[1] and outstreams[1].getvalue() return Result(runner=self, - output_bytes=output, + stdout_bytes=stdout, + stderr_bytes=stderr, exit_code=exit_code, exception=exception, exc_info=exc_info) diff --git a/libs/click/types.py b/libs/click/types.py index 36390026d..1f88032f5 100644 --- a/libs/click/types.py +++ b/libs/click/types.py @@ -1,5 +1,6 @@ import os import stat +from datetime import datetime from ._compat import open_stream, text_type, filename_to_ui, \ get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2 @@ -126,34 +127,54 @@ class StringParamType(ParamType): class Choice(ParamType): - """The choice type allows a value to be checked against a fixed set of - supported values. All of these values have to be strings. + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. """ + name = 'choice' - def __init__(self, choices): + def __init__(self, choices, case_sensitive=True): self.choices = choices + self.case_sensitive = case_sensitive def get_metavar(self, param): return '[%s]' % '|'.join(self.choices) def get_missing_message(self, param): - return 'Choose from %s.' % ', '.join(self.choices) + return 'Choose from:\n\t%s.' % ',\n\t'.join(self.choices) def convert(self, value, param, ctx): # Exact match if value in self.choices: return value - # Match through normalization + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = self.choices + if ctx is not None and \ ctx.token_normalize_func is not None: - value = ctx.token_normalize_func(value) - for choice in self.choices: - if ctx.token_normalize_func(choice) == value: - return choice + normed_value = ctx.token_normalize_func(value) + normed_choices = [ctx.token_normalize_func(choice) for choice in + self.choices] + + if not self.case_sensitive: + normed_value = normed_value.lower() + normed_choices = [choice.lower() for choice in normed_choices] + + if normed_value in normed_choices: + return normed_value self.fail('invalid choice: %s. (choose from %s)' % (value, ', '.join(self.choices)), param, ctx) @@ -162,6 +183,59 @@ class Choice(ParamType): return 'Choice(%r)' % list(self.choices) +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + name = 'datetime' + + def __init__(self, formats=None): + self.formats = formats or [ + '%Y-%m-%d', + '%Y-%m-%dT%H:%M:%S', + '%Y-%m-%d %H:%M:%S' + ] + + def get_metavar(self, param): + return '[{}]'.format('|'.join(self.formats)) + + def _try_to_convert_date(self, value, format): + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert(self, value, param, ctx): + # Exact match + for format in self.formats: + dtime = self._try_to_convert_date(value, format) + if dtime: + return dtime + + self.fail( + 'invalid datetime format: {}. (choose from {})'.format( + value, ', '.join(self.formats))) + + def __repr__(self): + return 'DateTime' + + class IntParamType(ParamType): name = 'integer' @@ -214,23 +288,6 @@ class IntRange(IntParamType): return 'IntRange(%r, %r)' % (self.min, self.max) -class BoolParamType(ParamType): - name = 'boolean' - - def convert(self, value, param, ctx): - if isinstance(value, bool): - return bool(value) - value = value.lower() - if value in ('true', '1', 'yes', 'y'): - return True - elif value in ('false', '0', 'no', 'n'): - return False - self.fail('%s is not a valid boolean' % value, param, ctx) - - def __repr__(self): - return 'BOOL' - - class FloatParamType(ParamType): name = 'float' @@ -245,6 +302,62 @@ class FloatParamType(ParamType): return 'FLOAT' +class FloatRange(FloatParamType): + """A parameter that works similar to :data:`click.FLOAT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + name = 'float range' + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = FloatParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if self.min is not None and rv < self.min or \ + self.max is not None and rv > self.max: + if self.min is None: + self.fail('%s is bigger than the maximum valid value ' + '%s.' % (rv, self.max), param, ctx) + elif self.max is None: + self.fail('%s is smaller than the minimum valid value ' + '%s.' % (rv, self.min), param, ctx) + else: + self.fail('%s is not in the valid range of %s to %s.' + % (rv, self.min, self.max), param, ctx) + return rv + + def __repr__(self): + return 'FloatRange(%r, %r)' % (self.min, self.max) + + +class BoolParamType(ParamType): + name = 'boolean' + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ('true', 't', '1', 'yes', 'y'): + return True + elif value in ('false', 'f', '0', 'no', 'n'): + return False + self.fail('%s is not a valid boolean' % value, param, ctx) + + def __repr__(self): + return 'BOOL' + + class UUIDParameterType(ParamType): name = 'uuid' @@ -273,9 +386,12 @@ class File(ParamType): opened in binary mode or for writing. The encoding parameter can be used to force a specific encoding. - The `lazy` flag controls if the file should be opened immediately or - upon first IO. The default is to be non lazy for standard input and - output streams as well as files opened for reading, lazy otherwise. + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. Starting with Click 2.0, files can also be opened atomically in which case all writes go into a separate file in the same folder and upon @@ -358,14 +474,16 @@ class Path(ParamType): :param readable: if true, a readable check is performed. :param resolve_path: if this is true, then the path is fully resolved before the value is passed onwards. This means - that it's absolute and symlinks are resolved. + that it's absolute and symlinks are resolved. It + will not expand a tilde-prefix, as this is + supposed to be done by the shell only. :param allow_dash: If this is set to `True`, a single dash to indicate standard streams is permitted. - :param type: optionally a string type that should be used to - represent the path. The default is `None` which - means the return value will be either bytes or - unicode depending on what makes most sense given the - input data Click deals with. + :param path_type: optionally a string type that should be used to + represent the path. The default is `None` which + means the return value will be either bytes or + unicode depending on what makes most sense given the + input data Click deals with. """ envvar_list_splitter = os.path.pathsep @@ -384,7 +502,7 @@ class Path(ParamType): if self.file_okay and not self.dir_okay: self.name = 'file' self.path_type = 'File' - if self.dir_okay and not self.file_okay: + elif self.dir_okay and not self.file_okay: self.name = 'directory' self.path_type = 'Directory' else: diff --git a/libs/click/utils.py b/libs/click/utils.py index eee626d3f..fc84369fc 100644 --- a/libs/click/utils.py +++ b/libs/click/utils.py @@ -43,6 +43,7 @@ def make_str(value): def make_default_short_help(help, max_length=45): + """Return a condensed version of help string.""" words = help.split() total_length = 0 result = [] @@ -171,7 +172,7 @@ def echo(message=None, file=None, nl=True, err=False, color=None): Primarily it means that you can print binary data as well as Unicode data on both 2.x and 3.x to the given file in the most appropriate way - possible. This is a very carefree function as in that it will try its + possible. This is a very carefree function in that it will try its best to not fail. As of Click 6.0 this includes support for unicode output on the Windows console. @@ -183,7 +184,7 @@ def echo(message=None, file=None, nl=True, err=False, color=None): - hide ANSI codes automatically if the destination file is not a terminal. - .. _colorama: http://pypi.python.org/pypi/colorama + .. _colorama: https://pypi.org/project/colorama/ .. versionchanged:: 6.0 As of Click 6.0 the echo function will properly support unicode @@ -413,3 +414,27 @@ def get_app_dir(app_name, roaming=True, force_posix=False): return os.path.join( os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), _posixify(app_name)) + + +class PacifyFlushWrapper(object): + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + + def flush(self): + try: + self.wrapped.flush() + except IOError as e: + import errno + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr): + return getattr(self.wrapped, attr) diff --git a/libs/markupsafe/__init__.py b/libs/markupsafe/__init__.py new file mode 100644 index 000000000..da05ed328 --- /dev/null +++ b/libs/markupsafe/__init__.py @@ -0,0 +1,327 @@ +# -*- coding: utf-8 -*- +""" +markupsafe +~~~~~~~~~~ + +Implements an escape function and a Markup string to replace HTML +special characters with safe representations. + +:copyright: 2010 Pallets +:license: BSD-3-Clause +""" +import re +import string + +from ._compat import int_types +from ._compat import iteritems +from ._compat import Mapping +from ._compat import PY2 +from ._compat import string_types +from ._compat import text_type +from ._compat import unichr + +__version__ = "1.1.1" + +__all__ = ["Markup", "soft_unicode", "escape", "escape_silent"] + +_striptags_re = re.compile(r"(|<[^>]*>)") +_entity_re = re.compile(r"&([^& ;]+);") + + +class Markup(text_type): + """A string that is ready to be safely inserted into an HTML or XML + document, either because it was escaped or because it was marked + safe. + + Passing an object to the constructor converts it to text and wraps + it to mark it safe without escaping. To escape the text, use the + :meth:`escape` class method instead. + + >>> Markup('Hello, World!') + Markup('Hello, World!') + >>> Markup(42) + Markup('42') + >>> Markup.escape('Hello, World!') + Markup('Hello <em>World</em>!') + + This implements the ``__html__()`` interface that some frameworks + use. Passing an object that implements ``__html__()`` will wrap the + output of that method, marking it safe. + + >>> class Foo: + ... def __html__(self): + ... return 'foo' + ... + >>> Markup(Foo()) + Markup('foo') + + This is a subclass of the text type (``str`` in Python 3, + ``unicode`` in Python 2). It has the same methods as that type, but + all methods escape their arguments and return a ``Markup`` instance. + + >>> Markup('%s') % 'foo & bar' + Markup('foo & bar') + >>> Markup('Hello ') + '' + Markup('Hello <foo>') + """ + + __slots__ = () + + def __new__(cls, base=u"", encoding=None, errors="strict"): + if hasattr(base, "__html__"): + base = base.__html__() + if encoding is None: + return text_type.__new__(cls, base) + return text_type.__new__(cls, base, encoding, errors) + + def __html__(self): + return self + + def __add__(self, other): + if isinstance(other, string_types) or hasattr(other, "__html__"): + return self.__class__(super(Markup, self).__add__(self.escape(other))) + return NotImplemented + + def __radd__(self, other): + if hasattr(other, "__html__") or isinstance(other, string_types): + return self.escape(other).__add__(self) + return NotImplemented + + def __mul__(self, num): + if isinstance(num, int_types): + return self.__class__(text_type.__mul__(self, num)) + return NotImplemented + + __rmul__ = __mul__ + + def __mod__(self, arg): + if isinstance(arg, tuple): + arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg) + else: + arg = _MarkupEscapeHelper(arg, self.escape) + return self.__class__(text_type.__mod__(self, arg)) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, text_type.__repr__(self)) + + def join(self, seq): + return self.__class__(text_type.join(self, map(self.escape, seq))) + + join.__doc__ = text_type.join.__doc__ + + def split(self, *args, **kwargs): + return list(map(self.__class__, text_type.split(self, *args, **kwargs))) + + split.__doc__ = text_type.split.__doc__ + + def rsplit(self, *args, **kwargs): + return list(map(self.__class__, text_type.rsplit(self, *args, **kwargs))) + + rsplit.__doc__ = text_type.rsplit.__doc__ + + def splitlines(self, *args, **kwargs): + return list(map(self.__class__, text_type.splitlines(self, *args, **kwargs))) + + splitlines.__doc__ = text_type.splitlines.__doc__ + + def unescape(self): + """Convert escaped markup back into a text string. This replaces + HTML entities with the characters they represent. + + >>> Markup('Main » About').unescape() + 'Main » About' + """ + from ._constants import HTML_ENTITIES + + def handle_match(m): + name = m.group(1) + if name in HTML_ENTITIES: + return unichr(HTML_ENTITIES[name]) + try: + if name[:2] in ("#x", "#X"): + return unichr(int(name[2:], 16)) + elif name.startswith("#"): + return unichr(int(name[1:])) + except ValueError: + pass + # Don't modify unexpected input. + return m.group() + + return _entity_re.sub(handle_match, text_type(self)) + + def striptags(self): + """:meth:`unescape` the markup, remove tags, and normalize + whitespace to single spaces. + + >>> Markup('Main »\tAbout').striptags() + 'Main » About' + """ + stripped = u" ".join(_striptags_re.sub("", self).split()) + return Markup(stripped).unescape() + + @classmethod + def escape(cls, s): + """Escape a string. Calls :func:`escape` and ensures that for + subclasses the correct type is returned. + """ + rv = escape(s) + if rv.__class__ is not cls: + return cls(rv) + return rv + + def make_simple_escaping_wrapper(name): # noqa: B902 + orig = getattr(text_type, name) + + def func(self, *args, **kwargs): + args = _escape_argspec(list(args), enumerate(args), self.escape) + _escape_argspec(kwargs, iteritems(kwargs), self.escape) + return self.__class__(orig(self, *args, **kwargs)) + + func.__name__ = orig.__name__ + func.__doc__ = orig.__doc__ + return func + + for method in ( + "__getitem__", + "capitalize", + "title", + "lower", + "upper", + "replace", + "ljust", + "rjust", + "lstrip", + "rstrip", + "center", + "strip", + "translate", + "expandtabs", + "swapcase", + "zfill", + ): + locals()[method] = make_simple_escaping_wrapper(method) + + def partition(self, sep): + return tuple(map(self.__class__, text_type.partition(self, self.escape(sep)))) + + def rpartition(self, sep): + return tuple(map(self.__class__, text_type.rpartition(self, self.escape(sep)))) + + def format(self, *args, **kwargs): + formatter = EscapeFormatter(self.escape) + kwargs = _MagicFormatMapping(args, kwargs) + return self.__class__(formatter.vformat(self, args, kwargs)) + + def __html_format__(self, format_spec): + if format_spec: + raise ValueError("Unsupported format specification " "for Markup.") + return self + + # not in python 3 + if hasattr(text_type, "__getslice__"): + __getslice__ = make_simple_escaping_wrapper("__getslice__") + + del method, make_simple_escaping_wrapper + + +class _MagicFormatMapping(Mapping): + """This class implements a dummy wrapper to fix a bug in the Python + standard library for string formatting. + + See http://bugs.python.org/issue13598 for information about why + this is necessary. + """ + + def __init__(self, args, kwargs): + self._args = args + self._kwargs = kwargs + self._last_index = 0 + + def __getitem__(self, key): + if key == "": + idx = self._last_index + self._last_index += 1 + try: + return self._args[idx] + except LookupError: + pass + key = str(idx) + return self._kwargs[key] + + def __iter__(self): + return iter(self._kwargs) + + def __len__(self): + return len(self._kwargs) + + +if hasattr(text_type, "format"): + + class EscapeFormatter(string.Formatter): + def __init__(self, escape): + self.escape = escape + + def format_field(self, value, format_spec): + if hasattr(value, "__html_format__"): + rv = value.__html_format__(format_spec) + elif hasattr(value, "__html__"): + if format_spec: + raise ValueError( + "Format specifier {0} given, but {1} does not" + " define __html_format__. A class that defines" + " __html__ must define __html_format__ to work" + " with format specifiers.".format(format_spec, type(value)) + ) + rv = value.__html__() + else: + # We need to make sure the format spec is unicode here as + # otherwise the wrong callback methods are invoked. For + # instance a byte string there would invoke __str__ and + # not __unicode__. + rv = string.Formatter.format_field(self, value, text_type(format_spec)) + return text_type(self.escape(rv)) + + +def _escape_argspec(obj, iterable, escape): + """Helper for various string-wrapped functions.""" + for key, value in iterable: + if hasattr(value, "__html__") or isinstance(value, string_types): + obj[key] = escape(value) + return obj + + +class _MarkupEscapeHelper(object): + """Helper for Markup.__mod__""" + + def __init__(self, obj, escape): + self.obj = obj + self.escape = escape + + def __getitem__(self, item): + return _MarkupEscapeHelper(self.obj[item], self.escape) + + def __str__(self): + return text_type(self.escape(self.obj)) + + __unicode__ = __str__ + + def __repr__(self): + return str(self.escape(repr(self.obj))) + + def __int__(self): + return int(self.obj) + + def __float__(self): + return float(self.obj) + + +# we have to import it down here as the speedups and native +# modules imports the markup type which is define above. +try: + from ._speedups import escape, escape_silent, soft_unicode +except ImportError: + from ._native import escape, escape_silent, soft_unicode + +if not PY2: + soft_str = soft_unicode + __all__.append("soft_str") diff --git a/libs/markupsafe/_compat.py b/libs/markupsafe/_compat.py new file mode 100644 index 000000000..bc05090f9 --- /dev/null +++ b/libs/markupsafe/_compat.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +markupsafe._compat +~~~~~~~~~~~~~~~~~~ + +:copyright: 2010 Pallets +:license: BSD-3-Clause +""" +import sys + +PY2 = sys.version_info[0] == 2 + +if not PY2: + text_type = str + string_types = (str,) + unichr = chr + int_types = (int,) + + def iteritems(x): + return iter(x.items()) + + from collections.abc import Mapping + +else: + text_type = unicode + string_types = (str, unicode) + unichr = unichr + int_types = (int, long) + + def iteritems(x): + return x.iteritems() + + from collections import Mapping diff --git a/libs/markupsafe/_constants.py b/libs/markupsafe/_constants.py new file mode 100644 index 000000000..7c57c2d29 --- /dev/null +++ b/libs/markupsafe/_constants.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +""" +markupsafe._constants +~~~~~~~~~~~~~~~~~~~~~ + +:copyright: 2010 Pallets +:license: BSD-3-Clause +""" + +HTML_ENTITIES = { + "AElig": 198, + "Aacute": 193, + "Acirc": 194, + "Agrave": 192, + "Alpha": 913, + "Aring": 197, + "Atilde": 195, + "Auml": 196, + "Beta": 914, + "Ccedil": 199, + "Chi": 935, + "Dagger": 8225, + "Delta": 916, + "ETH": 208, + "Eacute": 201, + "Ecirc": 202, + "Egrave": 200, + "Epsilon": 917, + "Eta": 919, + "Euml": 203, + "Gamma": 915, + "Iacute": 205, + "Icirc": 206, + "Igrave": 204, + "Iota": 921, + "Iuml": 207, + "Kappa": 922, + "Lambda": 923, + "Mu": 924, + "Ntilde": 209, + "Nu": 925, + "OElig": 338, + "Oacute": 211, + "Ocirc": 212, + "Ograve": 210, + "Omega": 937, + "Omicron": 927, + "Oslash": 216, + "Otilde": 213, + "Ouml": 214, + "Phi": 934, + "Pi": 928, + "Prime": 8243, + "Psi": 936, + "Rho": 929, + "Scaron": 352, + "Sigma": 931, + "THORN": 222, + "Tau": 932, + "Theta": 920, + "Uacute": 218, + "Ucirc": 219, + "Ugrave": 217, + "Upsilon": 933, + "Uuml": 220, + "Xi": 926, + "Yacute": 221, + "Yuml": 376, + "Zeta": 918, + "aacute": 225, + "acirc": 226, + "acute": 180, + "aelig": 230, + "agrave": 224, + "alefsym": 8501, + "alpha": 945, + "amp": 38, + "and": 8743, + "ang": 8736, + "apos": 39, + "aring": 229, + "asymp": 8776, + "atilde": 227, + "auml": 228, + "bdquo": 8222, + "beta": 946, + "brvbar": 166, + "bull": 8226, + "cap": 8745, + "ccedil": 231, + "cedil": 184, + "cent": 162, + "chi": 967, + "circ": 710, + "clubs": 9827, + "cong": 8773, + "copy": 169, + "crarr": 8629, + "cup": 8746, + "curren": 164, + "dArr": 8659, + "dagger": 8224, + "darr": 8595, + "deg": 176, + "delta": 948, + "diams": 9830, + "divide": 247, + "eacute": 233, + "ecirc": 234, + "egrave": 232, + "empty": 8709, + "emsp": 8195, + "ensp": 8194, + "epsilon": 949, + "equiv": 8801, + "eta": 951, + "eth": 240, + "euml": 235, + "euro": 8364, + "exist": 8707, + "fnof": 402, + "forall": 8704, + "frac12": 189, + "frac14": 188, + "frac34": 190, + "frasl": 8260, + "gamma": 947, + "ge": 8805, + "gt": 62, + "hArr": 8660, + "harr": 8596, + "hearts": 9829, + "hellip": 8230, + "iacute": 237, + "icirc": 238, + "iexcl": 161, + "igrave": 236, + "image": 8465, + "infin": 8734, + "int": 8747, + "iota": 953, + "iquest": 191, + "isin": 8712, + "iuml": 239, + "kappa": 954, + "lArr": 8656, + "lambda": 955, + "lang": 9001, + "laquo": 171, + "larr": 8592, + "lceil": 8968, + "ldquo": 8220, + "le": 8804, + "lfloor": 8970, + "lowast": 8727, + "loz": 9674, + "lrm": 8206, + "lsaquo": 8249, + "lsquo": 8216, + "lt": 60, + "macr": 175, + "mdash": 8212, + "micro": 181, + "middot": 183, + "minus": 8722, + "mu": 956, + "nabla": 8711, + "nbsp": 160, + "ndash": 8211, + "ne": 8800, + "ni": 8715, + "not": 172, + "notin": 8713, + "nsub": 8836, + "ntilde": 241, + "nu": 957, + "oacute": 243, + "ocirc": 244, + "oelig": 339, + "ograve": 242, + "oline": 8254, + "omega": 969, + "omicron": 959, + "oplus": 8853, + "or": 8744, + "ordf": 170, + "ordm": 186, + "oslash": 248, + "otilde": 245, + "otimes": 8855, + "ouml": 246, + "para": 182, + "part": 8706, + "permil": 8240, + "perp": 8869, + "phi": 966, + "pi": 960, + "piv": 982, + "plusmn": 177, + "pound": 163, + "prime": 8242, + "prod": 8719, + "prop": 8733, + "psi": 968, + "quot": 34, + "rArr": 8658, + "radic": 8730, + "rang": 9002, + "raquo": 187, + "rarr": 8594, + "rceil": 8969, + "rdquo": 8221, + "real": 8476, + "reg": 174, + "rfloor": 8971, + "rho": 961, + "rlm": 8207, + "rsaquo": 8250, + "rsquo": 8217, + "sbquo": 8218, + "scaron": 353, + "sdot": 8901, + "sect": 167, + "shy": 173, + "sigma": 963, + "sigmaf": 962, + "sim": 8764, + "spades": 9824, + "sub": 8834, + "sube": 8838, + "sum": 8721, + "sup": 8835, + "sup1": 185, + "sup2": 178, + "sup3": 179, + "supe": 8839, + "szlig": 223, + "tau": 964, + "there4": 8756, + "theta": 952, + "thetasym": 977, + "thinsp": 8201, + "thorn": 254, + "tilde": 732, + "times": 215, + "trade": 8482, + "uArr": 8657, + "uacute": 250, + "uarr": 8593, + "ucirc": 251, + "ugrave": 249, + "uml": 168, + "upsih": 978, + "upsilon": 965, + "uuml": 252, + "weierp": 8472, + "xi": 958, + "yacute": 253, + "yen": 165, + "yuml": 255, + "zeta": 950, + "zwj": 8205, + "zwnj": 8204, +} diff --git a/libs/markupsafe/_native.py b/libs/markupsafe/_native.py new file mode 100644 index 000000000..cd08752cd --- /dev/null +++ b/libs/markupsafe/_native.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +markupsafe._native +~~~~~~~~~~~~~~~~~~ + +Native Python implementation used when the C module is not compiled. + +:copyright: 2010 Pallets +:license: BSD-3-Clause +""" +from . import Markup +from ._compat import text_type + + +def escape(s): + """Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in + the string with HTML-safe sequences. Use this if you need to display + text that might contain such characters in HTML. + + If the object has an ``__html__`` method, it is called and the + return value is assumed to already be safe for HTML. + + :param s: An object to be converted to a string and escaped. + :return: A :class:`Markup` string with the escaped text. + """ + if hasattr(s, "__html__"): + return Markup(s.__html__()) + return Markup( + text_type(s) + .replace("&", "&") + .replace(">", ">") + .replace("<", "<") + .replace("'", "'") + .replace('"', """) + ) + + +def escape_silent(s): + """Like :func:`escape` but treats ``None`` as the empty string. + Useful with optional values, as otherwise you get the string + ``'None'`` when the value is ``None``. + + >>> escape(None) + Markup('None') + >>> escape_silent(None) + Markup('') + """ + if s is None: + return Markup() + return escape(s) + + +def soft_unicode(s): + """Convert an object to a string if it isn't already. This preserves + a :class:`Markup` string rather than converting it back to a basic + string, so it will still be marked as safe and won't be escaped + again. + + >>> value = escape('') + >>> value + Markup('<User 1>') + >>> escape(str(value)) + Markup('&lt;User 1&gt;') + >>> escape(soft_unicode(value)) + Markup('<User 1>') + """ + if not isinstance(s, text_type): + s = text_type(s) + return s diff --git a/libs/markupsafe/_speedups.c b/libs/markupsafe/_speedups.c new file mode 100644 index 000000000..12d2c4a7d --- /dev/null +++ b/libs/markupsafe/_speedups.c @@ -0,0 +1,423 @@ +/** + * markupsafe._speedups + * ~~~~~~~~~~~~~~~~~~~~ + * + * C implementation of escaping for better performance. Used instead of + * the native Python implementation when compiled. + * + * :copyright: 2010 Pallets + * :license: BSD-3-Clause + */ +#include + +#if PY_MAJOR_VERSION < 3 +#define ESCAPED_CHARS_TABLE_SIZE 63 +#define UNICHR(x) (PyUnicode_AS_UNICODE((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL))); + +static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE]; +static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE]; +#endif + +static PyObject* markup; + +static int +init_constants(void) +{ + PyObject *module; + +#if PY_MAJOR_VERSION < 3 + /* mapping of characters to replace */ + escaped_chars_repl['"'] = UNICHR("""); + escaped_chars_repl['\''] = UNICHR("'"); + escaped_chars_repl['&'] = UNICHR("&"); + escaped_chars_repl['<'] = UNICHR("<"); + escaped_chars_repl['>'] = UNICHR(">"); + + /* lengths of those characters when replaced - 1 */ + memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len)); + escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \ + escaped_chars_delta_len['&'] = 4; + escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3; +#endif + + /* import markup type so that we can mark the return value */ + module = PyImport_ImportModule("markupsafe"); + if (!module) + return 0; + markup = PyObject_GetAttrString(module, "Markup"); + Py_DECREF(module); + + return 1; +} + +#if PY_MAJOR_VERSION < 3 +static PyObject* +escape_unicode(PyUnicodeObject *in) +{ + PyUnicodeObject *out; + Py_UNICODE *inp = PyUnicode_AS_UNICODE(in); + const Py_UNICODE *inp_end = PyUnicode_AS_UNICODE(in) + PyUnicode_GET_SIZE(in); + Py_UNICODE *next_escp; + Py_UNICODE *outp; + Py_ssize_t delta=0, erepl=0, delta_len=0; + + /* First we need to figure out how long the escaped string will be */ + while (*(inp) || inp < inp_end) { + if (*inp < ESCAPED_CHARS_TABLE_SIZE) { + delta += escaped_chars_delta_len[*inp]; + erepl += !!escaped_chars_delta_len[*inp]; + } + ++inp; + } + + /* Do we need to escape anything at all? */ + if (!erepl) { + Py_INCREF(in); + return (PyObject*)in; + } + + out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, PyUnicode_GET_SIZE(in) + delta); + if (!out) + return NULL; + + outp = PyUnicode_AS_UNICODE(out); + inp = PyUnicode_AS_UNICODE(in); + while (erepl-- > 0) { + /* look for the next substitution */ + next_escp = inp; + while (next_escp < inp_end) { + if (*next_escp < ESCAPED_CHARS_TABLE_SIZE && + (delta_len = escaped_chars_delta_len[*next_escp])) { + ++delta_len; + break; + } + ++next_escp; + } + + if (next_escp > inp) { + /* copy unescaped chars between inp and next_escp */ + Py_UNICODE_COPY(outp, inp, next_escp-inp); + outp += next_escp - inp; + } + + /* escape 'next_escp' */ + Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len); + outp += delta_len; + + inp = next_escp + 1; + } + if (inp < inp_end) + Py_UNICODE_COPY(outp, inp, PyUnicode_GET_SIZE(in) - (inp - PyUnicode_AS_UNICODE(in))); + + return (PyObject*)out; +} +#else /* PY_MAJOR_VERSION < 3 */ + +#define GET_DELTA(inp, inp_end, delta) \ + while (inp < inp_end) { \ + switch (*inp++) { \ + case '"': \ + case '\'': \ + case '&': \ + delta += 4; \ + break; \ + case '<': \ + case '>': \ + delta += 3; \ + break; \ + } \ + } + +#define DO_ESCAPE(inp, inp_end, outp) \ + { \ + Py_ssize_t ncopy = 0; \ + while (inp < inp_end) { \ + switch (*inp) { \ + case '"': \ + memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ + outp += ncopy; ncopy = 0; \ + *outp++ = '&'; \ + *outp++ = '#'; \ + *outp++ = '3'; \ + *outp++ = '4'; \ + *outp++ = ';'; \ + break; \ + case '\'': \ + memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ + outp += ncopy; ncopy = 0; \ + *outp++ = '&'; \ + *outp++ = '#'; \ + *outp++ = '3'; \ + *outp++ = '9'; \ + *outp++ = ';'; \ + break; \ + case '&': \ + memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ + outp += ncopy; ncopy = 0; \ + *outp++ = '&'; \ + *outp++ = 'a'; \ + *outp++ = 'm'; \ + *outp++ = 'p'; \ + *outp++ = ';'; \ + break; \ + case '<': \ + memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ + outp += ncopy; ncopy = 0; \ + *outp++ = '&'; \ + *outp++ = 'l'; \ + *outp++ = 't'; \ + *outp++ = ';'; \ + break; \ + case '>': \ + memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ + outp += ncopy; ncopy = 0; \ + *outp++ = '&'; \ + *outp++ = 'g'; \ + *outp++ = 't'; \ + *outp++ = ';'; \ + break; \ + default: \ + ncopy++; \ + } \ + inp++; \ + } \ + memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ + } + +static PyObject* +escape_unicode_kind1(PyUnicodeObject *in) +{ + Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in); + Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in); + Py_UCS1 *outp; + PyObject *out; + Py_ssize_t delta = 0; + + GET_DELTA(inp, inp_end, delta); + if (!delta) { + Py_INCREF(in); + return (PyObject*)in; + } + + out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, + PyUnicode_IS_ASCII(in) ? 127 : 255); + if (!out) + return NULL; + + inp = PyUnicode_1BYTE_DATA(in); + outp = PyUnicode_1BYTE_DATA(out); + DO_ESCAPE(inp, inp_end, outp); + return out; +} + +static PyObject* +escape_unicode_kind2(PyUnicodeObject *in) +{ + Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in); + Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in); + Py_UCS2 *outp; + PyObject *out; + Py_ssize_t delta = 0; + + GET_DELTA(inp, inp_end, delta); + if (!delta) { + Py_INCREF(in); + return (PyObject*)in; + } + + out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535); + if (!out) + return NULL; + + inp = PyUnicode_2BYTE_DATA(in); + outp = PyUnicode_2BYTE_DATA(out); + DO_ESCAPE(inp, inp_end, outp); + return out; +} + + +static PyObject* +escape_unicode_kind4(PyUnicodeObject *in) +{ + Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in); + Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in); + Py_UCS4 *outp; + PyObject *out; + Py_ssize_t delta = 0; + + GET_DELTA(inp, inp_end, delta); + if (!delta) { + Py_INCREF(in); + return (PyObject*)in; + } + + out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111); + if (!out) + return NULL; + + inp = PyUnicode_4BYTE_DATA(in); + outp = PyUnicode_4BYTE_DATA(out); + DO_ESCAPE(inp, inp_end, outp); + return out; +} + +static PyObject* +escape_unicode(PyUnicodeObject *in) +{ + if (PyUnicode_READY(in)) + return NULL; + + switch (PyUnicode_KIND(in)) { + case PyUnicode_1BYTE_KIND: + return escape_unicode_kind1(in); + case PyUnicode_2BYTE_KIND: + return escape_unicode_kind2(in); + case PyUnicode_4BYTE_KIND: + return escape_unicode_kind4(in); + } + assert(0); /* shouldn't happen */ + return NULL; +} +#endif /* PY_MAJOR_VERSION < 3 */ + +static PyObject* +escape(PyObject *self, PyObject *text) +{ + static PyObject *id_html; + PyObject *s = NULL, *rv = NULL, *html; + + if (id_html == NULL) { +#if PY_MAJOR_VERSION < 3 + id_html = PyString_InternFromString("__html__"); +#else + id_html = PyUnicode_InternFromString("__html__"); +#endif + if (id_html == NULL) { + return NULL; + } + } + + /* we don't have to escape integers, bools or floats */ + if (PyLong_CheckExact(text) || +#if PY_MAJOR_VERSION < 3 + PyInt_CheckExact(text) || +#endif + PyFloat_CheckExact(text) || PyBool_Check(text) || + text == Py_None) + return PyObject_CallFunctionObjArgs(markup, text, NULL); + + /* if the object has an __html__ method that performs the escaping */ + html = PyObject_GetAttr(text ,id_html); + if (html) { + s = PyObject_CallObject(html, NULL); + Py_DECREF(html); + if (s == NULL) { + return NULL; + } + /* Convert to Markup object */ + rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL); + Py_DECREF(s); + return rv; + } + + /* otherwise make the object unicode if it isn't, then escape */ + PyErr_Clear(); + if (!PyUnicode_Check(text)) { +#if PY_MAJOR_VERSION < 3 + PyObject *unicode = PyObject_Unicode(text); +#else + PyObject *unicode = PyObject_Str(text); +#endif + if (!unicode) + return NULL; + s = escape_unicode((PyUnicodeObject*)unicode); + Py_DECREF(unicode); + } + else + s = escape_unicode((PyUnicodeObject*)text); + + /* convert the unicode string into a markup object. */ + rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL); + Py_DECREF(s); + return rv; +} + + +static PyObject* +escape_silent(PyObject *self, PyObject *text) +{ + if (text != Py_None) + return escape(self, text); + return PyObject_CallFunctionObjArgs(markup, NULL); +} + + +static PyObject* +soft_unicode(PyObject *self, PyObject *s) +{ + if (!PyUnicode_Check(s)) +#if PY_MAJOR_VERSION < 3 + return PyObject_Unicode(s); +#else + return PyObject_Str(s); +#endif + Py_INCREF(s); + return s; +} + + +static PyMethodDef module_methods[] = { + {"escape", (PyCFunction)escape, METH_O, + "escape(s) -> markup\n\n" + "Convert the characters &, <, >, ', and \" in string s to HTML-safe\n" + "sequences. Use this if you need to display text that might contain\n" + "such characters in HTML. Marks return value as markup string."}, + {"escape_silent", (PyCFunction)escape_silent, METH_O, + "escape_silent(s) -> markup\n\n" + "Like escape but converts None to an empty string."}, + {"soft_unicode", (PyCFunction)soft_unicode, METH_O, + "soft_unicode(object) -> string\n\n" + "Make a string unicode if it isn't already. That way a markup\n" + "string is not converted back to unicode."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + + +#if PY_MAJOR_VERSION < 3 + +#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ +#define PyMODINIT_FUNC void +#endif +PyMODINIT_FUNC +init_speedups(void) +{ + if (!init_constants()) + return; + + Py_InitModule3("markupsafe._speedups", module_methods, ""); +} + +#else /* Python 3.x module initialization */ + +static struct PyModuleDef module_definition = { + PyModuleDef_HEAD_INIT, + "markupsafe._speedups", + NULL, + -1, + module_methods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit__speedups(void) +{ + if (!init_constants()) + return NULL; + + return PyModule_Create(&module_definition); +} + +#endif diff --git a/views/settings_subtitles.html b/views/settings_subtitles.html index 599f4f7a3..8524527b7 100644 --- a/views/settings_subtitles.html +++ b/views/settings_subtitles.html @@ -433,6 +433,13 @@ +
+
+
We don't recommend enabling this option unless absolutely required (ie: + media player not supporting language code in subtitles filename). Results may vary. +
+
+
@@ -445,12 +452,12 @@ class="ui fluid search selection dropdown"> {% set enabled_languages = [] %} - {%for language in settings_languages%} - - {%if language['enabled'] == True%} - {{ enabled_languages.append(language['code2']|string)}} - {%endif%} - {%endfor%} + {% for language in settings_languages %} + + {% if language['enabled'] == True %} + {{ enabled_languages.append(language['code2']|string) }} + {% endif %} + {% endfor %}