"""The `L` class and helpers for it"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_foundation.ipynb.

# %% auto #0
__all__ = ['working_directory', 'add_docs', 'docs', 'coll_repr', 'is_bool', 'mask2idxs', 'cycle', 'zip_cycle', 'is_indexer',
           'product', 'flatmap', 'CollBase', 'L', 'curryable', 'splitter', 'linesplitter', 'save_config_file',
           'read_config_file', 'find_file_parents', 'Config']

# %% ../nbs/02_foundation.ipynb #0f974791
from .imports import *
from .basics import *
from functools import lru_cache,wraps
from contextlib import contextmanager
from copy import copy
from configparser import ConfigParser
import random,pickle,inspect

# %% ../nbs/02_foundation.ipynb #f059df0f
@contextmanager
def working_directory(path):
    "Change working directory to `path` and return to previous on exit."
    prev_cwd = Path.cwd()
    os.chdir(path)
    try: yield
    finally: os.chdir(prev_cwd)

# %% ../nbs/02_foundation.ipynb #c3fd027f
def add_docs(cls, cls_doc=None, **docs):
    "Copy values from `docs` to `cls` docstrings, and confirm all public methods are documented"
    if cls_doc is not None: cls.__doc__ = cls_doc
    for k,v in docs.items():
        f = getattr(cls,k)
        if hasattr(f,'__func__'): f = f.__func__ # required for class methods
        f.__doc__ = v
    # List of public callables without docstring
    nodoc = [c for n,c in vars(cls).items() if callable(c)
             and not n.startswith('_') and c.__doc__ is None]
    assert not nodoc, f"Missing docs: {nodoc}"
    assert cls.__doc__ is not None, f"Missing class docs: {cls}"

# %% ../nbs/02_foundation.ipynb #b180313a
def docs(cls):
    "Decorator version of `add_docs`, using `_docs` dict"
    add_docs(cls, **cls._docs)
    return cls

# %% ../nbs/02_foundation.ipynb #34a29b43
def coll_repr(c, max_n=250):
    "String repr of up to `max_n` items of (possibly lazy) collection `c`"
    res = ', '.join(itertools.islice(map(repr,c), max_n))
    res = '[' + res + ('...' if len(c)>max_n else '') + ']'
    if len(c)>=max(7,max_n): res = f'(#{len(c)}) ' + res
    return res


# %% ../nbs/02_foundation.ipynb #6cf96f49
def is_bool(x):
    "Check whether `x` is a bool or None"
    return isinstance(x,(bool,NoneType)) or risinstance('bool_', x)

# %% ../nbs/02_foundation.ipynb #66a28a53
def mask2idxs(mask):
    "Convert bool mask or index list to index `L`"
    if isinstance(mask,slice): return mask
    mask = list(mask)
    if len(mask)==0: return []
    it = mask[0]
    if hasattr(it,'item'): it = it.item()
    if is_bool(it): return [i for i,m in enumerate(mask) if m]
    return [int(i) for i in mask]

# %% ../nbs/02_foundation.ipynb #af754019
def cycle(o):
    "Like `itertools.cycle` except creates list of `None`s if `o` is empty"
    o = listify(o)
    return itertools.cycle(o) if o is not None and len(o) > 0 else itertools.cycle([None])

# %% ../nbs/02_foundation.ipynb #9346b91a
def zip_cycle(x, *args):
    "Like `itertools.zip_longest` but `cycle`s through elements of all but first argument"
    return zip(x, *map(cycle,args))

# %% ../nbs/02_foundation.ipynb #8a281173
def is_indexer(idx):
    "Test whether `idx` will index a single item in a list"
    return isinstance(idx,int) or not getattr(idx,'ndim',1)

# %% ../nbs/02_foundation.ipynb #3fd3133f
def product(xs):
    "The product of elements of `xs`, with `None`s removed"
    return reduce(operator.mul, [o for o in xs if o is not None], 1)

# %% ../nbs/02_foundation.ipynb #b1e1639e
def flatmap(f, xs, **kwargs):
    "Apply f to each element and flatten the results into a single list."
    return [y for x in xs for y in f(x, **kwargs)]

# %% ../nbs/02_foundation.ipynb #eefd2763
class CollBase:
    "Base class for composing a list of `items`"
    def __init__(self, items): self.items = items
    def __len__(self): return len(self.items)
    def __getitem__(self, k): return self.items[list(k) if isinstance(k,CollBase) else k]
    def __setitem__(self, k, v): self.items[list(k) if isinstance(k,CollBase) else k] = v
    def __delitem__(self, i): del(self.items[i])
    def __repr__(self): return self.items.__repr__()
    def __iter__(self): return self.items.__iter__()

# %% ../nbs/02_foundation.ipynb #d953eaf9
class _L_Meta(type):
    def __call__(cls, x=None, *args, **kwargs):
        if not args and not kwargs and x is not None and isinstance(x,cls): return x
        return super().__call__(x, *args, **kwargs)

# %% ../nbs/02_foundation.ipynb #f571f489
class L(GetAttr, CollBase, metaclass=_L_Meta):
    "Behaves like a list of `items` but can also index with list of indices or masks"
    _default='items'
    def __init__(self, items=None, *rest, use_list=False, match=None):
        if (use_list is not None) or not is_array(items):
            items = listify(items, *rest, use_list=use_list, match=match)
        super().__init__(items)

    @property
    def _xtra(self): return None
    def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs)
    def __getitem__(self, idx):
        "Retrieve `idx` (can be list of indices, or mask, or int) items"
        if isinstance(idx,int) and not hasattr(self.items,'iloc'): return self.items[idx]
        return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None)

    def _get(self, i):
        if is_indexer(i) or isinstance(i,slice): return getattr(self.items,'iloc',self.items)[i]
        i = mask2idxs(i)
        return (self.items.iloc[list(i)] if hasattr(self.items,'iloc')
                else self.items.__array__()[(i,)] if hasattr(self.items,'__array__')
                else [self.items[i_] for i_ in i])

    def __setitem__(self, idx, o):
        "Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable)"
        if isinstance(idx, (int, slice)): self.items[idx] = o
        else:
            idx = idx if isinstance(idx,L) else listify(idx)
            if not is_iter(o): o = [o]*len(idx)
            for i,o_ in zip(idx,o): self.items[i] = o_

    def __eq__(self,b):
        if b is None: return False
        if not hasattr(b, '__iter__'): return False
        if risinstance('ndarray', b): return array_equal(b, self)
        if isinstance(b, (str,dict)) or callable(b): return False
        return all_equal(b,self)

    def __iter__(self): return iter(self.items.itertuples() if hasattr(self.items,'iloc') else self.items)
    def __contains__(self,b): return b in self.items
    def __reversed__(self): return self._new(reversed(self.items))
    def __invert__(self): return self._new(not i for i in self)
    def __repr__(self): return repr(self.items)
    def _repr_pretty_(self, p, cycle):
        p.text('...' if cycle else repr(self.items) if is_array(self.items) else coll_repr(self))
    def __mul__ (a,b): return a._new(a.items*b)
    def __add__ (a,b): return a._new(a.items+listify(b))
    def __radd__(a,b): return a._new(b)+a
    def __addi__(a,b):
        a.items += list(b)
        return a

# %% ../nbs/02_foundation.ipynb #05e35e65
# Here we are fixing the signature of L. What happens is that the __call__ method on the MetaClass of L shadows the __init__
# giving the wrong signature (https://stackoverflow.com/questions/49740290/call-from-metaclass-shadows-signature-of-init).
def _f(items=None, *rest, use_list=False, match=None): ...
L.__signature__ = inspect.signature(_f)

# %% ../nbs/02_foundation.ipynb #68e52115
Sequence.register(L);

# %% ../nbs/02_foundation.ipynb #d966b525
@patch
def unique(self:L, sort=False, bidir=False, start=None):
    "Unique items, in stable order"
    return L(uniqueify(self, sort=sort, bidir=bidir, start=start))

# %% ../nbs/02_foundation.ipynb #840aef37
@patch
def val2idx(self:L):
    "Dict from value to index"
    return val2idx(self)

# %% ../nbs/02_foundation.ipynb #f2e038cc
@patch(cls_method=True)
def range(cls:L, a, b=None, step=None):
    "Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`"
    return cls(range_of(a, b=b, step=step))

# %% ../nbs/02_foundation.ipynb #d1d16238
@patch
def enumerate(self:L):
    "Same as `enumerate`"
    return L(enumerate(self))

# %% ../nbs/02_foundation.ipynb #ccd5c96c
@patch
def renumerate(self:L):
    "Same as `renumerate`"
    return L(renumerate(self))

# %% ../nbs/02_foundation.ipynb #13795373
@patch(cls_method=True)
def split(cls:L, s, sep=None, maxsplit=-1):
    "Class Method: Same as `str.split`, but returns an `L`"
    return cls(s.split(sep,maxsplit))

# %% ../nbs/02_foundation.ipynb #6da7b43c
@patch(cls_method=True)
def splitlines(cls:L, s, keepends=False):
    "Class Method: Same as `str.splitlines`, but returns an `L`"
    return cls(s.splitlines(keepends))

# %% ../nbs/02_foundation.ipynb #826ffa88
def curryable(f):
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        if not isinstance(self, L): return lambda items: f(L(items), self, *args, **kwargs)
        return f(L(self), *args, **kwargs)
    return wrapper

# %% ../nbs/02_foundation.ipynb #0d6407c6
@patch
@curryable
def map(self:L, f, *args, **kwargs):
    "Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`"
    return self._new(map_ex(self, f, *args, gen=False, **kwargs))

# %% ../nbs/02_foundation.ipynb #9c5a4633
def splitter(sep=None, maxsplit=-1):
    "Create a partial function that splits strings into `L`"
    return partial(L.split, sep=sep, maxsplit=maxsplit)

# %% ../nbs/02_foundation.ipynb #ce5b413b
def linesplitter(keepends=False):
    "Create a partial function that splits strings by lines into `L`"
    return partial(L.splitlines, keepends=keepends)

# %% ../nbs/02_foundation.ipynb #d2680cbd
@patch
@curryable
def groupby(self:L, key, val=noop):
    "Same as `fastcore.basics.groupby`"
    return groupby(self, key, val=val)

# %% ../nbs/02_foundation.ipynb #37dc972a
@patch
@curryable
def starmap(self:L, f, *args, **kwargs):
    "Like `map`, but use `itertools.starmap`"
    return self._new(itertools.starmap(partial(f,*args,**kwargs), self))

# %% ../nbs/02_foundation.ipynb #cb2283b0
@patch
@curryable
def rstarmap(self:L, f, *args, **kwargs):
    "Like `starmap`, but reverse the order of args"
    return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self))

# %% ../nbs/02_foundation.ipynb #c0846f2e
@patch
def map_dict(self:L, f=noop, *args, **kwargs):
    "Like `map`, but creates a dict from `items` to function results"
    return {k:f(k, *args,**kwargs) for k in self}

# %% ../nbs/02_foundation.ipynb #105ac114
@patch
def zip(self:L, cycled=False):
    "Create new `L` with `zip(*items)`"
    return self._new((zip_cycle if cycled else zip)(*self))

# %% ../nbs/02_foundation.ipynb #6340b2b3
@patch
def map_zip(self:L, f, *args, cycled=False, **kwargs):
    "Combine `zip` and `starmap`"
    return self.zip(cycled=cycled).starmap(f, *args, **kwargs)

# %% ../nbs/02_foundation.ipynb #bc012fad
@patch
def zipwith(self:L, *rest, cycled=False):
    "Create new `L` with `self` zip with each of `*rest`"
    return self._new([self, *rest]).zip(cycled=cycled)

# %% ../nbs/02_foundation.ipynb #5a40226f
@patch
def map_zipwith(self:L, f, *rest, cycled=False, **kwargs):
    "Combine `zipwith` and `starmap`"
    return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs)

# %% ../nbs/02_foundation.ipynb #66e76d03
@patch
@curryable
def filter(self:L, f=noop, negate=False, **kwargs):
    "Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`"
    return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs))

# %% ../nbs/02_foundation.ipynb #085b690b
@patch
@curryable
def starfilter(self:L, f, negate=False, **kwargs):
    "Like `filter`, but unpacks elements as args to `f`"
    _f = lambda x: f(*x, **kwargs)
    if negate: _f = not_(_f)
    return self._new(filter(_f, self))

# %% ../nbs/02_foundation.ipynb #e0fa80df
@patch
@curryable
def rstarfilter(self:L, f, negate=False, **kwargs):
    "Like `starfilter`, but reverse the order of args"
    _f = lambda x: f(*x[::-1], **kwargs)
    if negate: _f = not_(_f)
    return self._new(filter(_f, self))

# %% ../nbs/02_foundation.ipynb #a2515ab8
@patch
@curryable
def argwhere(self:L, f, negate=False, **kwargs):
    "Like `filter`, but return indices for matching items"
    return self._new(argwhere(self, f, negate, **kwargs))

# %% ../nbs/02_foundation.ipynb #cf22acd4
@patch
@curryable
def starargwhere(self:L, f, negate=False):
    "Like `argwhere`, but unpacks elements as args to `f`"
    _f = lambda x: f(*x)
    if negate: _f = not_(_f)
    return self._new(i for i,o in enumerate(self) if _f(o))

# %% ../nbs/02_foundation.ipynb #1f010f9c
@patch
@curryable
def rstarargwhere(self:L, f, negate=False):
    "Like `starargwhere`, but reverse the order of args"
    _f = lambda x: f(*x[::-1])
    if negate: _f = not_(_f)
    return self._new(i for i,o in enumerate(self) if _f(o))

# %% ../nbs/02_foundation.ipynb #3e7fa556
@patch
@curryable
def argfirst(self:L, f, negate=False):
    "Return index of first matching item"
    if negate: f = not_(f)
    return first(i for i,o in self.enumerate() if f(o))

# %% ../nbs/02_foundation.ipynb #90509c05
@patch
@curryable
def starargfirst(self:L, f, negate=False):
    "Like `argfirst`, but unpacks elements as args to `f`"
    _f = lambda x: f(*x)
    if negate: _f = not_(_f)
    return first(i for i,o in self.enumerate() if _f(o))

# %% ../nbs/02_foundation.ipynb #dc43b9b9
@patch
@curryable
def rstarargfirst(self:L, f, negate=False):
    "Like `starargfirst`, but reverse the order of args"
    _f = lambda x: f(*x[::-1])
    if negate: _f = not_(_f)
    return first(i for i,o in self.enumerate() if _f(o))

# %% ../nbs/02_foundation.ipynb #44055cd0
@patch
def itemgot(self:L, *idxs):
    "Create new `L` with item `idx` of all `items`"
    x = self
    for idx in idxs: x = x.map(itemgetter(idx))
    return x

# %% ../nbs/02_foundation.ipynb #10f3d35e
@patch
def attrgot(self:L, k, default=None):
    "Create new `L` with attr `k` (or value `k` for dicts) of all `items`."
    return self.map(lambda o: o.get(k,default) if isinstance(o, dict) else nested_attr(o,k,default))

# %% ../nbs/02_foundation.ipynb #9f238dfb
@patch
@curryable
def sorted(self:L, key=None, reverse=False, cmp=None, **kwargs):
    "New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`"
    return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs))

# %% ../nbs/02_foundation.ipynb #1c21919b
@patch
@curryable
def starsorted(self:L, key, reverse=False):
    "Like `sorted`, but unpacks elements as args to `key`"
    return self._new(sorted(self, key=lambda x: key(*x), reverse=reverse))

# %% ../nbs/02_foundation.ipynb #7549144e
@patch
@curryable
def rstarsorted(self:L, key, reverse=False):
    "Like `starsorted`, but reverse the order of args"
    return self._new(sorted(self, key=lambda x: key(*x[::-1]), reverse=reverse))

# %% ../nbs/02_foundation.ipynb #9d86b896
@patch
def concat(self:L):
    "Concatenate all elements of list"
    return self._new(itertools.chain.from_iterable(self.map(L)))

# %% ../nbs/02_foundation.ipynb #b493aad4
@patch
def copy(self:L):
    "Same as `list.copy`, but returns an `L`"
    return self._new(self.items.copy())

# %% ../nbs/02_foundation.ipynb #7b2691d6
@patch
def shuffle(self:L):
    "Same as `random.shuffle`, but not inplace"
    it = copy(self.items)
    random.shuffle(it)
    return self._new(it)

# %% ../nbs/02_foundation.ipynb #7b83c9e9
@patch
@curryable
def reduce(self:L, f, initial=None):
    "Wrapper for `functools.reduce`"
    return reduce(f, self) if initial is None else reduce(f, self, initial)

# %% ../nbs/02_foundation.ipynb #3db27a6a
@patch
@curryable
def starreduce(self:L, f, initial=None):
    "Like `reduce`, but unpacks elements as args to `f`"
    _f = lambda acc, x: f(acc, *x)
    return reduce(_f, self) if initial is None else reduce(_f, self, initial)

# %% ../nbs/02_foundation.ipynb #046f01fb
@patch
@curryable
def rstarreduce(self:L, f, initial=None):
    "Like `starreduce`, but reverse the order of unpacked args"
    _f = lambda acc, x: f(acc, *x[::-1])
    return reduce(_f, self) if initial is None else reduce(_f, self, initial)

# %% ../nbs/02_foundation.ipynb #84b3413b
@patch
def sum(self:L):
    "Sum of the items"
    return self.reduce(operator.add, 0)

# %% ../nbs/02_foundation.ipynb #2a96a0fd
@patch
def product(self:L):
    "Product of the items"
    return self.reduce(operator.mul, 1)

# %% ../nbs/02_foundation.ipynb #f82d9975
@patch
def map_first(self:L, f=noop, g=noop, *args, **kwargs):
    "First element of `map_filter`"
    return first(self.map(f, *args, **kwargs), g)

# %% ../nbs/02_foundation.ipynb #1366f0fa
@patch
def setattrs(self:L, attr, val):
    "Call `setattr` on all items"
    [setattr(o,attr,val) for o in self]

# %% ../nbs/02_foundation.ipynb #ab6bb396
@patch
def flatmap(self:L, f, **kwargs):
    "Apply f to each element and flatten the results into a single L."
    return L(flatmap(f, self, **kwargs))

# %% ../nbs/02_foundation.ipynb #9c927236
@patch
def cycle(self:L):
    "Same as `itertools.cycle`"
    return cycle(self)

# %% ../nbs/02_foundation.ipynb #79e84e6d
@patch
@curryable
def takewhile(self:L, f):
    "Same as `itertools.takewhile`"
    return self._new(itertools.takewhile(f, self))

# %% ../nbs/02_foundation.ipynb #c9f6e354
@patch
@curryable
def dropwhile(self:L, f):
    "Same as `itertools.dropwhile`"
    return self._new(itertools.dropwhile(f, self))

# %% ../nbs/02_foundation.ipynb #45f04c5c
@patch
@curryable
def startakewhile(self:L, f):
    "Like `takewhile`, but unpacks elements as args to `f`"
    return self._new(itertools.takewhile(lambda x: f(*x), self))

# %% ../nbs/02_foundation.ipynb #f64504c1
@patch
@curryable
def rstartakewhile(self:L, f):
    "Like `startakewhile`, but reverse the order of args"
    return self._new(itertools.takewhile(lambda x: f(*x[::-1]), self))

# %% ../nbs/02_foundation.ipynb #6544466c
@patch
@curryable
def stardropwhile(self:L, f):
    "Like `dropwhile`, but unpacks elements as args to `f`"
    return self._new(itertools.dropwhile(lambda x: f(*x), self))

# %% ../nbs/02_foundation.ipynb #91f75f7d
@patch
@curryable
def rstardropwhile(self:L, f):
    "Like `stardropwhile`, but reverse the order of args"
    return self._new(itertools.dropwhile(lambda x: f(*x[::-1]), self))

# %% ../nbs/02_foundation.ipynb #53cf2acc
@patch
@curryable
def accumulate(self:L, f=operator.add, initial=None):
    "Same as `itertools.accumulate`"
    return self._new(itertools.accumulate(self, f, initial=initial))

# %% ../nbs/02_foundation.ipynb #c55df328
@patch
def pairwise(self:L):
    "Same as `itertools.pairwise`"
    return self._new(itertools.pairwise(self))

# %% ../nbs/02_foundation.ipynb #1f2a370c
def _batched(iterable, n):
    "Batch data into tuples of length n. The last batch may be shorter."
    if n < 1: raise ValueError('n must be at least one')
    it = iter(iterable)
    while batch := tuple(itertools.islice(it, n)):
        yield batch

# %% ../nbs/02_foundation.ipynb #41aefe58
try: from itertools import batched
except ImportError: batched = _batched

# %% ../nbs/02_foundation.ipynb #d9f7ffb9
@patch
def batched(self:L, n):
    "Same as `itertools.batched` (but also works on older Python versions"
    return self._new(batched(self, n))

# %% ../nbs/02_foundation.ipynb #9e996aec
@patch
def compress(self:L, selectors):
    "Same as `itertools.compress`"
    return self._new(itertools.compress(self, selectors))

# %% ../nbs/02_foundation.ipynb #051afac0
@patch
def permutations(self:L, r=None):
    "Same as `itertools.permutations`"
    return self._new(itertools.permutations(self, r))

# %% ../nbs/02_foundation.ipynb #63066166
@patch
def combinations(self:L, r):
    "Same as `itertools.combinations`"
    return self._new(itertools.combinations(self, r))

# %% ../nbs/02_foundation.ipynb #f29ba907
@patch
@curryable
def partition(self:L, f=noop, **kwargs):
    "Split into two `L`s based on predicate `f`: (true_items, false_items)"
    a,b = [],[]
    for o in self: (a if f(o, **kwargs) else b).append(o)
    return self._new(a),self._new(b)

# %% ../nbs/02_foundation.ipynb #2dd5af2f
@patch
@curryable
def starpartition(self:L, f, **kwargs):
    "Like `partition`, but unpacks elements as args to `f`"
    a,b = [],[]
    for o in self: (a if f(*o, **kwargs) else b).append(o)
    return self._new(a),self._new(b)

# %% ../nbs/02_foundation.ipynb #63c70884
@patch
@curryable
def rstarpartition(self:L, f, **kwargs):
    "Like `starpartition`, but reverse the order of args"
    a,b = [],[]
    for o in self: (a if f(*o[::-1], **kwargs) else b).append(o)
    return self._new(a),self._new(b)

# %% ../nbs/02_foundation.ipynb #3c269425
@patch
def flatten(self:L):
    "Recursively flatten nested iterables (except strings)"
    def _flatten(o):
        for item in o:
            if isinstance(item, (str,bytes)) or not hasattr(item,'__iter__'): yield item
            else: yield from _flatten(item)
    return self._new(_flatten(self))

# %% ../nbs/02_foundation.ipynb #8e2bafde
def save_config_file(file, d, **kwargs):
    "Write settings dict to a new config file, or overwrite the existing one."
    config = ConfigParser(**kwargs)
    config['DEFAULT'] = d
    config.write(open(file, 'w'))

# %% ../nbs/02_foundation.ipynb #08333fdc
def read_config_file(file, **kwargs):
    config = ConfigParser(**kwargs)
    config.read(file, encoding='utf8')
    return config['DEFAULT']

# %% ../nbs/02_foundation.ipynb #95834369
def find_file_parents(fname, frompath=None):
    "Search `cfg_path` and its parents to find `cfg_name`"
    p = Path(frompath or Path.cwd()).expanduser().absolute()
    return first(o for o in [p, *p.parents] if (o/fname).exists())

# %% ../nbs/02_foundation.ipynb #e06640e8
class Config:
    "Reading and writing `ConfigParser` ini files"
    def __init__(self, cfg_path, cfg_name, create=None, save=True, extra_files=None, types=None, **cfg_kwargs):
        self.types = types or {}
        cfg_path = Path(cfg_path).expanduser().absolute()
        self.config_path,self.config_file = cfg_path,cfg_path/cfg_name
        self._cfg = ConfigParser(**cfg_kwargs)
        self.d = self._cfg['DEFAULT']
        found = [Path(o) for o in self._cfg.read(L(extra_files)+[self.config_file], encoding='utf8')]
        if self.config_file not in found and create is not None:
            self._cfg.read_dict({'DEFAULT':create})
            if save:
                cfg_path.mkdir(exist_ok=True, parents=True)
                save_config_file(self.config_file, create)

    def __repr__(self): return repr(dict(self._cfg.items('DEFAULT', raw=True)))
    def __setitem__(self,k,v): self.d[k] = str(v)
    def __contains__(self,k):  return k in self.d
    def save(self):            save_config_file(self.config_file,self.d)
    def __getattr__(self,k):   return stop(AttributeError(k)) if k=='d' or k not in self.d else self.get(k)
    def __getitem__(self,k):   return stop(IndexError(k)) if k not in self.d else self.get(k)

    def get(self,k,default=None):
        v = self.d.get(k, default)
        if v is None: return None
        typ = self.types.get(k, None)
        if typ==bool: return str2bool(v)
        if not typ: return str(v)
        if typ==Path: return self.config_path/v
        return typ(v)

    def path(self,k,default=None):
        v = self.get(k, default)
        return v if v is None else self.config_path/v

    @classmethod
    def find(cls, cfg_name, cfg_path=None, **kwargs):
        "Search `cfg_path` and its parents to find `cfg_name`"
        p = find_file_parents(cfg_name, cfg_path)
        return cls(p, cfg_name, **kwargs) if p else None
