Skip to content

Commit 44d6d92

Browse files
authored
Merge pull request #446 from seeM/only-nested-setdefault
2 parents 8c632ba + fefa8f8 commit 44d6d92

File tree

4 files changed

+167
-68
lines changed

4 files changed

+167
-68
lines changed

fastcore/_modidx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,12 @@
118118
'fastcore.basics.nested_attr': 'https://fastcore.fast.ai/basics.html#nested_attr',
119119
'fastcore.basics.nested_callable': 'https://fastcore.fast.ai/basics.html#nested_callable',
120120
'fastcore.basics.nested_idx': 'https://fastcore.fast.ai/basics.html#nested_idx',
121+
'fastcore.basics.nested_setdefault': 'https://fastcore.fast.ai/basics.html#nested_setdefault',
121122
'fastcore.basics.not_': 'https://fastcore.fast.ai/basics.html#not_',
122123
'fastcore.basics.null': 'https://fastcore.fast.ai/basics.html#null',
123124
'fastcore.basics.num_cpus': 'https://fastcore.fast.ai/basics.html#num_cpus',
124125
'fastcore.basics.num_methods': 'https://fastcore.fast.ai/basics.html#num_methods',
126+
'fastcore.basics.only': 'https://fastcore.fast.ai/basics.html#only',
125127
'fastcore.basics.otherwise': 'https://fastcore.fast.ai/basics.html#otherwise',
126128
'fastcore.basics.partialler': 'https://fastcore.fast.ai/basics.html#partialler',
127129
'fastcore.basics.patch': 'https://fastcore.fast.ai/basics.html#patch',

fastcore/basics.py

Lines changed: 66 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
'getcallable', 'getattrs', 'hasattrs', 'setattrs', 'try_attrs', 'GetAttrBase', 'GetAttr', 'delegate_attr',
1111
'ShowPrint', 'Int', 'Str', 'Float', 'flatten', 'concat', 'strcat', 'detuplify', 'replicate', 'setify',
1212
'merge', 'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle',
13-
'zip_cycle', 'sorted_ex', 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'nested_attr',
14-
'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx', 'uniqueify', 'loop_first_last', 'loop_first',
15-
'loop_last', 'fastuple', 'bind', 'mapt', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate',
16-
'using_attr', 'copy_func', 'patch_to', 'patch', 'patch_property', 'compile_re', 'ImportEnum', 'StrEnum',
17-
'str_enum', 'Stateful', 'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'typed', 'exec_new',
18-
'exec_import', 'str2bool', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub', 'mul', 'truediv', 'is_',
19-
'is_not']
13+
'zip_cycle', 'sorted_ex', 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'only', 'nested_attr',
14+
'nested_setdefault', 'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx', 'uniqueify',
15+
'loop_first_last', 'loop_first', 'loop_last', 'fastuple', 'bind', 'mapt', 'map_ex', 'compose', 'maps',
16+
'partialler', 'instantiate', 'using_attr', 'copy_func', 'patch_to', 'patch', 'patch_property', 'compile_re',
17+
'ImportEnum', 'StrEnum', 'str_enum', 'Stateful', 'PrettyString', 'even_mults', 'num_cpus', 'add_props',
18+
'typed', 'exec_new', 'exec_import', 'str2bool', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub', 'mul',
19+
'truediv', 'is_', 'is_not']
2020

2121
# %% ../nbs/01_basics.ipynb 1
2222
from .imports import *
@@ -651,19 +651,36 @@ def first(x, f=None, negate=False, **kwargs):
651651
return next(x, None)
652652

653653
# %% ../nbs/01_basics.ipynb 263
654+
def only(o):
655+
"Return the only item of `o`, raise if `o` doesn't have exactly one item"
656+
it = iter(o)
657+
try: res = next(it)
658+
except StopIteration: raise ValueError('iterable has 0 items') from None
659+
try: next(it)
660+
except StopIteration: return res
661+
raise ValueError(f'iterable has more than 1 item')
662+
663+
# %% ../nbs/01_basics.ipynb 265
654664
def nested_attr(o, attr, default=None):
655665
"Same as `getattr`, but if `attr` includes a `.`, then looks inside nested objects"
656666
try:
657667
for a in attr.split("."): o = getattr(o, a)
658668
except AttributeError: return default
659669
return o
660670

661-
# %% ../nbs/01_basics.ipynb 265
671+
# %% ../nbs/01_basics.ipynb 267
672+
def nested_setdefault(o, attr, default):
673+
"Same as `setdefault`, but if `attr` includes a `.`, then looks inside nested objects"
674+
attrs = attr.split('.')
675+
for a in attrs[:-1]: o = o.setdefault(a, type(o)())
676+
return o.setdefault(attrs[-1], default)
677+
678+
# %% ../nbs/01_basics.ipynb 271
662679
def nested_callable(o, attr):
663680
"Same as `nested_attr` but if not found will return `noop`"
664681
return nested_attr(o, attr, noop)
665682

666-
# %% ../nbs/01_basics.ipynb 267
683+
# %% ../nbs/01_basics.ipynb 273
667684
def _access(coll, idx): return coll.get(idx, None) if hasattr(coll, 'get') else coll[idx] if idx<len(coll) else None
668685

669686
def _nested_idx(coll, *idxs):
@@ -673,34 +690,34 @@ def _nested_idx(coll, *idxs):
673690
coll = coll.get(idx, None) if hasattr(coll, 'get') else coll[idx] if idx<len(coll) else None
674691
return coll,last_idx
675692

676-
# %% ../nbs/01_basics.ipynb 268
693+
# %% ../nbs/01_basics.ipynb 274
677694
def nested_idx(coll, *idxs):
678695
"Index into nested collections, dicts, etc, with `idxs`"
679696
if not coll or not idxs: return coll
680697
coll,idx = _nested_idx(coll, *idxs)
681698
if not coll or not idxs: return coll
682699
return _access(coll, idx)
683700

684-
# %% ../nbs/01_basics.ipynb 270
701+
# %% ../nbs/01_basics.ipynb 276
685702
def set_nested_idx(coll, value, *idxs):
686703
"Set value indexed like `nested_idx"
687704
coll,idx = _nested_idx(coll, *idxs)
688705
coll[idx] = value
689706

690-
# %% ../nbs/01_basics.ipynb 272
707+
# %% ../nbs/01_basics.ipynb 278
691708
def val2idx(x):
692709
"Dict from value to index"
693710
return {v:k for k,v in enumerate(x)}
694711

695-
# %% ../nbs/01_basics.ipynb 274
712+
# %% ../nbs/01_basics.ipynb 280
696713
def uniqueify(x, sort=False, bidir=False, start=None):
697714
"Unique elements in `x`, optional `sort`, optional return reverse correspondence, optional prepend with elements."
698715
res = list(dict.fromkeys(x))
699716
if start is not None: res = listify(start)+res
700717
if sort: res.sort()
701718
return (res,val2idx(res)) if bidir else res
702719

703-
# %% ../nbs/01_basics.ipynb 276
720+
# %% ../nbs/01_basics.ipynb 282
704721
# looping functions from https://github.com/willmcgugan/rich/blob/master/rich/_loop.py
705722
def loop_first_last(values):
706723
"Iterate and generate a tuple with a flag for first and last value."
@@ -713,17 +730,17 @@ def loop_first_last(values):
713730
first,previous_value = False,value
714731
yield first,True,previous_value
715732

716-
# %% ../nbs/01_basics.ipynb 278
733+
# %% ../nbs/01_basics.ipynb 284
717734
def loop_first(values):
718735
"Iterate and generate a tuple with a flag for first value."
719736
return ((b,o) for b,_,o in loop_first_last(values))
720737

721-
# %% ../nbs/01_basics.ipynb 280
738+
# %% ../nbs/01_basics.ipynb 286
722739
def loop_last(values):
723740
"Iterate and generate a tuple with a flag for last value."
724741
return ((b,o) for _,b,o in loop_first_last(values))
725742

726-
# %% ../nbs/01_basics.ipynb 283
743+
# %% ../nbs/01_basics.ipynb 289
727744
num_methods = """
728745
__add__ __sub__ __mul__ __matmul__ __truediv__ __floordiv__ __mod__ __divmod__ __pow__
729746
__lshift__ __rshift__ __and__ __xor__ __or__ __neg__ __pos__ __abs__
@@ -737,7 +754,7 @@ def loop_last(values):
737754
__ifloordiv__ __imod__ __ipow__ __ilshift__ __irshift__ __iand__ __ixor__ __ior__
738755
""".split()
739756

740-
# %% ../nbs/01_basics.ipynb 284
757+
# %% ../nbs/01_basics.ipynb 290
741758
class fastuple(tuple):
742759
"A `tuple` with elementwise ops and more friendly __init__ behavior"
743760
def __new__(cls, x=None, *rest):
@@ -774,7 +791,7 @@ def _f(self,*args): return self._op(op,*args)
774791
setattr(fastuple,'max',_get_op(max))
775792
setattr(fastuple,'min',_get_op(min))
776793

777-
# %% ../nbs/01_basics.ipynb 302
794+
# %% ../nbs/01_basics.ipynb 308
778795
class _Arg:
779796
def __init__(self,i): self.i = i
780797
arg0 = _Arg(0)
@@ -783,7 +800,7 @@ def __init__(self,i): self.i = i
783800
arg3 = _Arg(3)
784801
arg4 = _Arg(4)
785802

786-
# %% ../nbs/01_basics.ipynb 303
803+
# %% ../nbs/01_basics.ipynb 309
787804
class bind:
788805
"Same as `partial`, except you can use `arg0` `arg1` etc param placeholders"
789806
def __init__(self, func, *pargs, **pkwargs):
@@ -798,12 +815,12 @@ def __call__(self, *args, **kwargs):
798815
fargs = [args[x.i] if isinstance(x, _Arg) else x for x in self.pargs] + args[self.maxi+1:]
799816
return self.func(*fargs, **kwargs)
800817

801-
# %% ../nbs/01_basics.ipynb 315
818+
# %% ../nbs/01_basics.ipynb 321
802819
def mapt(func, *iterables):
803820
"Tuplified `map`"
804821
return tuple(map(func, *iterables))
805822

806-
# %% ../nbs/01_basics.ipynb 317
823+
# %% ../nbs/01_basics.ipynb 323
807824
def map_ex(iterable, f, *args, gen=False, **kwargs):
808825
"Like `map`, but use `bind`, and supports `str` and indexing"
809826
g = (bind(f,*args,**kwargs) if callable(f)
@@ -813,7 +830,7 @@ def map_ex(iterable, f, *args, gen=False, **kwargs):
813830
if gen: return res
814831
return list(res)
815832

816-
# %% ../nbs/01_basics.ipynb 325
833+
# %% ../nbs/01_basics.ipynb 331
817834
def compose(*funcs, order=None):
818835
"Create a function that composes all functions in `funcs`, passing along remaining `*args` and `**kwargs` to all"
819836
funcs = listify(funcs)
@@ -825,14 +842,14 @@ def _inner(x, *args, **kwargs):
825842
return x
826843
return _inner
827844

828-
# %% ../nbs/01_basics.ipynb 327
845+
# %% ../nbs/01_basics.ipynb 333
829846
def maps(*args, retain=noop):
830847
"Like `map`, except funcs are composed first"
831848
f = compose(*args[:-1])
832849
def _f(b): return retain(f(b), b)
833850
return map(_f, args[-1])
834851

835-
# %% ../nbs/01_basics.ipynb 329
852+
# %% ../nbs/01_basics.ipynb 335
836853
def partialler(f, *args, order=None, **kwargs):
837854
"Like `functools.partial` but also copies over docstring"
838855
fnew = partial(f,*args,**kwargs)
@@ -841,20 +858,20 @@ def partialler(f, *args, order=None, **kwargs):
841858
elif hasattr(f,'order'): fnew.order=f.order
842859
return fnew
843860

844-
# %% ../nbs/01_basics.ipynb 333
861+
# %% ../nbs/01_basics.ipynb 339
845862
def instantiate(t):
846863
"Instantiate `t` if it's a type, otherwise do nothing"
847864
return t() if isinstance(t, type) else t
848865

849-
# %% ../nbs/01_basics.ipynb 335
866+
# %% ../nbs/01_basics.ipynb 341
850867
def _using_attr(f, attr, x): return f(getattr(x,attr))
851868

852-
# %% ../nbs/01_basics.ipynb 336
869+
# %% ../nbs/01_basics.ipynb 342
853870
def using_attr(f, attr):
854871
"Construct a function which applies `f` to the argument's attribute `attr`"
855872
return partial(_using_attr, f, attr)
856873

857-
# %% ../nbs/01_basics.ipynb 340
874+
# %% ../nbs/01_basics.ipynb 346
858875
class _Self:
859876
"An alternative to `lambda` for calling methods on passed object."
860877
def __init__(self): self.nms,self.args,self.kwargs,self.ready = [],[],[],True
@@ -886,18 +903,18 @@ def _call(self, *args, **kwargs):
886903
self.ready = True
887904
return self
888905

889-
# %% ../nbs/01_basics.ipynb 341
906+
# %% ../nbs/01_basics.ipynb 347
890907
class _SelfCls:
891908
def __getattr__(self,k): return getattr(_Self(),k)
892909
def __getitem__(self,i): return self.__getattr__('__getitem__')(i)
893910
def __call__(self,*args,**kwargs): return self.__getattr__('_call')(*args,**kwargs)
894911

895912
Self = _SelfCls()
896913

897-
# %% ../nbs/01_basics.ipynb 342
914+
# %% ../nbs/01_basics.ipynb 348
898915
_all_ = ['Self']
899916

900-
# %% ../nbs/01_basics.ipynb 348
917+
# %% ../nbs/01_basics.ipynb 354
901918
def copy_func(f):
902919
"Copy a non-builtin function (NB `copy.copy` does not work for this)"
903920
if not isinstance(f,FunctionType): return copy(f)
@@ -908,7 +925,7 @@ def copy_func(f):
908925
fn.__qualname__ = f.__qualname__
909926
return fn
910927

911-
# %% ../nbs/01_basics.ipynb 354
928+
# %% ../nbs/01_basics.ipynb 360
912929
def patch_to(cls, as_prop=False, cls_method=False):
913930
"Decorator: add `f` to `cls`"
914931
if not isinstance(cls, (tuple,list)): cls=(cls,)
@@ -927,45 +944,45 @@ def _inner(f):
927944
return globals().get(nm, builtins.__dict__.get(nm, None))
928945
return _inner
929946

930-
# %% ../nbs/01_basics.ipynb 365
947+
# %% ../nbs/01_basics.ipynb 371
931948
def patch(f=None, *, as_prop=False, cls_method=False):
932949
"Decorator: add `f` to the first parameter's class (based on f's type annotations)"
933950
if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method)
934951
ann,glb,loc = get_annotations_ex(f)
935952
cls = union2tuple(eval_type(ann.pop('cls') if cls_method else next(iter(ann.values())), glb, loc))
936953
return patch_to(cls, as_prop=as_prop, cls_method=cls_method)(f)
937954

938-
# %% ../nbs/01_basics.ipynb 373
955+
# %% ../nbs/01_basics.ipynb 379
939956
def patch_property(f):
940957
"Deprecated; use `patch(as_prop=True)` instead"
941958
warnings.warn("`patch_property` is deprecated and will be removed; use `patch(as_prop=True)` instead")
942959
cls = next(iter(f.__annotations__.values()))
943960
return patch_to(cls, as_prop=True)(f)
944961

945-
# %% ../nbs/01_basics.ipynb 375
962+
# %% ../nbs/01_basics.ipynb 381
946963
def compile_re(pat):
947964
"Compile `pat` if it's not None"
948965
return None if pat is None else re.compile(pat)
949966

950-
# %% ../nbs/01_basics.ipynb 377
967+
# %% ../nbs/01_basics.ipynb 383
951968
class ImportEnum(enum.Enum):
952969
"An `Enum` that can have its values imported"
953970
@classmethod
954971
def imports(cls):
955972
g = sys._getframe(1).f_locals
956973
for o in cls: g[o.name]=o
957974

958-
# %% ../nbs/01_basics.ipynb 380
975+
# %% ../nbs/01_basics.ipynb 386
959976
class StrEnum(str,ImportEnum):
960977
"An `ImportEnum` that behaves like a `str`"
961978
def __str__(self): return self.name
962979

963-
# %% ../nbs/01_basics.ipynb 382
980+
# %% ../nbs/01_basics.ipynb 388
964981
def str_enum(name, *vals):
965982
"Simplified creation of `StrEnum` types"
966983
return StrEnum(name, {o:o for o in vals})
967984

968-
# %% ../nbs/01_basics.ipynb 384
985+
# %% ../nbs/01_basics.ipynb 390
969986
class Stateful:
970987
"A base class/mixin for objects that should not serialize all their state"
971988
_stateattrs=()
@@ -985,37 +1002,37 @@ def _init_state(self):
9851002
"Override for custom init and deserialization logic"
9861003
self._state = {}
9871004

988-
# %% ../nbs/01_basics.ipynb 390
1005+
# %% ../nbs/01_basics.ipynb 396
9891006
class PrettyString(str):
9901007
"Little hack to get strings to show properly in Jupyter."
9911008
def __repr__(self): return self
9921009

993-
# %% ../nbs/01_basics.ipynb 396
1010+
# %% ../nbs/01_basics.ipynb 402
9941011
def even_mults(start, stop, n):
9951012
"Build log-stepped array from `start` to `stop` in `n` steps."
9961013
if n==1: return stop
9971014
mult = stop/start
9981015
step = mult**(1/(n-1))
9991016
return [start*(step**i) for i in range(n)]
10001017

1001-
# %% ../nbs/01_basics.ipynb 398
1018+
# %% ../nbs/01_basics.ipynb 404
10021019
def num_cpus():
10031020
"Get number of cpus"
10041021
try: return len(os.sched_getaffinity(0))
10051022
except AttributeError: return os.cpu_count()
10061023

10071024
defaults.cpus = num_cpus()
10081025

1009-
# %% ../nbs/01_basics.ipynb 400
1026+
# %% ../nbs/01_basics.ipynb 406
10101027
def add_props(f, g=None, n=2):
10111028
"Create properties passing each of `range(n)` to f"
10121029
if g is None: return (property(partial(f,i)) for i in range(n))
10131030
return (property(partial(f,i), partial(g,i)) for i in range(n))
10141031

1015-
# %% ../nbs/01_basics.ipynb 403
1032+
# %% ../nbs/01_basics.ipynb 409
10161033
def _typeerr(arg, val, typ): return TypeError(f"{arg}=={val} not {typ}")
10171034

1018-
# %% ../nbs/01_basics.ipynb 404
1035+
# %% ../nbs/01_basics.ipynb 410
10191036
def typed(f):
10201037
"Decorator to check param and return types at runtime"
10211038
names = f.__code__.co_varnames
@@ -1032,21 +1049,21 @@ def _f(*args,**kwargs):
10321049
return res
10331050
return functools.update_wrapper(_f, f)
10341051

1035-
# %% ../nbs/01_basics.ipynb 412
1052+
# %% ../nbs/01_basics.ipynb 418
10361053
def exec_new(code):
10371054
"Execute `code` in a new environment and return it"
10381055
pkg = None if __name__=='__main__' else Path().cwd().name
10391056
g = {'__name__': __name__, '__package__': pkg}
10401057
exec(code, g)
10411058
return g
10421059

1043-
# %% ../nbs/01_basics.ipynb 414
1060+
# %% ../nbs/01_basics.ipynb 420
10441061
def exec_import(mod, sym):
10451062
"Import `sym` from `mod` in a new environment"
10461063
# pref = '' if __name__=='__main__' or mod[0]=='.' else '.'
10471064
return exec_new(f'from {mod} import {sym}')
10481065

1049-
# %% ../nbs/01_basics.ipynb 415
1066+
# %% ../nbs/01_basics.ipynb 421
10501067
def str2bool(s):
10511068
"Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,`true`,`on`,`1`->`True`)"
10521069
if not isinstance(s,str): return bool(s)

nbs/.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
**/*.ipynb filter=clean-nbs
22
**/*.ipynb diff=ipynb
3+
*.ipynb merge=nbdev-merge

0 commit comments

Comments
 (0)