Repository: jupyterlab/jupyterlab_server

Issue:
page_config.json should support json5
### Problem

Similar to overrides, `page_config.json` should also support json5 so that comments can be added.

### Proposed Solution

All json configs should always support json5. Maybe we add a common codepath to enforce this? I.e. instead of doing something like [this](https://github.com/jupyterlab/jupyterlab_server/blob/eb37a21d1deaf449074da2d087b57a773e26fdaa/jupyterlab_server/config.py#L91) we refactor [this](https://github.com/jupyterlab/jupyterlab_server/blob/eb37a21d1deaf449074da2d087b57a773e26fdaa/jupyterlab_server/settings_utils.py#L291) to be reused. 


Discussion:
codecov-commenter: ## [Codecov](https://codecov.io/gh/jupyterlab/jupyterlab_server/pull/388?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab) Report
Patch coverage: **`100.00`**% and project coverage change: **`+0.07`** :tada:
> Comparison is base [(`1dcca61`)](https://codecov.io/gh/jupyterlab/jupyterlab_server/commit/1dcca6107630d6315339cd1e83460e18db57194a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab) 80.86% compared to head [(`04df336`)](https://codecov.io/gh/jupyterlab/jupyterlab_server/pull/388?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab) 80.93%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #388      +/-   ##
==========================================
+ Coverage   80.86%   80.93%   +0.07%     
==========================================
  Files          22       22              
  Lines        1766     1773       +7     
  Branches      332      334       +2     
==========================================
+ Hits         1428     1435       +7     
  Misses        235      235              
  Partials      103      103              
```


| [Impacted Files](https://codecov.io/gh/jupyterlab/jupyterlab_server/pull/388?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab) | Coverage Δ | |
|---|---|---|
| [jupyterlab\_server/config.py](https://codecov.io/gh/jupyterlab/jupyterlab_server/pull/388?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab#diff-anVweXRlcmxhYl9zZXJ2ZXIvY29uZmlnLnB5) | `74.57% <100.00%> (+1.04%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/jupyterlab/jupyterlab_server/pull/388?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jupyterlab).


Patch:
diff --git a/jupyterlab_server/config.py b/jupyterlab_server/config.py
index 51a94b5..6e349c3 100644
--- a/jupyterlab_server/config.py
+++ b/jupyterlab_server/config.py
@@ -9,6 +9,7 @@ from glob import iglob
 from itertools import chain
 from os.path import join as pjoin
 
+import json5  # type:ignore
 from jupyter_core.paths import SYSTEM_CONFIG_PATH, jupyter_config_dir, jupyter_path
 from jupyter_server.services.config.manager import ConfigManager, recursive_update
 from jupyter_server.utils import url_path_join as ujoin
@@ -78,6 +79,26 @@ def get_static_page_config(app_settings_dir=None, logger=None, level="all"):
     return cm.get("page_config")
 
 
+def load_config(path):
+    """Load either a json5 or a json config file.
+
+    Parameters
+    ----------
+    path : str
+        Path to the file to be loaded
+
+    Returns
+    -------
+    Dict[Any, Any]
+        Dictionary of json or json5 data
+    """
+    with open(path, encoding="utf-8") as fid:
+        if path.endswith('.json5'):
+            return json5.load(fid)
+        else:
+            return json.load(fid)
+
+
 def get_page_config(labextensions_path, app_settings_dir=None, logger=None):  # noqa
     """Get the page config for the application handler"""
     # Build up the full page config
@@ -87,17 +108,20 @@ def get_page_config(labextensions_path, app_settings_dir=None, logger=None):  #
 
     # Start with the app_settings_dir as lowest priority
     if app_settings_dir:
-        app_page_config = pjoin(app_settings_dir, "page_config.json")
-        if osp.exists(app_page_config):
-            with open(app_page_config, encoding="utf-8") as fid:
-                data = json.load(fid)
-
-            # Convert lists to dicts
-            for key in [disabled_key, "deferredExtensions"]:
-                if key in data:
-                    data[key] = {key: True for key in data[key]}
-
-            recursive_update(page_config, data)
+        config_paths = [
+            pjoin(app_settings_dir, "page_config.json5"),
+            pjoin(app_settings_dir, "page_config.json"),
+        ]
+        for path in config_paths:
+            if osp.exists(path):
+                data = load_config(path)
+                # Convert lists to dicts
+                for key in [disabled_key, "deferredExtensions"]:
+                    if key in data:
+                        data[key] = {key: True for key in data[key]}
+
+                recursive_update(page_config, data)
+                break
 
     # Get the traitlets config
     static_page_config = get_static_page_config(logger=logger, level="all")


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 01ff6c9..7e40b86 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,17 +4,27 @@
 import json
 import os
 
+import json5  # type:ignore
+import pytest
+
 from jupyterlab_server.config import get_page_config
 
 
-def test_get_page_config(tmp_path):
+@pytest.mark.parametrize(
+    "lib,extension",
+    (
+        (json, "json"),
+        (json5, "json5"),
+    ),
+)
+def test_get_page_config(tmp_path, lib, extension):
     labext_path = [os.path.join(tmp_path, "ext")]
     settings_path = os.path.join(tmp_path, "settings")
     os.mkdir(settings_path)
 
-    with open(os.path.join(settings_path, "page_config.json"), "w") as fid:
+    with open(os.path.join(settings_path, f"page_config.{extension}"), "w") as fid:
         data = dict(deferredExtensions=["foo"])
-        json.dump(data, fid)
+        lib.dump(data, fid)
 
     static_dir = os.path.join(tmp_path, "static")
     os.mkdir(static_dir)