diff --git a/jupyterlab_server/settings_utils.py b/jupyterlab_server/settings_utils.py index bcd914d1..ed850a6c 100755 --- a/jupyterlab_server/settings_utils.py +++ b/jupyterlab_server/settings_utils.py @@ -266,26 +266,50 @@ def _path(root_dir, schema_name, make_dirs=False, extension='.json'): return path - def _get_overrides(app_settings_dir): - """Get overrides settings from `app_settings_dir`.""" + """Get overrides settings from `app_settings_dir`. + + The ordering of paths is: + - {app_settings_dir}/overrides.d/*.{json,json5} (many, namespaced by package) + - {app_settings_dir}/overrides.{json,json5} (singleton, owned by the user) + """ overrides, error = {}, "" - overrides_path = os.path.join(app_settings_dir, 'overrides.json') - if not os.path.exists(overrides_path): - overrides_path = os.path.join(app_settings_dir, 'overrides.json5') + overrides_d = os.path.join(app_settings_dir, 'overrides.d') + + # find (and sort) the conf.d overrides files + all_override_paths = sorted([ + *(glob(os.path.join(overrides_d, '*.json'))), + *(glob(os.path.join(overrides_d, '*.json5'))), + ]) + + all_override_paths += [ + os.path.join(app_settings_dir, 'overrides.json'), + os.path.join(app_settings_dir, 'overrides.json5') + ] + + for overrides_path in all_override_paths: + if not os.path.exists(overrides_path): + continue - if os.path.exists(overrides_path): with open(overrides_path, encoding='utf-8') as fid: try: - overrides = json5.load(fid) + if overrides_path.endswith('.json5'): + path_overrides = json5.load(fid) + else: + path_overrides = json.load(fid) + for plugin_id, config in path_overrides.items(): + recursive_update(overrides.setdefault(plugin_id, {}), config) + print(overrides_path, overrides) except Exception as e: error = e # Allow `default_settings_overrides.json` files in /labconfig dirs # to allow layering of defaults cm = ConfigManager(config_dir_name="labconfig") - recursive_update(overrides, cm.get('default_setting_overrides')) + + for plugin_id, config in cm.get('default_setting_overrides').items(): + recursive_update(overrides.setdefault(plugin_id, {}), config) return overrides, error diff --git a/jupyterlab_server/tests/test_settings_api.py b/jupyterlab_server/tests/test_settings_api.py index 216d4505..31736425 100755 --- a/jupyterlab_server/tests/test_settings_api.py +++ b/jupyterlab_server/tests/test_settings_api.py @@ -1,8 +1,9 @@ """Test the Settings service API. """ +from pathlib import Path +import json import pytest -import json import json5 import tornado @@ -28,6 +29,28 @@ async def test_get_settings_overrides_dicts(jp_fetch, labserverapp): assert len(schema['properties']['codeCellConfig']['default']) == 15 +@pytest.mark.parametrize('ext', ['json', 'json5']) +async def test_get_settings_overrides_d_dicts(jp_fetch, labserverapp, ext): + # Check that values that are dictionaries in overrides.d/*.json are + # merged with the schema. + id = '@jupyterlab/apputils-extension:themes' + overrides_d = Path(labserverapp.app_settings_dir) / "overrides.d" + overrides_d.mkdir(exist_ok=True, parents=True) + for i in range(10): + text = json.dumps({id: {'codeCellConfig': {'cursorBlinkRate': 530 + i}}}) + if ext == 'json5': + text += '\n// a comment' + (overrides_d / f"foo-{i}.{ext}").write_text(text, encoding='utf-8') + r = await jp_fetch('lab', 'api', 'settings', id) + validate_request(r) + res = r.body.decode() + data = json.loads(res) + assert data['id'] == id + schema = data['schema'] + # Check that the last overrides.d/*.json file is respected. + assert schema['properties']['codeCellConfig']['default']['cursorBlinkRate'] == 539 + + async def test_get_settings(jp_fetch, labserverapp): id = '@jupyterlab/apputils-extension:themes' r = await jp_fetch('lab', 'api', 'settings', id)