11import json
22import logging
33import os
4- import shutil
54import time
65from copy import deepcopy
76from typing import List , Optional , Type , Union
87
9- from deepmerge import always_merger as Merger
8+ from deepmerge import Merger , always_merger
109from jsonschema import Draft202012Validator as Validator
1110from jupyter_ai .models import DescribeConfigResponse , GlobalConfig , UpdateConfigRequest
1211from jupyter_ai_magics import JupyternautPersona , Persona
@@ -178,11 +177,23 @@ def _init_config_schema(self):
178177 with open (OUR_SCHEMA_PATH , encoding = "utf-8" ) as f :
179178 default_schema = json .load (f )
180179
180+ # Create a custom `deepmerge.Merger` object to merge lists using the
181+ # 'append_unique' strategy.
182+ #
183+ # This stops type union declarations like `["string", "null"]` from
184+ # growing into `["string", "null", "string", "null"]` on restart.
185+ # This fixes issue #1320.
186+ merger = Merger (
187+ [(list , ["append_unique" ]), (dict , ["merge" ]), (set , ["union" ])],
188+ ["override" ],
189+ ["override" ],
190+ )
191+
181192 # merge existing_schema into default_schema
182193 # specifying existing_schema as the second argument ensures that
183194 # existing_schema always overrides existing keys in default_schema, i.e.
184195 # this call only adds new keys in default_schema.
185- schema = Merger .merge (default_schema , existing_schema )
196+ schema = merger .merge (default_schema , existing_schema )
186197 with open (self .schema_path , encoding = "utf-8" , mode = "w" ) as f :
187198 json .dump (schema , f , indent = self .indentation_depth )
188199
@@ -194,15 +205,15 @@ def _init_validator(self) -> None:
194205
195206 def _init_config (self ):
196207 default_config = self ._init_defaults ()
197- if os .path .exists (self .config_path ):
208+ if os .path .exists (self .config_path ) and os . stat ( self . config_path ). st_size != 0 :
198209 self ._process_existing_config (default_config )
199210 else :
200211 self ._create_default_config (default_config )
201212
202213 def _process_existing_config (self , default_config ):
203214 with open (self .config_path , encoding = "utf-8" ) as f :
204215 existing_config = json .loads (f .read ())
205- merged_config = Merger .merge (
216+ merged_config = always_merger .merge (
206217 default_config ,
207218 {k : v for k , v in existing_config .items () if v is not None },
208219 )
@@ -481,7 +492,7 @@ def update_config(self, config_update: UpdateConfigRequest): # type:ignore
481492 raise KeyEmptyError ("API key value cannot be empty." )
482493
483494 config_dict = self ._read_config ().model_dump ()
484- Merger .merge (config_dict , config_update .model_dump (exclude_unset = True ))
495+ always_merger .merge (config_dict , config_update .model_dump (exclude_unset = True ))
485496 self ._write_config (GlobalConfig (** config_dict ))
486497
487498 # this cannot be a property, as the parent Configurable already defines the
0 commit comments