mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-24 06:37:16 -04:00
More work
This commit is contained in:
parent
ae6bd5269c
commit
9e0a530af6
21 changed files with 2327 additions and 414 deletions
114
bazarr/main.py
114
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/<path:url>', 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/<path:url>', 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/<query>', 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/<int:no>', 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/<int:no>', 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/<int:no>', 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/<int:no>', 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/<int:no>', 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/<int:no>', 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/<int:no>', 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/<int:no>', 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/<taskid>')
|
||||
# @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/<protocol>/<path: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/<protocol>/<path:provider>', 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/<int:no>')
|
||||
# @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/<int:no>')
|
||||
# @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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
``"_<prog_name>_COMPLETE"`` with prog name in
|
||||
``"_<prog_name>_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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 '<Result %s>' % (
|
||||
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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
327
libs/markupsafe/__init__.py
Normal file
327
libs/markupsafe/__init__.py
Normal file
|
@ -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, <em>World</em>!')
|
||||
Markup('Hello, <em>World</em>!')
|
||||
>>> Markup(42)
|
||||
Markup('42')
|
||||
>>> Markup.escape('Hello, <em>World</em>!')
|
||||
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 '<a href="/foo">foo</a>'
|
||||
...
|
||||
>>> Markup(Foo())
|
||||
Markup('<a href="/foo">foo</a>')
|
||||
|
||||
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('<em>%s</em>') % 'foo & bar'
|
||||
Markup('<em>foo & bar</em>')
|
||||
>>> Markup('<em>Hello</em> ') + '<foo>'
|
||||
Markup('<em>Hello</em> <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 » <em>About</em>').unescape()
|
||||
'Main » <em>About</em>'
|
||||
"""
|
||||
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 »\t<em>About</em>').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")
|
33
libs/markupsafe/_compat.py
Normal file
33
libs/markupsafe/_compat.py
Normal file
|
@ -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
|
264
libs/markupsafe/_constants.py
Normal file
264
libs/markupsafe/_constants.py
Normal file
|
@ -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,
|
||||
}
|
69
libs/markupsafe/_native.py
Normal file
69
libs/markupsafe/_native.py
Normal file
|
@ -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('<User 1>')
|
||||
>>> 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
|
423
libs/markupsafe/_speedups.c
Normal file
423
libs/markupsafe/_speedups.c
Normal file
|
@ -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 <Python.h>
|
||||
|
||||
#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
|
|
@ -433,6 +433,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ten wide column">
|
||||
<div class="fluid column">
|
||||
<div style="color: red;">We don't recommend enabling this option unless absolutely required (ie:
|
||||
media player not supporting language code in subtitles filename). Results may vary.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned row">
|
||||
|
@ -445,12 +452,12 @@
|
|||
class="ui fluid search selection dropdown">
|
||||
<option value="">Languages</option>
|
||||
{% set enabled_languages = [] %}
|
||||
{%for language in settings_languages%}
|
||||
<option value="{{ language['code2'] }}">{{ language['name'] }}</option>
|
||||
{%if language['enabled'] == True%}
|
||||
{{ enabled_languages.append(language['code2']|string)}}
|
||||
{%endif%}
|
||||
{%endfor%}
|
||||
{% for language in settings_languages %}
|
||||
<option value="{{ language['code2'] }}">{{ language['name'] }}</option>
|
||||
{% if language['enabled'] == True %}
|
||||
{{ enabled_languages.append(language['code2']|string) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue