From 1c1511a1d25edbfbf38838f7adf883508df27396 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 28 Jul 2018 11:05:29 -0700 Subject: [PATCH] Add code + tests for adding items to a list --- integration-tests/test_hub.py | 36 ++++++++++++++++++- tests/test_config.py | 30 ++++++++++++++++ tljh/config.py | 67 ++++++++++++++++++++++++++++++++--- 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index ddb1980f6..a9c15e623 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -4,7 +4,10 @@ import secrets import pytest from functools import partial +import asyncio import pwd +import grp +import sys def test_hub_up(): @@ -29,4 +32,35 @@ async def test_user_code_execute(): await u.assert_code_output("5 * 4", "20", 5, 5) # Assert that the user exists - assert pwd.getpwnam(f'jupyter-{username}') is not None \ No newline at end of file + assert pwd.getpwnam(f'jupyter-{username}') is not None + + +@pytest.mark.asyncio +async def test_user_admin_code(): + """ + User logs in, starts a server & executes code + """ + # This *must* be localhost, not an IP + # aiohttp throws away cookies if we are connecting to an IP! + hub_url = 'http://localhost' + username = secrets.token_hex(8) + + tljh_config_path = [sys.executable, '-m', 'tljh.config'] + + assert 0 == await (await asyncio.create_subprocess_exec(*tljh_config_path, 'add-item', 'users.admin', username)).wait() + assert 0 == await (await asyncio.create_subprocess_exec(*tljh_config_path, 'reload')).wait() + + # FIXME: wait for reload to finish & hub to come up + # Should be present in reload + await asyncio.sleep(1) + async with User(username, hub_url, partial(login_dummy, password='')) as u: + await u.login() + await u.ensure_server() + await u.start_kernel() + await u.assert_code_output("5 * 4", "20", 5, 5) + + # Assert that the user exists + assert pwd.getpwnam(f'jupyter-{username}') is not None + + # Assert that the user has admin rights + assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index 4d8a5910c..c0abdce79 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -51,6 +51,36 @@ def test_set_overwrite(): assert new_conf == {'a': 'hi'} +def test_add_to_config_one_level(): + conf = {} + + new_conf = config.add_item_to_config(conf, 'a.b', 'c') + assert new_conf == { + 'a': {'b': ['c']} + } + + +def test_add_to_config_zero_level(): + conf = {} + + new_conf = config.add_item_to_config(conf, 'a', 'b') + assert new_conf == { + 'a': ['b'] + } + +def test_add_to_config_multiple(): + conf = {} + + new_conf = config.add_item_to_config(conf, 'a.b.c', 'd') + assert new_conf == { + 'a': {'b': {'c': ['d']}} + } + + new_conf = config.add_item_to_config(new_conf, 'a.b.c', 'e') + assert new_conf == { + 'a': {'b': {'c': ['d', 'e']}} + } + def test_show_config(): """ Test stdout output when showing config diff --git a/tljh/config.py b/tljh/config.py index 9f1c38fd2..770896e3d 100644 --- a/tljh/config.py +++ b/tljh/config.py @@ -27,14 +27,14 @@ def set_item_in_config(config, property_path, value): config is not mutated. - propert_path is a series of dot separated values. Any part of the path + property_path is a series of dot separated values. Any part of the path that does not exist is created. """ path_components = property_path.split('.') # Mutate a copy of the config, not config itself cur_part = config_copy = deepcopy(config) - for i in range(len(path_components)): + for i, cur_path in enumerate(path_components): cur_path = path_components[i] if i == len(path_components) - 1: # Final component @@ -49,6 +49,34 @@ def set_item_in_config(config, property_path, value): return config_copy +def add_item_to_config(config, property_path, value): + """ + Add an item to a list in config. + """ + path_components = property_path.split('.') + + # Mutate a copy of the config, not config itself + cur_part = config_copy = deepcopy(config) + for i, cur_path in enumerate(path_components): + if i == len(path_components) - 1: + # Final component, it must be a list and we append to it + print('l', cur_path, cur_part) + if cur_path not in cur_part or not isinstance(cur_part[cur_path], list): + cur_part[cur_path] = [] + cur_part = cur_part[cur_path] + + cur_part.append(value) + else: + # If we are asked to create new non-leaf nodes, we will always make them dicts + # This means setting is *destructive* - will replace whatever is down there! + print('p', cur_path, cur_part) + if cur_path not in cur_part or not isinstance(cur_part[cur_path], dict): + cur_part[cur_path] = {} + cur_part = cur_part[cur_path] + + return config_copy + + def show_config(config_path): """ Pretty print config from given config_path @@ -80,6 +108,22 @@ def set_config_value(config_path, key_path, value): yaml.dump(config, f) +def add_config_value(config_path, key_path, value): + """ + Add value to list at key_path + """ + # FIXME: Have a file lock here + # FIXME: Validate schema here + try: + with open(config_path) as f: + config = yaml.load(f) + except FileNotFoundError: + config = {} + + config = add_item_to_config(config, key_path, value) + + with open(config_path, 'w') as f: + yaml.dump(config, f) def reload_component(component): """ @@ -123,6 +167,19 @@ def main(): help='Value ot set the configuration key to' ) + add_item_parser = subparsers.add_parser( + 'add-item', + help='Add a value to a list for a configuration property' + ) + add_item_parser.add_argument( + 'key_path', + help='Dot separated path to configuration key to set' + ) + add_item_parser.add_argument( + 'value', + help='Value ot set the configuration key to' + ) + reload_parser = subparsers.add_parser( 'reload', help='Reload a component to apply configuration change' @@ -141,10 +198,10 @@ def main(): show_config(args.config_path) elif args.action == 'set': set_config_value(args.config_path, args.key_path, args.value) + elif args.action == 'add-item': + add_config_value(args.config_path, args.key_path, args.value) elif args.action == 'reload': reload_component(args.component) if __name__ == '__main__': - main() - - + main() \ No newline at end of file