diff --git a/fencepy/helpers.py b/fencepy/helpers.py index 227aac6..e34c544 100644 --- a/fencepy/helpers.py +++ b/fencepy/helpers.py @@ -91,9 +91,20 @@ def findpybin(name, start): @contextmanager def redirected(out=sys.stdout, err=sys.stderr): + """Temporarily redirect stdout and/or stderr""" saved = sys.stdout, sys.stderr sys.stdout, sys.stderr = out, err try: yield finally: sys.stdout, sys.stderr = saved + + +def str2bool(value): + """Convert various acceptable string values into a bool""" + if type(value) in (str, unicode): + if value.lower() in ('true', 't', 'yes', 'y', '1'): + return True + elif value.lower() in ('false', 'f', 'no', 'n', '0'): + return False + raise ValueError('{0} is not an acceptable boolean value'.format(value)) diff --git a/fencepy/main.py b/fencepy/main.py index 69347ff..d36a7c7 100644 --- a/fencepy/main.py +++ b/fencepy/main.py @@ -9,16 +9,15 @@ import shutil import sys from . import plugins -from .helpers import getoutputoserror, findpybin +from .helpers import getoutputoserror, findpybin, str2bool -FENCEPY_ROOT = os.path.join(os.path.expanduser('~'), '.fencepy') -if not os.path.exists(FENCEPY_ROOT): - os.mkdir(FENCEPY_ROOT) +if sys.version.startswith('2'): + from ConfigParser import SafeConfigParser +else: + from configparser import SafeConfigParser +from logging.handlers import RotatingFileHandler import logging as l -l.basicConfig(filename=os.path.join(FENCEPY_ROOT, 'fencepy.log')) - -VENV_ROOT = os.path.join(FENCEPY_ROOT, 'virtualenvs') def _get_args(): @@ -45,20 +44,35 @@ def _get_args(): help='Use DIR as the root path for the fenced environment instead of the CWD') paths.add_argument('-D', '--virtualenv-dir', metavar='DIR', help='Use DIR as the root directory for the virtualenv instead of the default') + paths.add_argument('-F', '--fencepy-root', metavar='DIR', + help='Use DIR as the root directory for all fencepy ops instead of the default') # miscellaneous functionality misc = parser.add_argument_group('miscellaneous') - misc.add_argument('-P', '--plugins', - help='Comma-separated list of plugins to apply, defaults to all plugins') + misc.add_argument('-P', '--plugins', default='', dest='pluginstr', + help='Comma-separated list of plugins to apply, override any settings in config file') + misc.add_argument('-C', '--config-file', + help='Config file to use instead of ~/.fencepy/fencepy.conf') misc.add_argument('-v', '--verbose', '--debug', action='store_true', help='Print debug logging') misc.add_argument('-q', '--quiet', action='store_true', help='Silence all console output') misc.add_argument("-h", "--help", action="help", - help="show this help message and exit") + help="Show this help message and exit") + args = vars(parser.parse_args()) + # set up the root directory + if not args['fencepy_root']: + args['fencepy_root'] = os.path.join(os.path.expanduser('~'), '.fencepy') + if not os.path.exists(args['fencepy_root']): + os.mkdir(args['fencepy_root']) + # set up logging + f = l.Formatter('%(asctime)s [%(levelname)s] %(module)s: %(message)s') + h = RotatingFileHandler(os.path.join(args['fencepy_root'], 'fencepy.log')) + h.setFormatter(f) + l.getLogger('').addHandler(h) if not args['quiet']: f = l.Formatter('[%(levelname)s] %(message)s') h = l.StreamHandler() @@ -87,11 +101,12 @@ def _get_args(): # reset the virtualenv root, if necessary if not args['virtualenv_dir']: + venv_root = os.path.join(args['fencepy_root'], 'virtualenvs') # if we're one directory below the root, this logic needs to work differently parent = os.path.dirname(args['dir']) if parent in ('/', os.path.splitdrive(parent)[0]): - args['virtualenv_dir'] = os.path.join(VENV_ROOT, os.path.basename(args['dir'])) + args['virtualenv_dir'] = os.path.join(venv_root, os.path.basename(args['dir'])) else: # need the realpath here because in some circumstances windows paths get passed @@ -101,17 +116,38 @@ def _get_args(): if tokens[-1] == '': tokens = tokens[:-1] prjpart = '.'.join([os.path.basename(args['dir']), '.'.join([d[0] for d in tokens])]) - args['virtualenv_dir'] = os.path.join(VENV_ROOT, '-'.join((prjpart, args['pyversion']))) + args['virtualenv_dir'] = os.path.join(venv_root, '-'.join((prjpart, args['pyversion']))) # set the mode properly modecount = [args['activate'], args['create'], args['erase']].count(True) if modecount > 1: - l.error("please specify only one of -a, -c, -e") + raise RuntimeError('please specify only one of -a, -c, -e') elif modecount == 0: args['activate'] = True - # plugins - args['plugins'] = args['plugins'].split(',') if args['plugins'] else ['sublime', 'requirements', 'ps1'] + # only populate the parser if there's a valid file + config = SafeConfigParser() + readconf = True + if args['config_file']: + if not os.path.exists(args['config_file']): + raise IOError('specified config file {0} does not exist'.format(args['config_file'])) + else: + args['config_file'] = os.path.join(args['fencepy_root'], 'fencepy.conf') + if not os.path.exists(args['config_file']): + readconf = False + if readconf: + config.read(args['config_file']) + + # plugins -- all false means all true + if args['pluginstr']: + args['plugins'] = dict((key, key in args['pluginstr'].split(',')) for key in plugins.PLUGINS) + else: + args['plugins'] = dict((key, True) for key in plugins.PLUGINS) + if config.has_section('plugins'): + for key, value in config.items('plugins'): + if key not in plugins.PLUGINS: + raise KeyError('invalid configuration: {0} is not a valid plugin'.format(key)) + args['plugins'][key] = str2bool(value) return args @@ -185,7 +221,7 @@ def _create(args): return 1 # plugins - for plugin_name in args['plugins']: + for plugin_name in [key for key, value in args['plugins'].items() if value]: if getattr(plugins, 'install_{0}'.format(plugin_name))(args) == 1: return 1 diff --git a/fencepy/plugins.py b/fencepy/plugins.py index e78c89a..ad75fa5 100644 --- a/fencepy/plugins.py +++ b/fencepy/plugins.py @@ -14,6 +14,8 @@ import logging l = logging.getLogger('') +PLUGINS = ['requirements', 'sublime', 'ps1'] + def install_requirements(args): """Install requirements out of requirements.txt, if it exists""" diff --git a/tests/test_fencepy.py b/tests/test_fencepy.py index 852bd3e..2b0e67e 100644 --- a/tests/test_fencepy.py +++ b/tests/test_fencepy.py @@ -21,19 +21,22 @@ class TestFencepy(TestCase): def _fence(self, *args): - sys.argv = ['fencepy'] + list(args) + # always include the overridden fencepy directory + sys.argv = ['fencepy', '-F', self.fdir] + list(args) ret = fencepy.fence() sys.argv = ORIGINAL_ARGV return ret def _get_arg_dict(self, *args): - sys.argv = ['fencepy', '-q'] + list(args) + # always include the overridden fencepy directory + sys.argv = ['fencepy', '-F', self.fdir, '-q'] + list(args) ret = fencepy.main._get_args() sys.argv = ORIGINAL_ARGV return ret def setUp(self): self.tempdir = tempfile.mkdtemp() + self.fdir = os.path.join(self.tempdir, 'fencepy') self.pname = str(uuid.uuid4()) self.pdir = os.path.join(self.tempdir, self.pname) os.mkdir(self.pdir) @@ -154,5 +157,25 @@ def test_activate_bash(self): self._test_activate('bash', 'activate') def test_multiple_modes(self): - ret = self._fence('-a', '-c', '-e') - self.assertEqual(ret, 1, 'multiple modes are not allowed') + with raises(RuntimeError): + ret = self._fence('-a', '-c', '-e') + + def test_specify_plugins(self): + self.assertTrue(self.default_args['plugins']['requirements']) + self.assertTrue(self.default_args['plugins']['ps1']) + self.assertTrue(self.default_args['plugins']['sublime']) + args = self._get_arg_dict('-P', 'requirements') + self.assertTrue(args['plugins']['requirements']) + self.assertFalse(args['plugins']['ps1']) + self.assertFalse(args['plugins']['sublime']) + args = self._get_arg_dict('-P', 'requirements,ps1') + self.assertTrue(args['plugins']['requirements']) + self.assertTrue(args['plugins']['ps1']) + self.assertFalse(args['plugins']['sublime']) + + lines = ['[plugins]', 'requirements = false'] + open(os.path.join(self.fdir, 'fencepy.conf'), 'w').write(os.linesep.join(lines)) + args = self._get_arg_dict() + self.assertFalse(args['plugins']['requirements']) + self.assertTrue(args['plugins']['ps1']) + self.assertTrue(args['plugins']['sublime']) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e45939c..8d7b82f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -44,3 +44,12 @@ def test_pseudo_merge_dict_bad_dto(self): } with raises(ValueError): helpers.pseudo_merge_dict(dto, dfrom) + + def test_str2bool(self): + for true in ('True', 'true', 'TRUE', 'T', 't', 'Yes', 'yes', 'YES', 'Y', 'y', '1', 'truE', 'yEs'): + self.assertTrue(helpers.str2bool(true)) + for false in ('False', 'false', 'FALSE', 'F', 'f', 'No', 'no', 'NO', 'N', 'n', '0', 'faLsE', 'nO'): + self.assertFalse(helpers.str2bool(false)) + for error in ('bad', '10', None, 1, 'truee'): + with raises(ValueError): + helpers.str2bool(error)