More work

This commit is contained in:
Halali 2019-12-10 21:09:51 +01:00
parent ae6bd5269c
commit 9e0a530af6
21 changed files with 2327 additions and 414 deletions

View File

@ -145,12 +145,6 @@ def check_credentials(user, pw):
return False 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(): def api_authorize():
if 'apikey' in request.GET.dict: if 'apikey' in request.GET.dict:
if request.GET.dict['apikey'][0] == settings.auth.apikey: if request.GET.dict['apikey'][0] == settings.auth.apikey:
@ -198,14 +192,14 @@ def logout():
# @app.route('/') # @app.route('/')
# # @custom_auth_basic(check_credentials) # # @custom_auth_basic(check_credentials)
# def redirect_root(): # def redirect_root():
# authorize() #
# redirect(base_url) # redirect(base_url)
@app.route(base_url + 'shutdown/') @app.route(base_url + 'shutdown/')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def shutdown(): def shutdown():
authorize()
try: try:
server.stop() server.stop()
except: except:
@ -225,7 +219,7 @@ def shutdown():
@app.route(base_url + 'restart/') @app.route(base_url + 'restart/')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def restart(): def restart():
authorize()
try: try:
server.stop() server.stop()
except: except:
@ -246,7 +240,7 @@ def restart():
@app.route(base_url + 'wizard/') @app.route(base_url + 'wizard/')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def wizard(): def wizard():
authorize()
# Get languages list # Get languages list
settings_languages = database.execute("SELECT * FROM table_settings_languages ORDER BY name") 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']) @app.route(base_url + 'save_wizard', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def save_wizard(): def save_wizard():
authorize()
settings_general_ip = request.form.get('settings_general_ip') settings_general_ip = request.form.get('settings_general_ip')
settings_general_port = request.form.get('settings_general_port') settings_general_port = request.form.get('settings_general_port')
@ -477,7 +471,7 @@ def save_wizard():
@app.route(base_url + 'emptylog') @app.route(base_url + 'emptylog')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def emptylog(): def emptylog():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
empty_log() empty_log()
@ -489,14 +483,14 @@ def emptylog():
@app.route(base_url + 'bazarr.log') @app.route(base_url + 'bazarr.log')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def download_log(): def download_log():
authorize()
return static_file('bazarr.log', root=os.path.join(args.config_dir, 'log/'), download='bazarr.log') 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']) @app.route(base_url + 'image_proxy/<path:url>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def image_proxy(url): def image_proxy(url):
authorize()
apikey = settings.sonarr.apikey apikey = settings.sonarr.apikey
url_image = url_sonarr_short() + '/' + url + '?apikey=' + apikey url_image = url_sonarr_short() + '/' + url + '?apikey=' + apikey
try: try:
@ -514,7 +508,7 @@ def image_proxy(url):
@app.route(base_url + 'image_proxy_movies/<path:url>', methods=['GET']) @app.route(base_url + 'image_proxy_movies/<path:url>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def image_proxy_movies(url): def image_proxy_movies(url):
authorize()
apikey = settings.radarr.apikey apikey = settings.radarr.apikey
try: try:
url_image = (url_radarr_short() + '/' + url + '?apikey=' + apikey).replace('/fanart.jpg', '/banner.jpg') 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) @app.route(base_url)
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def redirect_root(): def redirect_root():
authorize()
if settings.general.getboolean('use_sonarr'): if settings.general.getboolean('use_sonarr'):
return redirect(base_url + 'series') return redirect(base_url + 'series')
elif settings.general.getboolean('use_radarr'): elif settings.general.getboolean('use_radarr'):
@ -548,7 +542,7 @@ def redirect_root():
@app.route(base_url + 'series/') @app.route(base_url + 'series/')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def series(): def series():
authorize()
series_count = database.execute("SELECT COUNT(*) as count FROM table_shows", only_one=True)['count'] series_count = database.execute("SELECT COUNT(*) as count FROM table_shows", only_one=True)['count']
page = request.data page = request.data
@ -605,7 +599,7 @@ def series():
@app.route(base_url + 'serieseditor/') @app.route(base_url + 'serieseditor/')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def serieseditor(): def serieseditor():
authorize()
# Get missing count # Get missing count
missing_count = database.execute("SELECT COUNT(*) as count FROM table_shows", only_one=True)['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']) @app.route(base_url + 'search_json/<query>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def search_json(query): def search_json(query):
authorize()
query = '%' + query + '%' query = '%' + query + '%'
search_list = [] search_list = []
@ -655,7 +649,7 @@ def search_json(query):
@app.route(base_url + 'edit_series/<int:no>', methods=['POST']) @app.route(base_url + 'edit_series/<int:no>', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def edit_series(no): def edit_series(no):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
lang = request.form.getlist('languages') lang = request.form.getlist('languages')
@ -693,7 +687,7 @@ def edit_series(no):
@app.route(base_url + 'edit_serieseditor', methods=['POST']) @app.route(base_url + 'edit_serieseditor', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def edit_serieseditor(): def edit_serieseditor():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
series = request.form.get('series') series = request.form.get('series')
@ -723,7 +717,7 @@ def edit_serieseditor():
@app.route(base_url + 'episodes/<int:no>', methods=['GET']) @app.route(base_url + 'episodes/<int:no>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def episodes(no): def episodes(no):
authorize()
series_details = database.execute("SELECT title, overview, poster, fanart, hearing_impaired, tvdbId, " series_details = database.execute("SELECT title, overview, poster, fanart, hearing_impaired, tvdbId, "
"audio_language, languages, path, forced FROM table_shows WHERE " "audio_language, languages, path, forced FROM table_shows WHERE "
@ -755,7 +749,7 @@ def episodes(no):
@app.route(base_url + 'movies') @app.route(base_url + 'movies')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def movies(): def movies():
authorize()
missing_count = database.execute("SELECT COUNT(*) as count FROM table_movies", only_one=True)['count'] missing_count = database.execute("SELECT COUNT(*) as count FROM table_movies", only_one=True)['count']
page = request.data page = request.data
@ -782,7 +776,7 @@ def movies():
@app.route(base_url + 'movieseditor') @app.route(base_url + 'movieseditor')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def movieseditor(): def movieseditor():
authorize()
missing_count = database.execute("SELECT COUNT(*) as count FROM table_movies", only_one=True)['count'] 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']) @app.route(base_url + 'edit_movieseditor', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def edit_movieseditor(): def edit_movieseditor():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
movies = request.form.get('movies') movies = request.form.get('movies')
@ -831,7 +825,7 @@ def edit_movieseditor():
@app.route(base_url + 'edit_movie/<int:no>', methods=['POST']) @app.route(base_url + 'edit_movie/<int:no>', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def edit_movie(no): def edit_movie(no):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
lang = request.form.getlist('languages') lang = request.form.getlist('languages')
@ -869,7 +863,7 @@ def edit_movie(no):
@app.route(base_url + 'movie/<int:no>', methods=['GET']) @app.route(base_url + 'movie/<int:no>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def movie(no): def movie(no):
authorize()
movies_details = database.execute("SELECT title, overview, poster, fanart, hearing_impaired, tmdbId, " movies_details = database.execute("SELECT title, overview, poster, fanart, hearing_impaired, tmdbId, "
"audio_language, languages, path, subtitles, radarrId, missing_subtitles, " "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']) @app.route(base_url + 'scan_disk/<int:no>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def scan_disk(no): def scan_disk(no):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
series_scan_subtitles(no) series_scan_subtitles(no)
@ -901,7 +895,7 @@ def scan_disk(no):
@app.route(base_url + 'scan_disk_movie/<int:no>', methods=['GET']) @app.route(base_url + 'scan_disk_movie/<int:no>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def scan_disk_movie(no): def scan_disk_movie(no):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
movies_scan_subtitles(no) movies_scan_subtitles(no)
@ -912,7 +906,7 @@ def scan_disk_movie(no):
@app.route(base_url + 'search_missing_subtitles/<int:no>', methods=['GET']) @app.route(base_url + 'search_missing_subtitles/<int:no>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def search_missing_subtitles(no): def search_missing_subtitles(no):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
add_job(series_download_subtitles, args=[no], name=('search_missing_subtitles_' + str(no))) 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']) @app.route(base_url + 'search_missing_subtitles_movie/<int:no>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def search_missing_subtitles_movie(no): def search_missing_subtitles_movie(no):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
add_job(movies_download_subtitles, args=[no], name=('movies_download_subtitles_' + str(no))) 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') @app.route(base_url + 'history')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def history(): def history():
authorize()
return render_template('history', bazarr_version=bazarr_version, base_url=base_url, current_port=settings.general.port) return render_template('history', bazarr_version=bazarr_version, base_url=base_url, current_port=settings.general.port)
@app.route(base_url + 'historyseries') @app.route(base_url + 'historyseries')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def historyseries(): def historyseries():
authorize()
row_count = database.execute("SELECT COUNT(*) as count FROM table_history LEFT JOIN table_shows on " row_count = database.execute("SELECT COUNT(*) as count FROM table_history LEFT JOIN table_shows on "
"table_history.sonarrSeriesId = table_shows.sonarrSeriesId WHERE " "table_history.sonarrSeriesId = table_shows.sonarrSeriesId WHERE "
@ -1020,7 +1014,7 @@ def historyseries():
@app.route(base_url + 'historymovies') @app.route(base_url + 'historymovies')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def historymovies(): def historymovies():
authorize()
row_count = database.execute("SELECT COUNT(*) as count FROM table_history_movie LEFT JOIN table_movies ON " row_count = database.execute("SELECT COUNT(*) as count FROM table_history_movie LEFT JOIN table_movies ON "
"table_history_movie.radarrId=table_movies.radarrId " "table_history_movie.radarrId=table_movies.radarrId "
@ -1097,14 +1091,14 @@ def historymovies():
@app.route(base_url + 'wanted') @app.route(base_url + 'wanted')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def wanted(): def wanted():
authorize()
return render_template('wanted', bazarr_version=bazarr_version, base_url=base_url, current_port=settings.general.port) return render_template('wanted', bazarr_version=bazarr_version, base_url=base_url, current_port=settings.general.port)
@app.route(base_url + 'wantedseries') @app.route(base_url + 'wantedseries')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def wantedseries(): def wantedseries():
authorize()
if settings.sonarr.getboolean('only_monitored'): if settings.sonarr.getboolean('only_monitored'):
monitored_only_query_string = " AND monitored='True'" monitored_only_query_string = " AND monitored='True'"
@ -1138,7 +1132,7 @@ def wantedseries():
@app.route(base_url + 'wantedmovies') @app.route(base_url + 'wantedmovies')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def wantedmovies(): def wantedmovies():
authorize()
if settings.radarr.getboolean('only_monitored'): if settings.radarr.getboolean('only_monitored'):
monitored_only_query_string = " AND monitored='True'" monitored_only_query_string = " AND monitored='True'"
@ -1169,7 +1163,7 @@ def wantedmovies():
@app.route(base_url + 'wanted_search_missing_subtitles') @app.route(base_url + 'wanted_search_missing_subtitles')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def wanted_search_missing_subtitles_list(): def wanted_search_missing_subtitles_list():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
add_job(wanted_search_missing_subtitles, name='manual_wanted_search_missing_subtitles') 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/') @app.route(base_url + 'settings/')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def _settings(): def _settings():
authorize()
settings_languages = database.execute("SELECT * FROM table_settings_languages ORDER BY name") settings_languages = database.execute("SELECT * FROM table_settings_languages ORDER BY name")
settings_providers = sorted(provider_manager.names()) settings_providers = sorted(provider_manager.names())
@ -1194,7 +1188,7 @@ def _settings():
@app.route(base_url + 'save_settings', methods=['POST']) @app.route(base_url + 'save_settings', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def save_settings(): def save_settings():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
settings_general_ip = request.form.get('settings_general_ip') settings_general_ip = request.form.get('settings_general_ip')
@ -1624,7 +1618,7 @@ def save_settings():
@app.route(base_url + 'check_update') @app.route(base_url + 'check_update')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def check_update(): def check_update():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
if not args.no_update: if not args.no_update:
@ -1636,7 +1630,7 @@ def check_update():
@app.route(base_url + 'system') @app.route(base_url + 'system')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def system(): def system():
authorize()
def get_time_from_interval(td_object): def get_time_from_interval(td_object):
seconds = int(td_object.total_seconds()) seconds = int(td_object.total_seconds())
@ -1720,7 +1714,7 @@ def system():
@app.route(base_url + 'logs') @app.route(base_url + 'logs')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def get_logs(): def get_logs():
authorize()
logs = [] logs = []
with open(os.path.join(args.config_dir, 'log', 'bazarr.log')) as file: with open(os.path.join(args.config_dir, 'log', 'bazarr.log')) as file:
for line in file.readlines(): for line in file.readlines():
@ -1735,7 +1729,7 @@ def get_logs():
@app.route(base_url + 'execute/<taskid>') @app.route(base_url + 'execute/<taskid>')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def execute_task(taskid): def execute_task(taskid):
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
execute_now(taskid) execute_now(taskid)
@ -1746,7 +1740,7 @@ def execute_task(taskid):
@app.route(base_url + 'remove_subtitles', methods=['POST']) @app.route(base_url + 'remove_subtitles', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def remove_subtitles(): def remove_subtitles():
authorize()
episodePath = request.form.get('episodePath') episodePath = request.form.get('episodePath')
language = request.form.get('language') language = request.form.get('language')
subtitlesPath = request.form.get('subtitlesPath') subtitlesPath = request.form.get('subtitlesPath')
@ -1765,7 +1759,7 @@ def remove_subtitles():
@app.route(base_url + 'remove_subtitles_movie', methods=['POST']) @app.route(base_url + 'remove_subtitles_movie', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def remove_subtitles_movie(): def remove_subtitles_movie():
authorize()
moviePath = request.form.get('moviePath') moviePath = request.form.get('moviePath')
language = request.form.get('language') language = request.form.get('language')
subtitlesPath = request.form.get('subtitlesPath') subtitlesPath = request.form.get('subtitlesPath')
@ -1783,7 +1777,7 @@ def remove_subtitles_movie():
@app.route(base_url + 'get_subtitle', methods=['POST']) @app.route(base_url + 'get_subtitle', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def get_subtitle(): def get_subtitle():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
episodePath = request.form.get('episodePath') episodePath = request.form.get('episodePath')
@ -1819,7 +1813,7 @@ def get_subtitle():
@app.route(base_url + 'manual_search', methods=['POST']) @app.route(base_url + 'manual_search', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def manual_search_json(): def manual_search_json():
authorize()
episodePath = request.form.get('episodePath') episodePath = request.form.get('episodePath')
sceneName = request.form.get('sceneName') sceneName = request.form.get('sceneName')
@ -1838,7 +1832,7 @@ def manual_search_json():
@app.route(base_url + 'manual_get_subtitle', methods=['POST']) @app.route(base_url + 'manual_get_subtitle', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def manual_get_subtitle(): def manual_get_subtitle():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
episodePath = request.form.get('episodePath') episodePath = request.form.get('episodePath')
@ -1876,7 +1870,7 @@ def manual_get_subtitle():
@app.route(base_url + 'manual_upload_subtitle', methods=['POST']) @app.route(base_url + 'manual_upload_subtitle', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def perform_manual_upload_subtitle(): def perform_manual_upload_subtitle():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
episodePath = request.form.get('episodePath') episodePath = request.form.get('episodePath')
@ -1920,7 +1914,7 @@ def perform_manual_upload_subtitle():
@app.route(base_url + 'get_subtitle_movie', methods=['POST']) @app.route(base_url + 'get_subtitle_movie', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def get_subtitle_movie(): def get_subtitle_movie():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
moviePath = request.form.get('moviePath') moviePath = request.form.get('moviePath')
@ -1955,7 +1949,7 @@ def get_subtitle_movie():
@app.route(base_url + 'manual_search_movie', methods=['POST']) @app.route(base_url + 'manual_search_movie', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def manual_search_movie_json(): def manual_search_movie_json():
authorize()
moviePath = request.form.get('moviePath') moviePath = request.form.get('moviePath')
sceneName = request.form.get('sceneName') sceneName = request.form.get('sceneName')
@ -1974,7 +1968,7 @@ def manual_search_movie_json():
@app.route(base_url + 'manual_get_subtitle_movie', methods=['POST']) @app.route(base_url + 'manual_get_subtitle_movie', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def manual_get_subtitle_movie(): def manual_get_subtitle_movie():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
moviePath = request.form.get('moviePath') moviePath = request.form.get('moviePath')
@ -2010,7 +2004,7 @@ def manual_get_subtitle_movie():
@app.route(base_url + 'manual_upload_subtitle_movie', methods=['POST']) @app.route(base_url + 'manual_upload_subtitle_movie', methods=['POST'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def perform_manual_upload_subtitle_movie(): def perform_manual_upload_subtitle_movie():
authorize()
ref = request.environ['HTTP_REFERER'] ref = request.environ['HTTP_REFERER']
moviePath = request.form.get('moviePath') moviePath = request.form.get('moviePath')
@ -2117,7 +2111,7 @@ def api_movies_history():
@app.route(base_url + 'test_url/<protocol>/<path:url>', methods=['GET']) @app.route(base_url + 'test_url/<protocol>/<path:url>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def test_url(protocol, url): def test_url(protocol, url):
authorize()
url = six.moves.urllib.parse.unquote(url) url = six.moves.urllib.parse.unquote(url)
try: try:
result = requests.get(protocol + "://" + url, allow_redirects=False, verify=False).json()['version'] 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']) @app.route(base_url + 'test_notification/<protocol>/<path:provider>', methods=['GET'])
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def test_notification(protocol, provider): def test_notification(protocol, provider):
authorize()
provider = six.moves.urllib.parse.unquote(provider) provider = six.moves.urllib.parse.unquote(provider)
apobj = apprise.Apprise() apobj = apprise.Apprise()
apobj.add(protocol + "://" + provider) apobj.add(protocol + "://" + provider)
@ -2144,7 +2138,7 @@ def test_notification(protocol, provider):
@app.route(base_url + 'notifications') @app.route(base_url + 'notifications')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def notifications(): def notifications():
authorize()
if queueconfig.notifications: if queueconfig.notifications:
return queueconfig.notifications.read() return queueconfig.notifications.read()
else: else:
@ -2154,14 +2148,14 @@ def notifications():
@app.route(base_url + 'running_tasks') @app.route(base_url + 'running_tasks')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def running_tasks_list(): def running_tasks_list():
authorize()
return dict(tasks=running_tasks) return dict(tasks=running_tasks)
@app.route(base_url + 'episode_history/<int:no>') @app.route(base_url + 'episode_history/<int:no>')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def episode_history(no): def episode_history(no):
authorize()
episode_history = database.execute("SELECT action, timestamp, language, provider, score FROM table_history " episode_history = database.execute("SELECT action, timestamp, language, provider, score FROM table_history "
"WHERE sonarrEpisodeId=? ORDER BY timestamp DESC", (no,)) "WHERE sonarrEpisodeId=? ORDER BY timestamp DESC", (no,))
for item in episode_history: for item in episode_history:
@ -2200,7 +2194,7 @@ def episode_history(no):
@app.route(base_url + 'movie_history/<int:no>') @app.route(base_url + 'movie_history/<int:no>')
# @custom_auth_basic(check_credentials) # @custom_auth_basic(check_credentials)
def movie_history(no): def movie_history(no):
authorize()
movie_history = database.execute("SELECT action, timestamp, language, provider, score FROM table_history_movie " movie_history = database.execute("SELECT action, timestamp, language, provider, score FROM table_history_movie "
"WHERE radarrId=? ORDER BY timestamp DESC", (no,)) "WHERE radarrId=? ORDER BY timestamp DESC", (no,))
for item in movie_history: for item in movie_history:

View File

@ -1,27 +1,81 @@
import copy
import os import os
import re import re
from .utils import echo from .utils import echo
from .parser import split_arg_string 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() { %(complete_func)s() {
local IFS=$'\n'
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
COMP_CWORD=$COMP_CWORD \\ COMP_CWORD=$COMP_CWORD \\
%(autocomplete_var)s=complete $1 ) ) %(autocomplete_var)s=complete $1 ) )
return 0 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_]') _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('-', '_')) 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, 'complete_func': '_%s_completion' % cf_name,
'script_names': prog_name, 'script_names': prog_name,
'autocomplete_var': complete_var, 'autocomplete_var': complete_var,
@ -29,37 +83,189 @@ def get_completion_script(prog_name, complete_var):
def resolve_ctx(cli, prog_name, args): 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) ctx = cli.make_context(prog_name, args, resilient_parsing=True)
while ctx.protected_args + ctx.args and isinstance(ctx.command, MultiCommand): args = ctx.protected_args + ctx.args
a = ctx.protected_args + ctx.args while args:
cmd = ctx.command.get_command(ctx, a[0]) if isinstance(ctx.command, MultiCommand):
if cmd is None: if not ctx.command.chain:
return None cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
ctx = cmd.make_context(a[0], a[1:], parent=ctx, resilient_parsing=True) 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 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): 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) ctx = resolve_ctx(cli, prog_name, args)
if ctx is None: if ctx is None:
return return []
choices = [] # In newer versions of bash long opts with '='s are partitioned, but it's easier to parse
if incomplete and not incomplete[:1].isalnum(): # 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: for param in ctx.command.params:
if not isinstance(param, Option): if isinstance(param, Option) and not param.hidden:
continue param_opts = [param_opt for param_opt in param.opts +
choices.extend(param.opts) param.secondary_opts if param_opt not in all_args or param.multiple]
choices.extend(param.secondary_opts) completions.extend([(o, param.help) for o in param_opts if o.startswith(incomplete)])
elif isinstance(ctx.command, MultiCommand): return completions
choices.extend(ctx.command.list_commands(ctx)) # 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: add_subcommand_completions(ctx, incomplete, completions)
if item.startswith(incomplete): # Sort before returning so that proper ordering can be enforced in custom types.
yield item 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']) cwords = split_arg_string(os.environ['COMP_WORDS'])
cword = int(os.environ['COMP_CWORD']) cword = int(os.environ['COMP_CWORD'])
args = cwords[1:cword] args = cwords[1:cword]
@ -69,15 +275,19 @@ def do_complete(cli, prog_name):
incomplete = '' incomplete = ''
for item in get_choices(cli, prog_name, args, 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 return True
def bashcomplete(cli, prog_name, complete_var, complete_instr): def bashcomplete(cli, prog_name, complete_var, complete_instr):
if complete_instr == 'source': if complete_instr.startswith('source'):
echo(get_completion_script(prog_name, complete_var)) shell = 'zsh' if complete_instr == 'source_zsh' else 'bash'
echo(get_completion_script(prog_name, complete_var, shell))
return True return True
elif complete_instr == 'complete': elif complete_instr == 'complete' or complete_instr == 'complete_zsh':
return do_complete(cli, prog_name) return do_complete(cli, prog_name, complete_instr == 'complete_zsh')
return False return False

View File

@ -7,24 +7,31 @@ from weakref import WeakKeyDictionary
PY2 = sys.version_info[0] == 2 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 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(): def get_filesystem_encoding():
return sys.getfilesystemencoding() or sys.getdefaultencoding() 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: if encoding is None:
encoding = get_best_encoding(stream) encoding = get_best_encoding(stream)
if errors is None: if errors is None:
errors = 'replace' errors = 'replace'
return _NonClosingTextIOWrapper(stream, encoding, errors, return _NonClosingTextIOWrapper(stream, encoding, errors,
line_buffering=True) line_buffering=True,
force_readable=force_readable,
force_writable=force_writable)
def is_ascii_encoding(encoding): def is_ascii_encoding(encoding):
@ -45,8 +52,10 @@ def get_best_encoding(stream):
class _NonClosingTextIOWrapper(io.TextIOWrapper): class _NonClosingTextIOWrapper(io.TextIOWrapper):
def __init__(self, stream, encoding, errors, **extra): def __init__(self, stream, encoding, errors,
self._stream = stream = _FixupStream(stream) force_readable=False, force_writable=False, **extra):
self._stream = stream = _FixupStream(stream, force_readable,
force_writable)
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
# The io module is a place where the Python 3 text behavior # 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 """The new io interface needs more from streams than streams
traditionally implement. As such, this fix-up code is necessary in traditionally implement. As such, this fix-up code is necessary in
some circumstances. 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._stream = stream
self._force_readable = force_readable
self._force_writable = force_writable
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self._stream, name) return getattr(self._stream, name)
@ -101,6 +116,8 @@ class _FixupStream(object):
return self._stream.read(size) return self._stream.read(size)
def readable(self): def readable(self):
if self._force_readable:
return True
x = getattr(self._stream, 'readable', None) x = getattr(self._stream, 'readable', None)
if x is not None: if x is not None:
return x() return x()
@ -111,6 +128,8 @@ class _FixupStream(object):
return True return True
def writable(self): def writable(self):
if self._force_writable:
return True
x = getattr(self._stream, 'writable', None) x = getattr(self._stream, 'writable', None)
if x is not None: if x is not None:
return x() return x()
@ -139,6 +158,7 @@ if PY2:
bytes = str bytes = str
raw_input = raw_input raw_input = raw_input
string_types = (str, unicode) string_types = (str, unicode)
int_types = (int, long)
iteritems = lambda x: x.iteritems() iteritems = lambda x: x.iteritems()
range_type = xrange range_type = xrange
@ -165,10 +185,13 @@ if PY2:
# available (which is why we use try-catch instead of the WIN variable # 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 # here), such as the Google App Engine development server on Windows. In
# those cases there is just nothing we can do. # those cases there is just nothing we can do.
def set_binary_mode(f):
return f
try: try:
import msvcrt import msvcrt
except ImportError: except ImportError:
set_binary_mode = lambda x: x pass
else: else:
def set_binary_mode(f): def set_binary_mode(f):
try: try:
@ -179,6 +202,21 @@ if PY2:
msvcrt.setmode(fileno, os.O_BINARY) msvcrt.setmode(fileno, os.O_BINARY)
return f 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): def isidentifier(x):
return _identifier_re.search(x) is not None return _identifier_re.search(x) is not None
@ -186,28 +224,35 @@ if PY2:
return set_binary_mode(sys.stdin) return set_binary_mode(sys.stdin)
def get_binary_stdout(): def get_binary_stdout():
_wrap_std_stream('stdout')
return set_binary_mode(sys.stdout) return set_binary_mode(sys.stdout)
def get_binary_stderr(): def get_binary_stderr():
_wrap_std_stream('stderr')
return set_binary_mode(sys.stderr) return set_binary_mode(sys.stderr)
def get_text_stdin(encoding=None, errors=None): def get_text_stdin(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stdin, encoding, errors) rv = _get_windows_console_stream(sys.stdin, encoding, errors)
if rv is not None: if rv is not None:
return rv 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): def get_text_stdout(encoding=None, errors=None):
_wrap_std_stream('stdout')
rv = _get_windows_console_stream(sys.stdout, encoding, errors) rv = _get_windows_console_stream(sys.stdout, encoding, errors)
if rv is not None: if rv is not None:
return rv 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): def get_text_stderr(encoding=None, errors=None):
_wrap_std_stream('stderr')
rv = _get_windows_console_stream(sys.stderr, encoding, errors) rv = _get_windows_console_stream(sys.stderr, encoding, errors)
if rv is not None: if rv is not None:
return rv 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): def filename_to_ui(value):
if isinstance(value, bytes): if isinstance(value, bytes):
@ -218,6 +263,7 @@ else:
text_type = str text_type = str
raw_input = input raw_input = input
string_types = (str,) string_types = (str,)
int_types = (int,)
range_type = range range_type = range
isidentifier = lambda x: x.isidentifier() isidentifier = lambda x: x.isidentifier()
iteritems = lambda x: iter(x.items()) iteritems = lambda x: iter(x.items())
@ -298,7 +344,8 @@ else:
return False 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): if _is_binary_reader(text_reader, False):
binary_reader = text_reader binary_reader = text_reader
else: else:
@ -324,9 +371,11 @@ else:
# we're so fundamentally fucked that nothing can repair it. # we're so fundamentally fucked that nothing can repair it.
if errors is None: if errors is None:
errors = 'replace' 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): if _is_binary_writer(text_writer, False):
binary_writer = text_writer binary_writer = text_writer
else: else:
@ -352,7 +401,8 @@ else:
# we're so fundamentally fucked that nothing can repair it. # we're so fundamentally fucked that nothing can repair it.
if errors is None: if errors is None:
errors = 'replace' 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(): def get_binary_stdin():
reader = _find_binary_reader(sys.stdin) reader = _find_binary_reader(sys.stdin)
@ -379,19 +429,22 @@ else:
rv = _get_windows_console_stream(sys.stdin, encoding, errors) rv = _get_windows_console_stream(sys.stdin, encoding, errors)
if rv is not None: if rv is not None:
return rv 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): def get_text_stdout(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stdout, encoding, errors) rv = _get_windows_console_stream(sys.stdout, encoding, errors)
if rv is not None: if rv is not None:
return rv 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): def get_text_stderr(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stderr, encoding, errors) rv = _get_windows_console_stream(sys.stderr, encoding, errors)
if rv is not None: if rv is not None:
return rv 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): def filename_to_ui(value):
if isinstance(value, bytes): 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 # Standard streams first. These are simple because they don't need
# special handling for the atomic flag. It's entirely ignored. # special handling for the atomic flag. It's entirely ignored.
if filename == '-': if filename == '-':
if 'w' in mode: if any(m in mode for m in ['w', 'a', 'x']):
if 'b' in mode: if 'b' in mode:
return get_binary_stdout(), False return get_binary_stdout(), False
return get_text_stdout(encoding=encoding, errors=errors), 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: else:
f = os.fdopen(fd, mode) 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. # Used in a destructor call, needs extra protection from interpreter cleanup.
@ -533,7 +586,7 @@ if WIN:
# Windows has a smaller terminal # Windows has a smaller terminal
DEFAULT_COLUMNS = 79 DEFAULT_COLUMNS = 79
from ._winconsole import _get_windows_console_stream from ._winconsole import _get_windows_console_stream, _wrap_std_stream
def _get_argv_encoding(): def _get_argv_encoding():
import locale import locale
@ -595,6 +648,7 @@ else:
return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding()
_get_windows_console_stream = lambda *x: None _get_windows_console_stream = lambda *x: None
_wrap_std_stream = lambda *x: None
def term_len(x): def term_len(x):
@ -620,6 +674,7 @@ def _make_cached_stream_func(src_func, wrapper_func):
return rv return rv
rv = wrapper_func() rv = wrapper_func()
try: try:
stream = src_func() # In case wrapper_func() modified the stream
cache[stream] = rv cache[stream] = rv
except Exception: except Exception:
pass pass

View File

@ -1,20 +1,24 @@
# -*- coding: utf-8 -*-
""" """
click._termui_impl click._termui_impl
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
This module contains implementations for the termui module. To keep the This module contains implementations for the termui module. To keep the
import time of Click down, some infrequently used functionality is placed import time of Click down, some infrequently used functionality is
in this module and only imported as needed. placed in this module and only imported as needed.
:copyright: (c) 2014 by Armin Ronacher. :copyright: © 2014 by the Pallets team.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE.rst for more details.
""" """
import os import os
import sys import sys
import time import time
import math import math
import contextlib
from ._compat import _default_text_stdout, range_type, PY2, isatty, \ 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 .utils import echo
from .exceptions import ClickException from .exceptions import ClickException
@ -41,7 +45,7 @@ def _length_hint(obj):
except TypeError: except TypeError:
return None return None
if hint is NotImplemented or \ if hint is NotImplemented or \
not isinstance(hint, (int, long)) or \ not isinstance(hint, int_types) or \
hint < 0: hint < 0:
return None return None
return hint return hint
@ -88,6 +92,7 @@ class ProgressBar(object):
self.current_item = None self.current_item = None
self.is_hidden = not isatty(self.file) self.is_hidden = not isatty(self.file)
self._last_line = None self._last_line = None
self.short_limit = 0.5
def __enter__(self): def __enter__(self):
self.entered = True self.entered = True
@ -101,10 +106,13 @@ class ProgressBar(object):
if not self.entered: if not self.entered:
raise RuntimeError('You need to use progress bars in a with block.') raise RuntimeError('You need to use progress bars in a with block.')
self.render_progress() self.render_progress()
return self return self.generator()
def is_fast(self):
return time.time() - self.start <= self.short_limit
def render_finish(self): def render_finish(self):
if self.is_hidden: if self.is_hidden or self.is_fast():
return return
self.file.write(AFTER_BAR) self.file.write(AFTER_BAR)
self.file.flush() self.file.flush()
@ -129,13 +137,13 @@ class ProgressBar(object):
def format_eta(self): def format_eta(self):
if self.eta_known: if self.eta_known:
t = self.eta + 1 t = int(self.eta)
seconds = t % 60 seconds = t % 60
t /= 60 t //= 60
minutes = t % 60 minutes = t % 60
t /= 60 t //= 60
hours = t % 24 hours = t % 24
t /= 24 t //= 24
if t > 0: if t > 0:
days = t days = t
return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds)
@ -152,25 +160,27 @@ class ProgressBar(object):
def format_pct(self): def format_pct(self):
return ('% 4d%%' % int(self.pct * 100))[1:] return ('% 4d%%' % int(self.pct * 100))[1:]
def format_progress_line(self): def format_bar(self):
show_percent = self.show_percent
info_bits = []
if self.length_known: if self.length_known:
bar_length = int(self.pct * self.width) bar_length = int(self.pct * self.width)
bar = self.fill_char * bar_length bar = self.fill_char * bar_length
bar += self.empty_char * (self.width - bar_length) bar += self.empty_char * (self.width - bar_length)
if show_percent is None: elif self.finished:
show_percent = not self.show_pos bar = self.fill_char * self.width
else: else:
if self.finished: bar = list(self.empty_char * (self.width or 1))
bar = self.fill_char * self.width if self.time_per_iteration != 0:
else: bar[int((math.cos(self.pos * self.time_per_iteration)
bar = list(self.empty_char * (self.width or 1)) / 2.0 + 0.5) * self.width)] = self.fill_char
if self.time_per_iteration != 0: bar = ''.join(bar)
bar[int((math.cos(self.pos * self.time_per_iteration) return bar
/ 2.0 + 0.5) * self.width)] = self.fill_char
bar = ''.join(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: if self.show_pos:
info_bits.append(self.format_pos()) info_bits.append(self.format_pos())
@ -185,49 +195,47 @@ class ProgressBar(object):
return (self.bar_template % { return (self.bar_template % {
'label': self.label, 'label': self.label,
'bar': bar, 'bar': self.format_bar(),
'info': self.info_sep.join(info_bits) 'info': self.info_sep.join(info_bits)
}).rstrip() }).rstrip()
def render_progress(self): def render_progress(self):
from .termui import get_terminal_size from .termui import get_terminal_size
nl = False
if self.is_hidden: if self.is_hidden:
buf = [self.label] return
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
clear_width = self.width buf = []
if self.max_width is not None: # Update width in case the terminal has been resized
clear_width = self.max_width 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) clear_width = self.width
line = self.format_progress_line() if self.max_width is not None:
line_len = term_len(line) clear_width = self.max_width
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)) 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) line = ''.join(buf)
# Render the line only if it changed. # 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 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() self.file.flush()
def make_step(self, n_steps): def make_step(self, n_steps):
@ -239,7 +247,16 @@ class ProgressBar(object):
return return
self.last_eta = time.time() 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 self.eta_known = self.length_known
@ -252,54 +269,56 @@ class ProgressBar(object):
self.current_item = None self.current_item = None
self.finished = True 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: if self.is_hidden:
return next(self.iter) for rv in self.iter:
try: yield rv
rv = next(self.iter) else:
self.current_item = rv for rv in self.iter:
except StopIteration: self.current_item = rv
yield rv
self.update(1)
self.finish() self.finish()
self.render_progress() 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.""" """Decide what method to use for paging through text."""
stdout = _default_text_stdout() stdout = _default_text_stdout()
if not isatty(sys.stdin) or not isatty(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() pager_cmd = (os.environ.get('PAGER', None) or '').strip()
if pager_cmd: if pager_cmd:
if WIN: if WIN:
return _tempfilepager(text, pager_cmd, color) return _tempfilepager(generator, pager_cmd, color)
return _pipepager(text, pager_cmd, color) return _pipepager(generator, pager_cmd, color)
if os.environ.get('TERM') in ('dumb', 'emacs'): 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'): 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: if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0:
return _pipepager(text, 'less', color) return _pipepager(generator, 'less', color)
import tempfile import tempfile
fd, filename = tempfile.mkstemp() fd, filename = tempfile.mkstemp()
os.close(fd) os.close(fd)
try: try:
if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0:
return _pipepager(text, 'more', color) return _pipepager(generator, 'more', color)
return _nullpager(stdout, text, color) return _nullpager(stdout, generator, color)
finally: finally:
os.unlink(filename) os.unlink(filename)
def _pipepager(text, cmd, color): def _pipepager(generator, cmd, color):
"""Page through text by feeding it to another program. Invoking a """Page through text by feeding it to another program. Invoking a
pager through this might support colors. 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: elif 'r' in less_flags or 'R' in less_flags:
color = True color = True
if not color:
text = strip_ansi(text)
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
env=env) env=env)
encoding = get_best_encoding(c.stdin) encoding = get_best_encoding(c.stdin)
try: try:
c.stdin.write(text.encode(encoding, 'replace')) for text in generator:
c.stdin.close() if not color:
text = strip_ansi(text)
c.stdin.write(text.encode(encoding, 'replace'))
except (IOError, KeyboardInterrupt): except (IOError, KeyboardInterrupt):
pass pass
else:
c.stdin.close()
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting # Less doesn't respect ^C, but catches it for its own UI purposes (aborting
# search or other commands inside less). # search or other commands inside less).
@ -346,10 +367,12 @@ def _pipepager(text, cmd, color):
break break
def _tempfilepager(text, cmd, color): def _tempfilepager(generator, cmd, color):
"""Page through text by invoking a program on a temporary file.""" """Page through text by invoking a program on a temporary file."""
import tempfile import tempfile
filename = tempfile.mktemp() filename = tempfile.mktemp()
# TODO: This never terminates if the passed generator never terminates.
text = "".join(generator)
if not color: if not color:
text = strip_ansi(text) text = strip_ansi(text)
encoding = get_best_encoding(sys.stdout) encoding = get_best_encoding(sys.stdout)
@ -361,11 +384,12 @@ def _tempfilepager(text, cmd, color):
os.unlink(filename) os.unlink(filename)
def _nullpager(stream, text, color): def _nullpager(stream, generator, color):
"""Simply print unformatted text. This is the ultimate fallback.""" """Simply print unformatted text. This is the ultimate fallback."""
if not color: for text in generator:
text = strip_ansi(text) if not color:
stream.write(text) text = strip_ansi(text)
stream.write(text)
class Editor(object): class Editor(object):
@ -478,6 +502,14 @@ def open_url(url, wait=False, locate=False):
args = 'start %s "" "%s"' % ( args = 'start %s "" "%s"' % (
wait and '/WAIT' or '', url.replace('"', '')) wait and '/WAIT' or '', url.replace('"', ''))
return os.system(args) 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: try:
if locate: if locate:
@ -497,32 +529,69 @@ def open_url(url, wait=False, locate=False):
def _translate_ch_to_exc(ch): def _translate_ch_to_exc(ch):
if ch == '\x03': if ch == u'\x03':
raise KeyboardInterrupt() 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() raise EOFError()
if WIN: if WIN:
import msvcrt import msvcrt
@contextlib.contextmanager
def raw_terminal():
yield
def getchar(echo): 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: 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) _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 return rv
else: else:
import tty import tty
import termios import termios
def getchar(echo): @contextlib.contextmanager
def raw_terminal():
if not isatty(sys.stdin): if not isatty(sys.stdin):
f = open('/dev/tty') f = open('/dev/tty')
fd = f.fileno() fd = f.fileno()
@ -533,9 +602,7 @@ else:
old_settings = termios.tcgetattr(fd) old_settings = termios.tcgetattr(fd)
try: try:
tty.setraw(fd) tty.setraw(fd)
ch = os.read(fd, 32) yield fd
if echo and isatty(sys.stdout):
sys.stdout.write(ch)
finally: finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
sys.stdout.flush() sys.stdout.flush()
@ -543,5 +610,12 @@ else:
f.close() f.close()
except termios.error: except termios.error:
pass 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

View File

@ -14,6 +14,8 @@ click = sys.modules[__name__.rsplit('.', 1)[0]]
def _find_unicode_literals_frame(): def _find_unicode_literals_frame():
import __future__ import __future__
if not hasattr(sys, '_getframe'): # not all Python implementations have it
return 0
frm = sys._getframe(1) frm = sys._getframe(1)
idx = 1 idx = 1
while frm is not None: while frm is not None:
@ -41,7 +43,7 @@ def _check_for_unicode_literals():
'because it can introduce subtle bugs in your ' 'because it can introduce subtle bugs in your '
'code. You should instead use explicit u"" literals ' 'code. You should instead use explicit u"" literals '
'for your unicode strings. For more information see ' 'for your unicode strings. For more information see '
'http://click.pocoo.org/python3/'), 'https://click.palletsprojects.com/python3/'),
stacklevel=bad_frame) stacklevel=bad_frame)
@ -60,8 +62,11 @@ def _verify_python3_env():
extra = '' extra = ''
if os.name == 'posix': if os.name == 'posix':
import subprocess import subprocess
rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, try:
stderr=subprocess.PIPE).communicate()[0] rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()[0]
except OSError:
rv = b''
good_locales = set() good_locales = set()
has_c_utf8 = False has_c_utf8 = False
@ -94,7 +99,7 @@ def _verify_python3_env():
else: else:
extra += ( extra += (
'This system lists a couple of UTF-8 supporting locales that\n' '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' 'discovered: %s'
) % ', '.join(sorted(good_locales)) ) % ', '.join(sorted(good_locales))
@ -112,7 +117,9 @@ def _verify_python3_env():
'is not supported' 'is not supported'
) % bad_locale ) % bad_locale
raise RuntimeError('Click will abort further execution because Python 3 ' raise RuntimeError(
'was configured to use ASCII as encoding for the ' 'Click will abort further execution because Python 3 was'
'environment. Consult http://click.pocoo.org/python3/' ' configured to use ASCII as encoding for the environment.'
'for mitigation steps.' + extra) ' Consult https://click.palletsprojects.com/en/7.x/python3/ for'
' mitigation steps.' + extra
)

View File

@ -15,7 +15,7 @@ import zlib
import time import time
import ctypes import ctypes
import msvcrt 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, \ from ctypes import byref, POINTER, c_int, c_char, c_char_p, \
c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE
try: 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): def _get_text_stdin(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
@ -210,14 +244,14 @@ def _get_text_stdin(buffer_stream):
def _get_text_stdout(buffer_stream): def _get_text_stdout(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
_WindowsConsoleWriter(STDOUT_HANDLE), io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
'utf-16-le', 'strict', line_buffering=True) 'utf-16-le', 'strict', line_buffering=True)
return ConsoleStream(text_stream, buffer_stream) return ConsoleStream(text_stream, buffer_stream)
def _get_text_stderr(buffer_stream): def _get_text_stderr(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
_WindowsConsoleWriter(STDERR_HANDLE), io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
'utf-16-le', 'strict', line_buffering=True) 'utf-16-le', 'strict', line_buffering=True)
return ConsoleStream(text_stream, buffer_stream) return ConsoleStream(text_stream, buffer_stream)
@ -261,7 +295,7 @@ def _get_windows_console_stream(f, encoding, errors):
func = _stream_factories.get(f.fileno()) func = _stream_factories.get(f.fileno())
if func is not None: if func is not None:
if not PY2: if not PY2:
f = getattr(f, 'buffer') f = getattr(f, 'buffer', None)
if f is None: if f is None:
return None return None
else: else:

View File

@ -1,4 +1,5 @@
import errno import errno
import inspect
import os import os
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
@ -6,15 +7,16 @@ from itertools import repeat
from functools import update_wrapper from functools import update_wrapper
from .types import convert_type, IntRange, BOOL 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, \ from .exceptions import ClickException, UsageError, BadParameter, Abort, \
MissingParameter MissingParameter, Exit
from .termui import prompt, confirm from .termui import prompt, confirm, style
from .formatting import HelpFormatter, join_options from .formatting import HelpFormatter, join_options
from .parser import OptionParser, split_opt from .parser import OptionParser, split_opt
from .globals import push_context, pop_context 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 from ._unicodefun import _check_for_unicode_literals, _verify_python3_env
@ -24,6 +26,24 @@ _missing = object()
SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...'
SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [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): def _bashcomplete(cmd, prog_name, complete_var=None):
"""Internal handler for the bash completion support.""" """Internal handler for the bash completion support."""
@ -35,7 +55,7 @@ def _bashcomplete(cmd, prog_name, complete_var=None):
from ._bashcomplete import bashcomplete from ._bashcomplete import bashcomplete
if bashcomplete(cmd, prog_name, complete_var, complete_instr): 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): 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 ' raise RuntimeError('%s. Command "%s" is set to chain and "%s" was '
'added as subcommand but it in itself is a ' 'added as subcommand but it in itself is a '
'multi command. ("%s" is a %s within a chained ' 'multi command. ("%s" is a %s within a chained '
'%s named "%s"). This restriction was supposed to ' '%s named "%s").' % (
'be lifted in 6.0 but the fix was flawed. This '
'will be fixed in Click 7.0' % (
hint, base_command.name, cmd_name, hint, base_command.name, cmd_name,
cmd_name, cmd.__class__.__name__, cmd_name, cmd.__class__.__name__,
base_command.__class__.__name__, base_command.__class__.__name__,
@ -165,7 +183,8 @@ class Context(object):
add some safety mapping on the right. add some safety mapping on the right.
:param resilient_parsing: if this flag is enabled then Click will :param resilient_parsing: if this flag is enabled then Click will
parse without any interactivity or callback 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. things such as completion support.
:param allow_extra_args: if this is set to `True` then extra arguments :param allow_extra_args: if this is set to `True` then extra arguments
at the end will not raise an error and will be 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 self.token_normalize_func = token_normalize_func
#: Indicates if resilient parsing is enabled. In that case Click #: 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 self.resilient_parsing = resilient_parsing
# If there is no envvar prefix yet, but the parent has one and # 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, auto_envvar_prefix = '%s_%s' % (parent.auto_envvar_prefix,
self.info_name.upper()) self.info_name.upper())
else: else:
self.auto_envvar_prefix = auto_envvar_prefix.upper() auto_envvar_prefix = auto_envvar_prefix.upper()
self.auto_envvar_prefix = auto_envvar_prefix self.auto_envvar_prefix = auto_envvar_prefix
if color is None and parent is not None: if color is None and parent is not None:
@ -372,7 +392,7 @@ class Context(object):
@property @property
def meta(self): def meta(self):
"""This is a dictionary which is shared with all the contexts """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 state here if they need to. It is however the responsibility of
that code to manage this dictionary well. that code to manage this dictionary well.
@ -481,7 +501,7 @@ class Context(object):
def exit(self, code=0): def exit(self, code=0):
"""Exits the application with a given exit code.""" """Exits the application with a given exit code."""
sys.exit(code) raise Exit(code)
def get_usage(self): def get_usage(self):
"""Helper method to get formatted usage string for the current """Helper method to get formatted usage string for the current
@ -655,7 +675,7 @@ class BaseCommand(object):
name from ``sys.argv[0]``. name from ``sys.argv[0]``.
:param complete_var: the environment variable that controls the :param complete_var: the environment variable that controls the
bash completion support. The default is bash completion support. The default is
``"_<prog_name>_COMPLETE"`` with prog name in ``"_<prog_name>_COMPLETE"`` with prog_name in
uppercase. uppercase.
:param standalone_mode: the default behavior is to invoke the script :param standalone_mode: the default behavior is to invoke the script
in standalone mode. Click will then in standalone mode. Click will then
@ -670,7 +690,7 @@ class BaseCommand(object):
constructor. See :class:`Context` for more information. constructor. See :class:`Context` for more information.
""" """
# If we are in Python 3, we will verify that the environment is # 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. # broken script.
if not PY2: if not PY2:
_verify_python3_env() _verify_python3_env()
@ -697,6 +717,13 @@ class BaseCommand(object):
rv = self.invoke(ctx) rv = self.invoke(ctx)
if not standalone_mode: if not standalone_mode:
return rv 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() ctx.exit()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
echo(file=sys.stderr) echo(file=sys.stderr)
@ -708,9 +735,24 @@ class BaseCommand(object):
sys.exit(e.exit_code) sys.exit(e.exit_code)
except IOError as e: except IOError as e:
if e.errno == errno.EPIPE: if e.errno == errno.EPIPE:
sys.stdout = PacifyFlushWrapper(sys.stdout)
sys.stderr = PacifyFlushWrapper(sys.stderr)
sys.exit(1) sys.exit(1)
else: else:
raise 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: except Abort:
if not standalone_mode: if not standalone_mode:
raise raise
@ -743,11 +785,16 @@ class Command(BaseCommand):
shown on the command listing of the parent command. shown on the command listing of the parent command.
:param add_help_option: by default each command registers a ``--help`` :param add_help_option: by default each command registers a ``--help``
option. This can be disabled by this parameter. 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, def __init__(self, name, context_settings=None, callback=None,
params=None, help=None, epilog=None, short_help=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) BaseCommand.__init__(self, name, context_settings)
#: the callback to execute when the command fires. This might be #: the callback to execute when the command fires. This might be
#: `None` in which case nothing happens. #: `None` in which case nothing happens.
@ -756,13 +803,17 @@ class Command(BaseCommand):
#: should show up in the help page and execute. Eager parameters #: should show up in the help page and execute. Eager parameters
#: will automatically be handled before non eager ones. #: will automatically be handled before non eager ones.
self.params = params or [] 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.help = help
self.epilog = epilog self.epilog = epilog
self.options_metavar = options_metavar 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.short_help = short_help
self.add_help_option = add_help_option self.add_help_option = add_help_option
self.hidden = hidden
self.deprecated = deprecated
def get_usage(self, ctx): def get_usage(self, ctx):
formatter = ctx.make_formatter() formatter = ctx.make_formatter()
@ -816,8 +867,6 @@ class Command(BaseCommand):
def make_parser(self, ctx): def make_parser(self, ctx):
"""Creates the underlying option parser for this command.""" """Creates the underlying option parser for this command."""
parser = OptionParser(ctx) 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): for param in self.get_params(ctx):
param.add_to_parser(parser, ctx) param.add_to_parser(parser, ctx)
return parser return parser
@ -830,6 +879,10 @@ class Command(BaseCommand):
self.format_help(ctx, formatter) self.format_help(ctx, formatter)
return formatter.getvalue().rstrip('\n') 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): def format_help(self, ctx, formatter):
"""Writes the help into the formatter if it exists. """Writes the help into the formatter if it exists.
@ -850,7 +903,14 @@ class Command(BaseCommand):
if self.help: if self.help:
formatter.write_paragraph() formatter.write_paragraph()
with formatter.indentation(): 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): def format_options(self, ctx, formatter):
"""Writes all the options into the formatter if they exist.""" """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) """Given a context, this invokes the attached callback (if it exists)
in the right way. in the right way.
""" """
_maybe_show_deprecated_notice(self)
if self.callback is not None: if self.callback is not None:
return ctx.invoke(self.callback, **ctx.params) 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 """Extra format methods for multi methods that adds all the commands
after the options. after the options.
""" """
rows = [] commands = []
for subcommand in self.list_commands(ctx): for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand) cmd = self.get_command(ctx, subcommand)
# What is this, the tool lied about a command. Ignore it # What is this, the tool lied about a command. Ignore it
if cmd is None: if cmd is None:
continue continue
if cmd.hidden:
continue
help = cmd.short_help or '' commands.append((subcommand, cmd))
rows.append((subcommand, help))
if rows: # allow for 3 times the default spacing
with formatter.section('Commands'): if len(commands):
formatter.write_dl(rows) 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): def parse_args(self, ctx, args):
if not args and self.no_args_is_help and not ctx.resilient_parsing: 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 # an option we want to kick off parsing again for arguments to
# resolve things like --help which now should go to the main # resolve things like --help which now should go to the main
# place. # place.
if cmd is None: if cmd is None and not ctx.resilient_parsing:
if split_opt(cmd_name)[0]: if split_opt(cmd_name)[0]:
self.parse_args(ctx, ctx.args) self.parse_args(ctx, ctx.args)
ctx.fail('No such command "%s".' % original_cmd_name) ctx.fail('No such command "%s".' % original_cmd_name)
@ -1216,7 +1287,7 @@ class CommandCollection(MultiCommand):
class Parameter(object): 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 :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
not supported by design as some of the internals for parsing are not supported by design as some of the internals for parsing are
intentionally not finalized. intentionally not finalized.
@ -1261,7 +1332,8 @@ class Parameter(object):
def __init__(self, param_decls=None, type=None, required=False, def __init__(self, param_decls=None, type=None, required=False,
default=None, callback=None, nargs=None, metavar=None, 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.name, self.opts, self.secondary_opts = \
self._parse_decls(param_decls or (), expose_value) self._parse_decls(param_decls or (), expose_value)
@ -1284,6 +1356,7 @@ class Parameter(object):
self.is_eager = is_eager self.is_eager = is_eager
self.metavar = metavar self.metavar = metavar
self.envvar = envvar self.envvar = envvar
self.autocompletion = autocompletion
@property @property
def human_readable_name(self): def human_readable_name(self):
@ -1316,10 +1389,10 @@ class Parameter(object):
def consume_value(self, ctx, opts): def consume_value(self, ctx, opts):
value = opts.get(self.name) value = opts.get(self.name)
if value is None:
value = ctx.lookup_default(self.name)
if value is None: if value is None:
value = self.value_from_envvar(ctx) value = self.value_from_envvar(ctx)
if value is None:
value = ctx.lookup_default(self.name)
return value return value
def type_cast_value(self, ctx, value): def type_cast_value(self, ctx, value):
@ -1364,7 +1437,7 @@ class Parameter(object):
def full_process_value(self, ctx, value): def full_process_value(self, ctx, value):
value = self.process_value(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) value = self.get_default(ctx)
if self.required and self.value_is_missing(value): if self.required and self.value_is_missing(value):
@ -1416,6 +1489,13 @@ class Parameter(object):
def get_usage_pieces(self, ctx): def get_usage_pieces(self, ctx):
return [] 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): class Option(Parameter):
"""Options are usually optional values on the command line and """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. All other parameters are passed onwards to the parameter constructor.
:param show_default: controls if the default value should be shown on the :param show_default: controls if the default value should be shown on the
help page. Normally, defaults are not shown. help page. Normally, defaults are not shown. If this
:param prompt: if set to `True` or a non empty string then the user will value is a string, it shows the string instead of the
be prompted for input if not set. If set to `True` the value. This is particularly useful for dynamic options.
prompt will be the option name capitalized. :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 :param confirmation_prompt: if set then the value will need to be confirmed
if it was prompted for. if it was prompted for.
:param hide_input: if this is `True` then the input on the prompt will be :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 variable in case a prefix is defined on the
context. context.
:param help: the help string. :param help: the help string.
:param hidden: hide this option from help outputs.
""" """
param_type_name = 'option' param_type_name = 'option'
@ -1455,7 +1541,8 @@ class Option(Parameter):
prompt=False, confirmation_prompt=False, prompt=False, confirmation_prompt=False,
hide_input=False, is_flag=None, flag_value=None, hide_input=False, is_flag=None, flag_value=None,
multiple=False, count=False, allow_from_autoenv=True, 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 default_is_missing = attrs.get('default', _missing) is _missing
Parameter.__init__(self, param_decls, type=type, **attrs) Parameter.__init__(self, param_decls, type=type, **attrs)
@ -1468,6 +1555,7 @@ class Option(Parameter):
self.prompt = prompt_text self.prompt = prompt_text
self.confirmation_prompt = confirmation_prompt self.confirmation_prompt = confirmation_prompt
self.hide_input = hide_input self.hide_input = hide_input
self.hidden = hidden
# Flags # Flags
if is_flag is None: if is_flag is None:
@ -1500,6 +1588,8 @@ class Option(Parameter):
self.allow_from_autoenv = allow_from_autoenv self.allow_from_autoenv = allow_from_autoenv
self.help = help self.help = help
self.show_default = show_default self.show_default = show_default
self.show_choices = show_choices
self.show_envvar = show_envvar
# Sanity check for stuff we don't support # Sanity check for stuff we don't support
if __debug__: if __debug__:
@ -1548,8 +1638,8 @@ class Option(Parameter):
opts.append(decl) opts.append(decl)
if name is None and possible_names: if name is None and possible_names:
possible_names.sort(key=lambda x: len(x[0])) possible_names.sort(key=lambda x: -len(x[0])) # group long options first
name = possible_names[-1][1].replace('-', '_').lower() name = possible_names[0][1].replace('-', '_').lower()
if not isidentifier(name): if not isidentifier(name):
name = None name = None
@ -1595,6 +1685,8 @@ class Option(Parameter):
parser.add_option(self.opts, **kwargs) parser.add_option(self.opts, **kwargs)
def get_help_record(self, ctx): def get_help_record(self, ctx):
if self.hidden:
return
any_prefix_is_slash = [] any_prefix_is_slash = []
def _write_opts(opts): def _write_opts(opts):
@ -1611,11 +1703,28 @@ class Option(Parameter):
help = self.help or '' help = self.help or ''
extra = [] 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: if self.default is not None and self.show_default:
extra.append('default: %s' % ( if isinstance(self.show_default, string_types):
', '.join('%s' % d for d in self.default) default_string = '({})'.format(self.show_default)
if isinstance(self.default, (list, tuple)) elif isinstance(self.default, (list, tuple)):
else self.default, )) 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: if self.required:
extra.append('required') extra.append('required')
if extra: if extra:
@ -1649,8 +1758,8 @@ class Option(Parameter):
if self.is_bool_flag: if self.is_bool_flag:
return confirm(self.prompt, default) return confirm(self.prompt, default)
return prompt(self.prompt, default=default, return prompt(self.prompt, default=default, type=self.type,
hide_input=self.hide_input, hide_input=self.hide_input, show_choices=self.show_choices,
confirmation_prompt=self.confirmation_prompt, confirmation_prompt=self.confirmation_prompt,
value_proc=lambda x: self.process_value(ctx, x)) value_proc=lambda x: self.process_value(ctx, x))
@ -1710,7 +1819,9 @@ class Argument(Parameter):
def make_metavar(self): def make_metavar(self):
if self.metavar is not None: if self.metavar is not None:
return self.metavar return self.metavar
var = self.name.upper() var = self.type.get_metavar(self)
if not var:
var = self.name.upper()
if not self.required: if not self.required:
var = '[%s]' % var var = '[%s]' % var
if self.nargs != 1: if self.nargs != 1:
@ -1725,16 +1836,17 @@ class Argument(Parameter):
if len(decls) == 1: if len(decls) == 1:
name = arg = decls[0] name = arg = decls[0]
name = name.replace('-', '_').lower() name = name.replace('-', '_').lower()
elif len(decls) == 2:
name, arg = decls
else: else:
raise TypeError('Arguments take exactly one or two ' raise TypeError('Arguments take exactly one '
'parameter declarations, got %d' % len(decls)) 'parameter declaration, got %d' % len(decls))
return name, [arg], [] return name, [arg], []
def get_usage_pieces(self, ctx): def get_usage_pieces(self, ctx):
return [self.make_metavar()] return [self.make_metavar()]
def get_error_hint(self, ctx):
return '"%s"' % self.make_metavar()
def add_to_parser(self, parser, ctx): def add_to_parser(self, parser, ctx):
parser.add_argument(dest=self.name, nargs=self.nargs, parser.add_argument(dest=self.name, nargs=self.nargs,
obj=self) obj=self)

View File

@ -61,7 +61,7 @@ def make_pass_decorator(object_type, ensure=False):
raise RuntimeError('Managed to invoke callback without a ' raise RuntimeError('Managed to invoke callback without a '
'context object of type %r existing' 'context object of type %r existing'
% object_type.__name__) % 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 update_wrapper(new_func, f)
return decorator return decorator
@ -85,12 +85,12 @@ def _make_command(f, name, attrs, cls):
help = inspect.cleandoc(help) help = inspect.cleandoc(help)
attrs['help'] = help attrs['help'] = help
_check_for_unicode_literals() _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) callback=f, params=params, **attrs)
def command(name=None, cls=None, **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 callback. This will also automatically attach all decorated
:func:`option`\s and :func:`argument`\s as parameters to the command. :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`. command :class:`Group`.
:param name: the name of the command. This defaults to the function :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 :param cls: the command class to instantiate. This defaults to
:class:`Command`. :class:`Command`.
""" """
@ -164,10 +164,13 @@ def option(*param_decls, **attrs):
:class:`Option`. :class:`Option`.
""" """
def decorator(f): def decorator(f):
if 'help' in attrs: # Issue 926, copy attrs, so pre-defined options can re-use the same cls=
attrs['help'] = inspect.cleandoc(attrs['help']) option_attrs = attrs.copy()
OptionClass = attrs.pop('cls', Option)
_param_memo(f, OptionClass(param_decls, **attrs)) 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 f
return decorator return decorator
@ -235,7 +238,11 @@ def version_option(version=None, *param_decls, **attrs):
:param others: everything else is forwarded to :func:`option`. :param others: everything else is forwarded to :func:`option`.
""" """
if version is None: 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): def decorator(f):
prog_name = attrs.pop('prog_name', None) prog_name = attrs.pop('prog_name', None)
message = attrs.pop('message', '%(prog)s, version %(version)s') message = attrs.pop('message', '%(prog)s, version %(version)s')

View File

@ -2,6 +2,12 @@ from ._compat import PY2, filename_to_ui, get_text_stderr
from .utils import echo 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): class ClickException(Exception):
"""An exception that Click can handle and show to the user.""" """An exception that Click can handle and show to the user."""
@ -9,15 +15,25 @@ class ClickException(Exception):
exit_code = 1 exit_code = 1
def __init__(self, message): def __init__(self, message):
ctor_msg = message
if PY2: if PY2:
if message is not None: if ctor_msg is not None:
message = message.encode('utf-8') ctor_msg = ctor_msg.encode('utf-8')
Exception.__init__(self, message) Exception.__init__(self, ctor_msg)
self.message = message self.message = message
def format_message(self): def format_message(self):
return self.message 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): def show(self, file=None):
if file is None: if file is None:
file = get_text_stderr() file = get_text_stderr()
@ -37,14 +53,20 @@ class UsageError(ClickException):
def __init__(self, message, ctx=None): def __init__(self, message, ctx=None):
ClickException.__init__(self, message) ClickException.__init__(self, message)
self.ctx = ctx self.ctx = ctx
self.cmd = self.ctx and self.ctx.command or None
def show(self, file=None): def show(self, file=None):
if file is None: if file is None:
file = get_text_stderr() file = get_text_stderr()
color = None 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: if self.ctx is not None:
color = self.ctx.color 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) echo('Error: %s' % self.format_message(), file=file, color=color)
@ -76,11 +98,11 @@ class BadParameter(UsageError):
if self.param_hint is not None: if self.param_hint is not None:
param_hint = self.param_hint param_hint = self.param_hint
elif self.param is not None: 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: else:
return 'Invalid value: %s' % self.message return 'Invalid value: %s' % self.message
if isinstance(param_hint, (tuple, list)): param_hint = _join_param_hints(param_hint)
param_hint = ' / '.join('"%s"' % x for x in param_hint)
return 'Invalid value for %s: %s' % (param_hint, self.message) return 'Invalid value for %s: %s' % (param_hint, self.message)
@ -105,11 +127,10 @@ class MissingParameter(BadParameter):
if self.param_hint is not None: if self.param_hint is not None:
param_hint = self.param_hint param_hint = self.param_hint
elif self.param is not None: 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: else:
param_hint = None param_hint = None
if isinstance(param_hint, (tuple, list)): param_hint = _join_param_hints(param_hint)
param_hint = ' / '.join('"%s"' % x for x in param_hint)
param_type = self.param_type param_type = self.param_type
if param_type is None and self.param is not None: if param_type is None and self.param is not None:
@ -164,10 +185,13 @@ class BadOptionUsage(UsageError):
for an option is not correct. for an option is not correct.
.. versionadded:: 4.0 .. 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) UsageError.__init__(self, message, ctx)
self.option_name = option_name
class BadArgumentUsage(UsageError): class BadArgumentUsage(UsageError):
@ -199,3 +223,13 @@ class FileError(ClickException):
class Abort(RuntimeError): class Abort(RuntimeError):
"""An internal signalling exception that signals Click to abort.""" """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

View File

@ -9,7 +9,7 @@ def get_current_context(silent=False):
access the current context object from anywhere. This is a more implicit access the current context object from anywhere. This is a more implicit
alternative to the :func:`pass_context` decorator. This function is alternative to the :func:`pass_context` decorator. This function is
primarily useful for helpers such as :func:`echo` which might be 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. To push the current context, :meth:`Context.scope` can be used.

View File

@ -1,20 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
click.parser click.parser
~~~~~~~~~~~~ ~~~~~~~~~~~~
This module started out as largely a copy paste from the stdlib's 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 module with the features removed that we do not need from
optparse because we implement them in Click on a higher level (for optparse because we implement them in Click on a higher level (for
instance type handling, help formatting and a lot more). 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 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 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 generated and optparse in the stdlib uses gettext for no good reason
and might cause us issues. and might cause us issues.
""" """
import re import re
from collections import deque from collections import deque
from .exceptions import UsageError, NoSuchOption, BadOptionUsage, \ from .exceptions import UsageError, NoSuchOption, BadOptionUsage, \
@ -74,8 +75,8 @@ def _unpack_args(args, nargs_spec):
def _error_opt_args(nargs, opt): def _error_opt_args(nargs, opt):
if nargs == 1: if nargs == 1:
raise BadOptionUsage('%s option requires an argument' % opt) raise BadOptionUsage(opt, '%s option requires an argument' % opt)
raise BadOptionUsage('%s option requires %d arguments' % (opt, nargs)) raise BadOptionUsage(opt, '%s option requires %d arguments' % (opt, nargs))
def split_opt(opt): def split_opt(opt):
@ -321,7 +322,7 @@ class OptionParser(object):
if opt not in self._long_opt: if opt not in self._long_opt:
possibilities = [word for word in self._long_opt possibilities = [word for word in self._long_opt
if word.startswith(opt)] if word.startswith(opt)]
raise NoSuchOption(opt, possibilities=possibilities) raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
option = self._long_opt[opt] option = self._long_opt[opt]
if option.takes_value: if option.takes_value:
@ -342,7 +343,7 @@ class OptionParser(object):
del state.rargs[:nargs] del state.rargs[:nargs]
elif explicit_value is not None: 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: else:
value = None value = None
@ -364,7 +365,7 @@ class OptionParser(object):
if self.ignore_unknown_options: if self.ignore_unknown_options:
unknown_options.append(ch) unknown_options.append(ch)
continue continue
raise NoSuchOption(opt) raise NoSuchOption(opt, ctx=self.ctx)
if option.takes_value: if option.takes_value:
# Any characters left in arg? Pretend they're the # Any characters left in arg? Pretend they're the
# next arg, and stop consuming characters of arg. # next arg, and stop consuming characters of arg.

View File

@ -1,12 +1,14 @@
import os import os
import sys import sys
import struct import struct
import inspect
import itertools
from ._compat import raw_input, text_type, string_types, \ from ._compat import raw_input, text_type, string_types, \
isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN
from .utils import echo from .utils import echo
from .exceptions import Abort, UsageError from .exceptions import Abort, UsageError
from .types import convert_type from .types import convert_type, Choice, Path
from .globals import resolve_color_default from .globals import resolve_color_default
@ -14,8 +16,25 @@ from .globals import resolve_color_default
# functions to customize how they work. # functions to customize how they work.
visible_prompt_func = raw_input visible_prompt_func = raw_input
_ansi_colors = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', _ansi_colors = {
'cyan', 'white', 'reset') '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' _ansi_reset_all = '\033[0m'
@ -24,23 +43,27 @@ def hidden_prompt_func(prompt):
return getpass.getpass(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 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: if default is not None and show_default:
prompt = '%s [%s]' % (prompt, default) prompt = '%s [%s]' % (prompt, default)
return prompt + suffix return prompt + suffix
def prompt(text, default=None, hide_input=False, def prompt(text, default=None, hide_input=False, confirmation_prompt=False,
confirmation_prompt=False, type=None, type=None, value_proc=None, prompt_suffix=': ', show_default=True,
value_proc=None, prompt_suffix=': ', err=False, show_choices=True):
show_default=True, err=False):
"""Prompts a user for input. This is a convenience function that can """Prompts a user for input. This is a convenience function that can
be used to prompt a user for input later. be used to prompt a user for input later.
If the user aborts the input by sending a interrupt signal, this If the user aborts the input by sending a interrupt signal, this
function will catch it and raise a :exc:`Abort` exception. function will catch it and raise a :exc:`Abort` exception.
.. versionadded:: 7.0
Added the show_choices parameter.
.. versionadded:: 6.0 .. versionadded:: 6.0
Added unicode support for cmd.exe on Windows. 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 show_default: shows or hides the default value in the prompt.
:param err: if set to true the file defaults to ``stderr`` instead of :param err: if set to true the file defaults to ``stderr`` instead of
``stdout``, the same as with echo. ``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 result = None
@ -82,17 +109,18 @@ def prompt(text, default=None, hide_input=False,
if value_proc is None: if value_proc is None:
value_proc = convert_type(type, default) 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:
while 1: while 1:
value = prompt_func(prompt) value = prompt_func(prompt)
if value: if value:
break 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: elif default is not None:
if isinstance(value_proc, Path):
# validate Path default value(exists, dir_okay etc.)
value = default
break
return default return default
try: try:
result = value_proc(value) result = value_proc(value)
@ -166,8 +194,14 @@ def get_terminal_size():
sz = shutil_get_terminal_size() sz = shutil_get_terminal_size()
return sz.columns, sz.lines 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: 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): def ioctl_gwinsz(fd):
try: try:
@ -195,22 +229,33 @@ def get_terminal_size():
return int(cr[1]), int(cr[0]) 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 """This function takes a text and shows it via an environment specific
pager on stdout. pager on stdout.
.. versionchanged:: 3.0 .. versionchanged:: 3.0
Added the `color` flag. 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 :param color: controls if the pager supports ANSI colors or not. The
default is autodetection. default is autodetection.
""" """
color = resolve_color_default(color) 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 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, 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`` * ``magenta``
* ``cyan`` * ``cyan``
* ``white`` (might be light gray) * ``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) * ``reset`` (reset the color code only)
.. versionadded:: 2.0 .. versionadded:: 2.0
.. versionadded:: 7.0
Added support for bright colors.
:param text: the string to style with ansi codes. :param text: the string to style with ansi codes.
:param fg: if provided this will become the foreground color. :param fg: if provided this will become the foreground color.
:param bg: if provided this will become the background 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 = [] bits = []
if fg: if fg:
try: try:
bits.append('\033[%dm' % (_ansi_colors.index(fg) + 30)) bits.append('\033[%dm' % (_ansi_colors[fg]))
except ValueError: except KeyError:
raise TypeError('Unknown color %r' % fg) raise TypeError('Unknown color %r' % fg)
if bg: if bg:
try: try:
bits.append('\033[%dm' % (_ansi_colors.index(bg) + 40)) bits.append('\033[%dm' % (_ansi_colors[bg] + 10))
except ValueError: except KeyError:
raise TypeError('Unknown color %r' % bg) raise TypeError('Unknown color %r' % bg)
if bold is not None: if bold is not None:
bits.append('\033[%dm' % (1 if bold else 22)) bits.append('\033[%dm' % (1 if bold else 22))
@ -405,7 +461,7 @@ def unstyle(text):
return strip_ansi(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 """This function combines :func:`echo` and :func:`style` into one
call. As such the following two calls are the same:: 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 .. 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, def edit(text=None, editor=None, env=None, require_save=True,
@ -466,7 +524,7 @@ def launch(url, wait=False, locate=False):
Examples:: Examples::
click.launch('http://click.pocoo.org/') click.launch('https://click.palletsprojects.com/')
click.launch('/my/downloaded/file', locate=True) click.launch('/my/downloaded/file', locate=True)
.. versionadded:: 2.0 .. versionadded:: 2.0
@ -499,6 +557,10 @@ def getchar(echo=False):
Note that this will always read from the terminal, even if something Note that this will always read from the terminal, even if something
is piped into the standard input. 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 .. versionadded:: 2.0
:param echo: if set to `True`, the character read will also show up on :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) 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): def pause(info='Press any key to continue ...', err=False):
"""This command stops execution and waits for the user to press any """This command stops execution and waits for the user to press any
key to continue. This is similar to the Windows batch "pause" key to continue. This is similar to the Windows batch "pause"

View File

@ -3,8 +3,9 @@ import sys
import shutil import shutil
import tempfile import tempfile
import contextlib 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 # If someone wants to vendor click, we want to ensure the
@ -72,27 +73,44 @@ def make_input_stream(input, charset):
class Result(object): class Result(object):
"""Holds the captured result of an invoked CLI script.""" """Holds the captured result of an invoked CLI script."""
def __init__(self, runner, output_bytes, exit_code, exception, def __init__(self, runner, stdout_bytes, stderr_bytes, exit_code,
exc_info=None): exception, exc_info=None):
#: The runner that created the result #: The runner that created the result
self.runner = runner self.runner = runner
#: The output as bytes. #: The standard output as bytes.
self.output_bytes = output_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. #: The exit code as integer.
self.exit_code = exit_code self.exit_code = exit_code
#: The exception that happend if one did. #: The exception that happened if one did.
self.exception = exception self.exception = exception
#: The traceback #: The traceback
self.exc_info = exc_info self.exc_info = exc_info
@property @property
def output(self): def output(self):
"""The output as unicode string.""" """The (standard) output as unicode string."""
return self.output_bytes.decode(self.runner.charset, 'replace') \ 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') .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): def __repr__(self):
return '<Result %s>' % ( return '<%s %s>' % (
type(self).__name__,
self.exception and repr(self.exception) or 'okay', self.exception and repr(self.exception) or 'okay',
) )
@ -111,14 +129,21 @@ class CliRunner(object):
to stdout. This is useful for showing examples in to stdout. This is useful for showing examples in
some circumstances. Note that regular prompts some circumstances. Note that regular prompts
will automatically echo the input. 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: if charset is None:
charset = 'utf-8' charset = 'utf-8'
self.charset = charset self.charset = charset
self.env = env or {} self.env = env or {}
self.echo_stdin = echo_stdin self.echo_stdin = echo_stdin
self.mix_stderr = mix_stderr
def get_default_prog_name(self, cli): def get_default_prog_name(self, cli):
"""Given a command object it will return the default program name """Given a command object it will return the default program name
@ -163,16 +188,27 @@ class CliRunner(object):
env = self.make_env(env) env = self.make_env(env)
if PY2: if PY2:
sys.stdout = sys.stderr = bytes_output = StringIO() bytes_output = StringIO()
if self.echo_stdin: if self.echo_stdin:
input = EchoingStdin(input, bytes_output) input = EchoingStdin(input, bytes_output)
sys.stdout = bytes_output
if not self.mix_stderr:
bytes_error = StringIO()
sys.stderr = bytes_error
else: else:
bytes_output = io.BytesIO() bytes_output = io.BytesIO()
if self.echo_stdin: if self.echo_stdin:
input = EchoingStdin(input, bytes_output) input = EchoingStdin(input, bytes_output)
input = io.TextIOWrapper(input, encoding=self.charset) input = io.TextIOWrapper(input, encoding=self.charset)
sys.stdout = sys.stderr = io.TextIOWrapper( sys.stdout = io.TextIOWrapper(
bytes_output, encoding=self.charset) 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 sys.stdin = input
@ -196,6 +232,7 @@ class CliRunner(object):
return char return char
default_color = color default_color = color
def should_strip_ansi(stream=None, color=None): def should_strip_ansi(stream=None, color=None):
if color is None: if color is None:
return not default_color return not default_color
@ -221,7 +258,7 @@ class CliRunner(object):
pass pass
else: else:
os.environ[key] = value os.environ[key] = value
yield bytes_output yield (bytes_output, not self.mix_stderr and bytes_error)
finally: finally:
for key, value in iteritems(old_env): for key, value in iteritems(old_env):
if value is None: if value is None:
@ -241,7 +278,7 @@ class CliRunner(object):
clickpkg.formatting.FORCED_WIDTH = old_forced_width clickpkg.formatting.FORCED_WIDTH = old_forced_width
def invoke(self, cli, args=None, input=None, env=None, 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 """Invokes a command in an isolated environment. The arguments are
forwarded directly to the command line script, the `extra` keyword forwarded directly to the command line script, the `extra` keyword
arguments are passed to the :meth:`~clickpkg.Command.main` function of arguments are passed to the :meth:`~clickpkg.Command.main` function of
@ -260,7 +297,10 @@ class CliRunner(object):
The ``color`` parameter was added. The ``color`` parameter was added.
:param cli: the command to invoke :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 input: the input data for `sys.stdin`.
:param env: the environment overrides. :param env: the environment overrides.
:param catch_exceptions: Whether to catch any other exceptions than :param catch_exceptions: Whether to catch any other exceptions than
@ -270,36 +310,48 @@ class CliRunner(object):
application can still override this explicitly. application can still override this explicitly.
""" """
exc_info = None 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 exception = None
exit_code = 0 exit_code = 0
if isinstance(args, string_types):
args = shlex.split(args)
try: try:
cli.main(args=args or (), prog_name = extra.pop("prog_name")
prog_name=self.get_default_prog_name(cli), **extra) 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: 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 exception = e
exc_info = sys.exc_info()
exit_code = e.code
if not isinstance(exit_code, int): if not isinstance(exit_code, int):
sys.stdout.write(str(exit_code)) sys.stdout.write(str(exit_code))
sys.stdout.write('\n') sys.stdout.write('\n')
exit_code = 1 exit_code = 1
except Exception as e: except Exception as e:
if not catch_exceptions: if not catch_exceptions:
raise raise
exception = e exception = e
exit_code = -1 exit_code = 1
exc_info = sys.exc_info() exc_info = sys.exc_info()
finally: finally:
sys.stdout.flush() sys.stdout.flush()
output = out.getvalue() stdout = outstreams[0].getvalue()
stderr = outstreams[1] and outstreams[1].getvalue()
return Result(runner=self, return Result(runner=self,
output_bytes=output, stdout_bytes=stdout,
stderr_bytes=stderr,
exit_code=exit_code, exit_code=exit_code,
exception=exception, exception=exception,
exc_info=exc_info) exc_info=exc_info)

View File

@ -1,5 +1,6 @@
import os import os
import stat import stat
from datetime import datetime
from ._compat import open_stream, text_type, filename_to_ui, \ from ._compat import open_stream, text_type, filename_to_ui, \
get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2 get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2
@ -126,34 +127,54 @@ class StringParamType(ParamType):
class Choice(ParamType): class Choice(ParamType):
"""The choice type allows a value to be checked against a fixed set of """The choice type allows a value to be checked against a fixed set
supported values. All of these values have to be strings. 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. See :ref:`choice-opts` for an example.
:param case_sensitive: Set to false to make choices case
insensitive. Defaults to true.
""" """
name = 'choice' name = 'choice'
def __init__(self, choices): def __init__(self, choices, case_sensitive=True):
self.choices = choices self.choices = choices
self.case_sensitive = case_sensitive
def get_metavar(self, param): def get_metavar(self, param):
return '[%s]' % '|'.join(self.choices) return '[%s]' % '|'.join(self.choices)
def get_missing_message(self, param): 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): def convert(self, value, param, ctx):
# Exact match # Exact match
if value in self.choices: if value in self.choices:
return value 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 \ if ctx is not None and \
ctx.token_normalize_func is not None: ctx.token_normalize_func is not None:
value = ctx.token_normalize_func(value) normed_value = ctx.token_normalize_func(value)
for choice in self.choices: normed_choices = [ctx.token_normalize_func(choice) for choice in
if ctx.token_normalize_func(choice) == value: self.choices]
return choice
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)' % self.fail('invalid choice: %s. (choose from %s)' %
(value, ', '.join(self.choices)), param, ctx) (value, ', '.join(self.choices)), param, ctx)
@ -162,6 +183,59 @@ class Choice(ParamType):
return 'Choice(%r)' % list(self.choices) 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): class IntParamType(ParamType):
name = 'integer' name = 'integer'
@ -214,23 +288,6 @@ class IntRange(IntParamType):
return 'IntRange(%r, %r)' % (self.min, self.max) 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): class FloatParamType(ParamType):
name = 'float' name = 'float'
@ -245,6 +302,62 @@ class FloatParamType(ParamType):
return 'FLOAT' 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): class UUIDParameterType(ParamType):
name = 'uuid' name = 'uuid'
@ -273,9 +386,12 @@ class File(ParamType):
opened in binary mode or for writing. The encoding parameter can be used opened in binary mode or for writing. The encoding parameter can be used
to force a specific encoding. to force a specific encoding.
The `lazy` flag controls if the file should be opened immediately or The `lazy` flag controls if the file should be opened immediately or upon
upon first IO. The default is to be non lazy for standard input and first IO. The default is to be non-lazy for standard input and output
output streams as well as files opened for reading, lazy otherwise. 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 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 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 readable: if true, a readable check is performed.
:param resolve_path: if this is true, then the path is fully resolved :param resolve_path: if this is true, then the path is fully resolved
before the value is passed onwards. This means 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 :param allow_dash: If this is set to `True`, a single dash to indicate
standard streams is permitted. standard streams is permitted.
:param type: optionally a string type that should be used to :param path_type: optionally a string type that should be used to
represent the path. The default is `None` which represent the path. The default is `None` which
means the return value will be either bytes or means the return value will be either bytes or
unicode depending on what makes most sense given the unicode depending on what makes most sense given the
input data Click deals with. input data Click deals with.
""" """
envvar_list_splitter = os.path.pathsep envvar_list_splitter = os.path.pathsep
@ -384,7 +502,7 @@ class Path(ParamType):
if self.file_okay and not self.dir_okay: if self.file_okay and not self.dir_okay:
self.name = 'file' self.name = 'file'
self.path_type = '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.name = 'directory'
self.path_type = 'Directory' self.path_type = 'Directory'
else: else:

View File

@ -43,6 +43,7 @@ def make_str(value):
def make_default_short_help(help, max_length=45): def make_default_short_help(help, max_length=45):
"""Return a condensed version of help string."""
words = help.split() words = help.split()
total_length = 0 total_length = 0
result = [] 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 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 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 best to not fail. As of Click 6.0 this includes support for unicode
output on the Windows console. 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 - hide ANSI codes automatically if the destination file is not a
terminal. terminal.
.. _colorama: http://pypi.python.org/pypi/colorama .. _colorama: https://pypi.org/project/colorama/
.. versionchanged:: 6.0 .. versionchanged:: 6.0
As of Click 6.0 the echo function will properly support unicode 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( return os.path.join(
os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')),
_posixify(app_name)) _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
View 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 &lt;em&gt;World&lt;/em&gt;!')
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 &amp; bar</em>')
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
"""
__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 &raquo; <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 &raquo;\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")

View 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

View 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,
}

View 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("&", "&amp;")
.replace(">", "&gt;")
.replace("<", "&lt;")
.replace("'", "&#39;")
.replace('"', "&#34;")
)
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('&lt;User 1&gt;')
>>> escape(str(value))
Markup('&amp;lt;User 1&amp;gt;')
>>> escape(soft_unicode(value))
Markup('&lt;User 1&gt;')
"""
if not isinstance(s, text_type):
s = text_type(s)
return s

423
libs/markupsafe/_speedups.c Normal file
View 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("&#34;");
escaped_chars_repl['\''] = UNICHR("&#39;");
escaped_chars_repl['&'] = UNICHR("&amp;");
escaped_chars_repl['<'] = UNICHR("&lt;");
escaped_chars_repl['>'] = UNICHR("&gt;");
/* 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

View File

@ -433,6 +433,13 @@
</div> </div>
</div> </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>
<div class="middle aligned row"> <div class="middle aligned row">
@ -445,12 +452,12 @@
class="ui fluid search selection dropdown"> class="ui fluid search selection dropdown">
<option value="">Languages</option> <option value="">Languages</option>
{% set enabled_languages = [] %} {% set enabled_languages = [] %}
{%for language in settings_languages%} {% for language in settings_languages %}
<option value="{{ language['code2'] }}">{{ language['name'] }}</option> <option value="{{ language['code2'] }}">{{ language['name'] }}</option>
{%if language['enabled'] == True%} {% if language['enabled'] == True %}
{{ enabled_languages.append(language['code2']|string)}} {{ enabled_languages.append(language['code2']|string) }}
{%endif%} {% endif %}
{%endfor%} {% endfor %}
</select> </select>
</div> </div>
</div> </div>