99import io
1010import json
1111import os
12+ import copy
1213
1314from six import PY3
1415from traitlets .config import LoggingConfigurable
@@ -36,9 +37,23 @@ def recursive_update(target, new):
3637 target [k ] = v
3738
3839
40+ def remove_defaults (data , defaults ):
41+ """Recursively remove items from dict that are already in defaults"""
42+ # copy the iterator, since data will be modified
43+ for key , value in list (data .items ()):
44+ if key in defaults :
45+ if isinstance (value , dict ):
46+ remove_defaults (data [key ], defaults [key ])
47+ if not data [key ]: # prune empty subdicts
48+ del data [key ]
49+ else :
50+ if value == defaults [key ]:
51+ del data [key ]
52+
53+
3954class BaseJSONConfigManager (LoggingConfigurable ):
4055 """General JSON config manager
41-
56+
4257 Deals with persisting/storing config in a json file with optionally
4358 default values in a {section_name}.d directory.
4459 """
@@ -62,19 +77,22 @@ def directory(self, section_name):
6277 """Returns the directory name for the section name: {config_dir}/{section_name}.d"""
6378 return os .path .join (self .config_dir , section_name + '.d' )
6479
65- def get (self , section_name ):
80+ def get (self , section_name , include_root = True ):
6681 """Retrieve the config data for the specified section.
6782
6883 Returns the data as a dictionary, or an empty dictionary if the file
6984 doesn't exist.
85+
86+ When include_root is False, it will not read the root .json file,
87+ effectively returning the default values.
7088 """
71- paths = [self .file_name (section_name )]
89+ paths = [self .file_name (section_name )] if include_root else []
7290 if self .read_directory :
7391 pattern = os .path .join (self .directory (section_name ), '*.json' )
7492 # These json files should be processed first so that the
7593 # {section_name}.json take precedence.
7694 # The idea behind this is that installing a Python package may
77- # put a json file somewhere in the a .d directory, while the
95+ # put a json file somewhere in the a .d directory, while the
7896 # .json file is probably a user configuration.
7997 paths = sorted (glob .glob (pattern )) + paths
8098 self .log .debug ('Paths used for configuration of %s: \n \t %s' , section_name , '\n \t ' .join (paths ))
@@ -91,6 +109,12 @@ def set(self, section_name, data):
91109 filename = self .file_name (section_name )
92110 self .ensure_config_dir_exists ()
93111
112+ if self .read_directory :
113+ # we will modify data in place, so make a copy
114+ data = copy .deepcopy (data )
115+ defaults = self .get (section_name , include_root = False )
116+ remove_defaults (data , defaults )
117+
94118 # Generate the JSON up front, since it could raise an exception,
95119 # in order to avoid writing half-finished corrupted data to disk.
96120 json_content = json .dumps (data , indent = 2 )
0 commit comments