Source code for Exscript.util.interact

#
# Copyright (C) 2010-2017 Samuel Abels
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
Tools for interacting with the user on the command line.
"""
from __future__ import print_function, absolute_import
from future import standard_library
standard_library.install_aliases()
from builtins import input
from builtins import str
from builtins import object
import os
import sys
import getpass
import configparser
import codecs
import shutil
from tempfile import NamedTemporaryFile
from .. import Account
from .cast import to_list


[docs]class InputHistory(object): """ When prompting a user for input it is often useful to record his input in a file, and use previous input as a default value. This class allows for recording user input in a config file to allow for such functionality. """
[docs] def __init__(self, filename='~/.exscript_history', section=os.path.basename(sys.argv[0])): """ Constructor. The filename argument allows for listing on or more config files, and is passed to Python's RawConfigParser; please consult the documentation of RawConfigParser.read() if you require more information. The optional section argument allows to specify a section under which the input is stored in the config file. The section defaults to the name of the running script. Silently creates a tempfile if the given file can not be opened, such that the object behavior does not change, but the history is not remembered across instances. :type filename: str|list(str) :param filename: The config file. :type section: str :param section: The section in the configfile. """ self.section = section self.parser = configparser.ConfigParser() filename = os.path.expanduser(filename) try: self.file = open(filename, 'a+') except IOError: import warnings warnings.warn('could not open %s, using tempfile' % filename) self.file = NamedTemporaryFile() self.parser.readfp(self.file) if not self.parser.has_section(self.section): self.parser.add_section(self.section)
[docs] def get(self, key, default=None): """ Returns the input with the given key from the section that was passed to the constructor. If either the section or the key are not found, the default value is returned. :type key: str :param key: The key for which to return a value. :type default: str|object :param default: The default value that is returned. :rtype: str|object :return: The value from the config file, or the default. """ if not self.parser: return default try: return self.parser.get(self.section, key) except (configparser.NoSectionError, configparser.NoOptionError): return default
[docs] def set(self, key, value): """ Saves the input with the given key in the section that was passed to the constructor. If either the section or the key are not found, they are created. Does nothing if the given value is None. :type key: str :param key: The key for which to define a value. :type value: str|None :param value: The value that is defined, or None. :rtype: str|None :return: The given value. """ if value is None: return None self.parser.set(self.section, key, value) # Unfortunately ConfigParser attempts to write a string to the file # object, and NamedTemporaryFile uses binary mode. So we nee to create # the tempfile, and then re-open it. with NamedTemporaryFile(delete=False) as tmpfile: pass with codecs.open(tmpfile.name, 'w', encoding='utf8') as fp: self.parser.write(fp) self.file.close() shutil.move(tmpfile.name, self.file.name) self.file = open(self.file.name) return value
[docs]def prompt(key, message, default=None, doverh=True, strip=True, check=None, history=None): """ Prompt the user for input. This function is similar to Python's built in raw_input, with the following differences: - You may specify a default value that is returned if the user presses "enter" without entering anything. - The user's input is recorded in a config file, and offered as the default value the next time this function is used (based on the key argument). The config file is based around the :class:`InputHistory`. If a history object is not passed in the history argument, a new one will be created. The key argument specifies under which name the input is saved in the config file. The given default value is only used if a default was not found in the history. The strip argument specifies that the returned value should be stripped of whitespace (default). The check argument allows for validating the input; if the validation fails, the user is prompted again before the value is stored in the InputHistory. Example usage:: def validate(input): if len(input) < 4: return 'Please enter at least 4 characters!' value = prompt('test', 'Enter a value', 'My Default', check = validate) print('You entered:', value) This leads to the following output:: Please enter a value [My Default]: abc Please enter at least 4 characters! Please enter a value [My Default]: Foobar You entered: Foobar The next time the same code is started, the input 'Foobar' is remembered:: Please enter a value [Foobar]: (enters nothing) You entered: Foobar :type key: str :param key: The key under which to store the input in the :class:`InputHistory`. :type message: str :param message: The user prompt. :type default: str|None :param default: The offered default if none was found in the history. :type doverh: bool :param doverh: Whether to prefer default value over history value. :type strip: bool :param strip: Whether to remove whitespace from the input. :type check: callable :param check: A function that is called for validating the input. :type history: :class:`InputHistory` or None :param history: The history used for recording default values, or None. """ if history is None: history = InputHistory() if not doverh or default is None: default = history.get(key, str(default)) while True: if default is None: value = input('%s: ' % message) else: value = input('%s [%s]: ' % (message, default)) or default if strip and isinstance(value, str): value = value.strip() if not check: break errors = check(value) if errors: print('\n'.join(to_list(errors))) else: break history.set(key, value) return value
[docs]def get_filename(key, message, default=None, history=None): """ Like :meth:`prompt`, but only accepts the name of an existing file as an input. :type key: str :param key: The key under which to store the input in the :class:`InputHistory`. :type message: str :param message: The user prompt. :type default: str|None :param default: The offered default if none was found in the history. :type history: :class:`InputHistory` or None :param history: The history used for recording default values, or None. """ def _validate(string): if not os.path.isfile(string): return 'File not found. Please enter a filename.' return prompt(key, message, default, True, _validate, history)
[docs]def get_user(prompt=None): """ Prompts the user for his login name, defaulting to the USER environment variable. Returns a string containing the username. May throw an exception if EOF is given by the user. :type prompt: str|None :param prompt: The user prompt or the default one if None. :rtype: string :return: A username. """ # Read username and password. try: env_user = getpass.getuser() except KeyError: env_user = '' if prompt is None: prompt = "Please enter your user name" if env_user is None or env_user == '': user = input('%s: ' % prompt) else: user = input('%s [%s]: ' % (prompt, env_user)) if user == '': user = env_user return user
[docs]def get_login(): """ Prompts the user for the login name using get_user(), and also asks for the password. Returns a tuple containing the username and the password. May throw an exception if EOF is given by the user. :rtype: (string, string) :return: A tuple containing the username and the password. """ user = get_user() password = getpass.getpass('Please enter your password: ') return user, password
[docs]def read_login(): """ Like get_login(), but returns an Account object. :rtype: Account :return: A new account. """ user, password = get_login() return Account(user, password)