[1,of,2,RFC] RFC: implement immutable config objects
David Soria Parra
dsp at experimentalworks.net
Wed Mar 29 18:25:26 EDT 2017
On Mon, Mar 27, 2017 at 11:38:06AM -0700, Jun Wu wrote:
> # HG changeset patch
> # User Jun Wu <quark at fb.com>
> # Date 1490635856 25200
> # Mon Mar 27 10:30:56 2017 -0700
> # Node ID 4eb7c76340791f379a34f9df4ec42e0c8b9b2a2f
> # Parent 4a8d065bbad80d3b3401010375bc80165404aa87
> RFC: implement immutable config objects
I like the overall approach and that it's parallel to config.config() and
ui.config. We might in later patches combine config.config into this.
> +class abstractimmutableconfig(object):
> + """minimal interface defined for a read-only config accessor
> +
> + The immutable config object should get its state set and frozen during
> + __init__ and should not have any setconfig-like method.
> +
> + The immutable config layer knows about sections, and should probably uses
> + ordered dict for each section. To simplify things, this layer does not care
> + about "source", "unset" or any filesystem IO. They're up to the upper layer
> + to deal with. For example, the upper layer could store "None" as "unset"s,
> + and store (value, source) as a tuple together.
> + """
> +
> + def __init__(self, title):
> + """title: useful to identify the config"""
> + self.title = title
> +
> + def subconfigs(self, section=None):
> + """return a list-ish of child config objects filtered by section"""
> + raise NotImplementedError
> +
> + def get(self, section, item):
> + """return value or None"""
> + return self.getsection(section).get(item)
> +
> + def getsection(self, section):
> + """return a dict-ish"""
> + raise NotImplementedError
> +
> + def sections(self):
> + """return an iter-able"""
> + raise NotImplementedError
> +
> +class atomicconfig(abstractimmutableconfig):
> + """immutable config that converted from a list-ish"""
> +
> + def __init__(self, title, entries=None):
> + """
> + title: a title used to identify the atomicconfig
> + entries: a list-ish of (section, item, value)
> + """
> + super(atomicconfig, self).__init__(title)
> + self._sections = util.sortdict()
Can we use collections.defaultdict(util.sortdict) here?
> + for entry in entries:
> + section, item, value = entry
> + if section not in self._sections:
> + self._sections[section] = util.sortdict()
> + if item is not None:
> + self._sections[section][item] = value
> +
> + def subconfigs(self, section=None):
> + return ()
> +
> + def getsection(self, section):
> + return self._sections.get(section, {})
> +
> + def sections(self):
> + return self._sections.keys()
> +
> +class mergedconfig(abstractimmutableconfig):
> + """immutable config that is a merge of a list of immutable configs"""
> +
> + def __init__(self, title, subconfigs):
> + super(mergedconfig, self).__init__(title)
> + self._subconfigs = tuple(subconfigs) # make it immutable
> + self._cachedsections = {}
> +
> + def subconfigs(self, section=None):
I am not sure about this API. We already have getsection() from
abstractimmutableconfig. I think this method does two things depending
on the arguments.
> + if section is None:
> + return self._subconfigs
> + else:
> + return self._sectionconfigs.get(section, ())
> +
> + def sections(self):
> + return self._sectionconfigs.keys()
> +
> + @util.propertycache
> + def _sectionconfigs(self):
> + """{section: [subconfig]}"""
> + sectionconfigs = {}
> + for subconfig in self._subconfigs:
> + for section in subconfig.sections():
> + sectionconfigs.setdefault(section, []).append(subconfig)
> + return sectionconfigs
> +
> + def getsection(self, section):
> + items = self._cachedsections.get(section, None)
> + if items is None:
> + subconfigs = self._sectionconfigs.get(section, [])
> + if len(subconfigs) == 1:
> + # no need to merge configs
> + items = subconfigs[0].getsection(section)
> + else:
> + # merge configs
> + items = util.sortdict()
> + for subconfig in subconfigs:
> + subconfigitems = subconfig.getsection(section).items()
Could we do an `items.update(subconfig.getsection(section))`?
> + for item, value in subconfigitems:
> + items[item] = value
> + self._cachedsections[section] = items
> + return items
> +
> + def append(self, subconfig):
> + """return a new mergedconfig with the new subconfig appended"""
> + return mergedconfig(self.title, list(self._subconfigs) + [subconfig])
> +
> + def prepend(self, subconfig):
> + """return a new mergedconfig with the new subconfig prepended"""
> + return mergedconfig(self.title, [subconfig] + list(self._subconfigs))
> +
> + def filter(self, func):
> + """return a new mergedconfig with only selected subconfigs
> + func: subconfig -> bool
> + """
> + return mergedconfig(self.title, filter(func, self._subconfigs))
> +
> +class fileconfig(atomicconfig):
> + """immutable config constructed from config files"""
> +
> + def __init__(self, title, fp, sections=None, remap=None):
> + # Currently, just use the legacy, non-immutable "config" object to do
> + # the parsing, remap stuff. In the future we may want to detach from it
> + cfg = config()
> + cfg.read(title, fp, sections=sections, remap=remap)
> +
> + def cfgwalker():
> + # visible config, with source
> + for section in cfg:
> + emptysection = True
> + for item, value in cfg.items(section):
> + emptysection = False
> + source = cfg.source(section, item)
> + yield (section, item, (value, source))
> + if emptysection:
> + # create the section
> + yield (section, None, (None, None))
> + # "%unset" configs
> + for (section, item) in cfg._unset:
> + # TODO "%unset" could have line numbers
> + if cfg.get(section, item) is None:
> + yield (section, item, (None, '%s:(unset)' % title))
> +
> + super(fileconfig, self).__init__(title, cfgwalker())
> +
> +class filteredconfig(abstractimmutableconfig):
> + """immutable config that changes other configs"""
> +
> + def __init__(self, title, subconfig, filters=None):
> + """
> + filters: a dict-ish, {section: filterfunc or sortdict-ish}
> + a filterfunc takes sortdict-ish and returns sortdict-ish
> + a sortdict-ish will replace the section directly
> + """
> + super(filteredconfig, self).__init__(title)
> + self._filters = filters or {}
> + self._subconfig = subconfig
> + self._cachedsections = {}
> +
> + def subconfigs(self, section=None):
I think this is actually not needed except for dumpconfig, which might just be a
helper that knows about the internals.
> + return (self._subconfig,)
> +
> + def sections(self):
> + return self._subconfig.sections()
> +
> + def getsection(self, section):
> + if section not in self._filters:
> + return self._subconfig.getsection(section)
> + items = self._cachedsections.get(section, None)
> + if items is None:
> + filter = self._filters[section]
> + if util.safehasattr(filter, '__call__'):
this should be `callable`
> + items = filter(self._subconfig.getsection(section))
> + else:
> + items = filter
> + self._cachedsections[section] = items
> + return items
More information about the Mercurial-devel
mailing list