# Fastcore > --- # Source: https://fastcore.fast.ai/basics.html.md # Basic functionality ## Basics ------------------------------------------------------------------------ source ### ifnone ``` python def ifnone( a, b ): ``` *`b` if `a` is None else `a`* Since `b if a is None else a` is such a common pattern, we wrap it in a function. However, be careful, because python will evaluate *both* `a` and `b` when calling [`ifnone`](https://fastcore.fast.ai/basics.html#ifnone) (which it doesn’t do if using the `if` version directly). ``` python test_eq(ifnone(None,1), 1) test_eq(ifnone(2 ,1), 2) ``` ------------------------------------------------------------------------ source ### maybe_attr ``` python def maybe_attr( o, attr ): ``` *`getattr(o,attr,o)`* Return the attribute `attr` for object `o`. If the attribute doesn’t exist, then return the object `o` instead. ``` python class myobj: myattr='foo' test_eq(maybe_attr(myobj, 'myattr'), 'foo') test_eq(maybe_attr(myobj, 'another_attr'), myobj) ``` ------------------------------------------------------------------------ source ### basic_repr ``` python def basic_repr( flds:NoneType=None ): ``` *Minimal `__repr__`* In types which provide rich display functionality in Jupyter, their `__repr__` is also called in order to provide a fallback text representation. Unfortunately, this includes a memory address which changes on every invocation, making it non-deterministic. This causes diffs to get messy and creates conflicts in git. To fix this, put `__repr__=basic_repr()` inside your class. ``` python class SomeClass: __repr__=basic_repr() repr(SomeClass()) ``` 'SomeClass()' If you pass a list of attributes (`flds`) of an object, then this will generate a string with the name of each attribute and its corresponding value. The format of this string is `key=value`, where `key` is the name of the attribute, and `value` is the value of the attribute. For each value, attempt to use the `__name__` attribute, otherwise fall back to using the value’s `__repr__` when constructing the string. ``` python class SomeClass: a=1 b='foo' __repr__=basic_repr('a,b') __name__='some-class' repr(SomeClass()) ``` "SomeClass(a=1, b='foo')" Nested objects work too: ``` python class AnotherClass: c=SomeClass() d='bar' __repr__=basic_repr(['c', 'd']) repr(AnotherClass()) ``` "AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')" Instance variables (but not class variables) are shown if [`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) is called with no arguments: ``` python class SomeClass: def __init__(self, a=1, b='foo'): self.a,self.b = a,b __repr__=basic_repr() repr(SomeClass()) ``` "SomeClass(a=1, b='foo')" ------------------------------------------------------------------------ source ### BasicRepr ``` python def BasicRepr( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Base class for objects needing a basic `__repr__`* As a shortcut for creating a `__repr__` for instance variables, you can inherit from [`BasicRepr`](https://fastcore.fast.ai/basics.html#basicrepr): ``` python class SomeClass(BasicRepr): def __init__(self, a=1, b='foo'): self.a,self.b = a,b repr(SomeClass()) ``` "SomeClass(a=1, b='foo')" ------------------------------------------------------------------------ source ### is_array ``` python def is_array( x ): ``` *`True` if `x` supports `__array__` or `iloc`* ``` python is_array(np.array(1)),is_array([1]) ``` (True, False) ------------------------------------------------------------------------ source ### listify ``` python def listify( o:NoneType=None, rest:VAR_POSITIONAL, use_list:bool=False, match:NoneType=None ): ``` *Convert `o` to a `list`* Conversion is designed to “do what you mean”, e.g: ``` python test_eq(listify('hi'), ['hi']) test_eq(listify(b'hi'), [b'hi']) test_eq(listify(array(1)), [array(1)]) test_eq(listify(1), [1]) test_eq(listify([1,2]), [1,2]) test_eq(listify(range(3)), [0,1,2]) test_eq(listify(None), []) test_eq(listify(1,2), [1,2]) ``` ``` python arr = np.arange(9).reshape(3,3) listify(arr) ``` [array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])] ``` python listify(array([1,2])) ``` [array([1, 2])] Generators are turned into lists too: ``` python gen = (o for o in range(3)) test_eq(listify(gen), [0,1,2]) ``` Use `match` to provide a length to match: ``` python test_eq(listify(1,match=3), [1,1,1]) ``` If `match` is a sequence, it’s length is used: ``` python test_eq(listify(1,match=range(3)), [1,1,1]) ``` If the listified item is not of length `1`, it must be the same length as `match`: ``` python test_eq(listify([1,1,1],match=3), [1,1,1]) test_fail(lambda: listify([1,1],match=3)) ``` ------------------------------------------------------------------------ source ### tuplify ``` python def tuplify( o, use_list:bool=False, match:NoneType=None ): ``` *Make `o` a tuple* ``` python test_eq(tuplify(None),()) test_eq(tuplify([1,2,3]),(1,2,3)) test_eq(tuplify(1,match=[1,2,3]),(1,1,1)) ``` ------------------------------------------------------------------------ source ### true ``` python def true( x ): ``` *Test whether `x` is truthy; collections with \>0 elements are considered `True`* ``` python [(o,true(o)) for o in (array(0),array(1),array([0]),array([0,1]),1,0,'',None)] ``` [(array(0), False), (array(1), True), (array([0]), True), (array([0, 1]), True), (1, True), (0, False), ('', False), (None, False)] ------------------------------------------------------------------------ source ### NullType ``` python def NullType( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *An object that is `False` and can be called, chained, and indexed* ``` python bool(null.hi().there[3]) ``` False ------------------------------------------------------------------------ source ### tonull ``` python def tonull( x ): ``` *Convert `None` to `null`* ``` python bool(tonull(None).hi().there[3]) ``` False ------------------------------------------------------------------------ source ### get_class ``` python def get_class( nm, fld_names:VAR_POSITIONAL, sup:NoneType=None, doc:NoneType=None, funcs:NoneType=None, anno:NoneType=None, flds:VAR_KEYWORD ): ``` *Dynamically create a class, optionally inheriting from `sup`, containing `fld_names`* ``` python _t = get_class('_t', 'a', b=2, anno={'b':int}) t = _t() test_eq(t.a, None) test_eq(t.b, 2) t = _t(1, b=3) test_eq(t.a, 1) test_eq(t.b, 3) t = _t(1, 3) test_eq(t.a, 1) test_eq(t.b, 3) test_eq(t, pickle.loads(pickle.dumps(t))) test_eq(_t.__annotations__, {'b':int, 'a':typing.Any}) repr(t) ``` '_t(a=1, b=3)' Most often you’ll want to call [`mk_class`](https://fastcore.fast.ai/basics.html#mk_class), since it adds the class to your module. See [`mk_class`](https://fastcore.fast.ai/basics.html#mk_class) for more details and examples of use (which also apply to [`get_class`](https://fastcore.fast.ai/basics.html#get_class)). ------------------------------------------------------------------------ source ### mk_class ``` python def mk_class( nm, fld_names:VAR_POSITIONAL, sup:NoneType=None, doc:NoneType=None, funcs:NoneType=None, mod:NoneType=None, anno:NoneType=None, flds:VAR_KEYWORD ): ``` *Create a class using [`get_class`](https://fastcore.fast.ai/basics.html#get_class) and add to the caller’s module* Any `kwargs` will be added as class attributes, and `sup` is an optional (tuple of) base classes. ``` python mk_class('_t', a=1, sup=dict) t = _t() test_eq(t.a, 1) assert(isinstance(t,dict)) ``` A `__init__` is provided that sets attrs for any `kwargs`, and for any `args` (matching by position to fields), along with a `__repr__` which prints all attrs. The docstring is set to `doc`. You can pass `funcs` which will be added as attrs with the function names. ``` python def foo(self): return 1 mk_class('_t', 'a', sup=dict, doc='test doc', funcs=foo) t = _t(3, b=2) test_eq(t.a, 3) test_eq(t.b, 2) test_eq(t.foo(), 1) test_eq(t.__doc__, 'test doc') t ``` {} ------------------------------------------------------------------------ source ### wrap_class ``` python def wrap_class( nm, fld_names:VAR_POSITIONAL, sup:NoneType=None, doc:NoneType=None, funcs:NoneType=None, flds:VAR_KEYWORD ): ``` *Decorator: makes function a method of a new class `nm` passing parameters to [`mk_class`](https://fastcore.fast.ai/basics.html#mk_class)* ``` python @wrap_class('_t', a=2) def bar(self,x): return x+1 t = _t() test_eq(t.a, 2) test_eq(t.bar(3), 4) ``` ------------------------------------------------------------------------ source #### ignore_exceptions ``` python def ignore_exceptions( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Context manager to ignore exceptions* ``` python with ignore_exceptions(): # Exception will be ignored raise Exception ``` ------------------------------------------------------------------------ source ### exec_local ``` python def exec_local( code, var_name ): ``` *Call `exec` on `code` and return the var `var_name`* ``` python test_eq(exec_local("a=1", "a"), 1) ``` ------------------------------------------------------------------------ source ### risinstance ``` python def risinstance( types, obj:NoneType=None ): ``` *Curried `isinstance` but with args reversed* ``` python assert risinstance(int, 1) assert not risinstance(str, 0) assert risinstance(int)(1) assert not risinstance(int)(None) ``` `types` can also be strings: ``` python assert risinstance(('str','int'), 'a') assert risinstance('str', 'a') assert not risinstance('int', 'a') ``` ------------------------------------------------------------------------ source ### ver2tuple ``` python def ver2tuple( v:str )->tuple: ``` ``` python test_eq(ver2tuple('3.8.1'), (3,8,1)) test_eq(ver2tuple('3.1'), (3,1,0)) test_eq(ver2tuple('3.'), (3,0,0)) test_eq(ver2tuple('3'), (3,0,0)) ``` ## NoOp These are used when you need a pass-through function. ------------------------------------------------------------------------ ### noop ``` python def noop( x:NoneType=None, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Do nothing* ``` python noop() test_eq(noop(1),1) ``` ------------------------------------------------------------------------ ### noops ``` python def noops( x:NoneType=None, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Do nothing (method)* ``` python class _t: foo=noops test_eq(_t().foo(1),1) ``` ## Infinite Lists These lists are useful for things like padding an array or adding index column(s) to arrays. [`Inf`](https://fastcore.fast.ai/basics.html#inf) defines the following properties: - `count: itertools.count()` - `zeros: itertools.cycle([0])` - `ones : itertools.cycle([1])` - `nones: itertools.cycle([None])` ``` python test_eq([o for i,o in zip(range(5), Inf.count)], [0, 1, 2, 3, 4]) test_eq([o for i,o in zip(range(5), Inf.zeros)], [0]*5) test_eq([o for i,o in zip(range(5), Inf.ones)], [1]*5) test_eq([o for i,o in zip(range(5), Inf.nones)], [None]*5) ``` ## Operator Functions ------------------------------------------------------------------------ source ### in\_ ``` python def in_( x, a ): ``` *`True` if `x in a`* ``` python # test if element is in another assert in_('c', ('b', 'c', 'a')) assert in_(4, [2,3,4,5]) assert in_('t', 'fastai') test_fail(in_('h', 'fastai')) # use in_ as a partial assert in_('fastai')('t') assert in_([2,3,4,5])(4) test_fail(in_('fastai')('h')) ``` In addition to [`in_`](https://fastcore.fast.ai/basics.html#in_), the following functions are provided matching the behavior of the equivalent versions in `operator`: *lt gt le ge eq ne add sub mul truediv is\_ is_not mod*. ``` python lt(3,5),gt(3,5),is_(None,None),in_(0,[1,2]),mod(3,2) ``` (True, False, True, False, 1) Similarly to `_in`, they also have additional functionality: if you only pass one param, they return a partial function that passes that param as the second positional parameter. ``` python lt(5)(3),gt(5)(3),is_(None)(None),in_([1,2])(0),mod(2)(3) ``` (True, False, True, False, 1) ------------------------------------------------------------------------ source ### ret_true ``` python def ret_true( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Predicate: always `True`* ``` python assert ret_true(1,2,3) assert ret_true(False) ``` ------------------------------------------------------------------------ source ### ret_false ``` python def ret_false( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Predicate: always `False`* ------------------------------------------------------------------------ source ### stop ``` python def stop( e:type=StopIteration ): ``` *Raises exception `e` (by default `StopIteration`)* ------------------------------------------------------------------------ source ### gen ``` python def gen( func, seq, cond:function=ret_true ): ``` *Like `(func(o) for o in seq if cond(func(o)))` but handles `StopIteration`* ``` python test_eq(gen(noop, Inf.count, lt(5)), range(5)) test_eq(gen(operator.neg, Inf.count, gt(-5)), [0,-1,-2,-3,-4]) test_eq(gen(lambda o:o if o<5 else stop(), Inf.count), range(5)) ``` ------------------------------------------------------------------------ source ### chunked ``` python def chunked( it, chunk_sz:NoneType=None, drop_last:bool=False, n_chunks:NoneType=None, pad:bool=False, pad_val:NoneType=None ): ``` *Return batches from iterator `it` of size `chunk_sz` (or return `n_chunks` total)* Note that you must pass either `chunk_sz`, or `n_chunks`, but not both. ``` python t = list(range(10)) test_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]]) test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ]) t = map(lambda o:stop() if o==6 else o, Inf.count) test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5]]) t = map(lambda o:stop() if o==7 else o, Inf.count) test_eq(chunked(t,3), [[0, 1, 2], [3, 4, 5], [6]]) t = np.arange(10) test_eq(chunked(t,3), [[0,1,2], [3,4,5], [6,7,8], [9]]) test_eq(chunked(t,3,True), [[0,1,2], [3,4,5], [6,7,8], ]) test_eq(chunked([], 3), []) test_eq(chunked([], n_chunks=3), []) ``` Pass `pad=True` and an optional `pad_val` to pad the last chunk: ``` python t = list(range(10)) test_eq(chunked(t,3,pad=True), [[0,1,2], [3,4,5], [6,7,8], [9,None,None]]) test_eq(chunked(t,3,pad=True,pad_val=0), [[0,1,2], [3,4,5], [6,7,8], [9,0,0]]) test_eq(chunked(t,4,pad=True,pad_val=-1), [[0,1,2,3], [4,5,6,7], [8,9,-1,-1]]) test_eq(chunked(range(5),2,pad=True), [[0,1], [2,3], [4,None]]) ``` ------------------------------------------------------------------------ source ### otherwise ``` python def otherwise( x, tst, y ): ``` *`y if tst(x) else x`* ``` python test_eq(otherwise(2+1, gt(3), 4), 3) test_eq(otherwise(2+1, gt(2), 4), 4) ``` ## Attribute Helpers These functions reduce boilerplate when setting or manipulating attributes or properties of objects. ------------------------------------------------------------------------ source ### custom_dir ``` python def custom_dir( c, add ): ``` *Implement custom `__dir__`, adding `add` to `cls`* [`custom_dir`](https://fastcore.fast.ai/basics.html#custom_dir) allows you extract the [`__dict__` property of a class](https://stackoverflow.com/questions/19907442/explain-dict-attribute) and appends the list `add` to it. ``` python class _T: def f(): pass s = custom_dir(_T(), add=['foo', 'bar']) assert {'foo', 'bar', 'f'}.issubset(s) ``` ------------------------------------------------------------------------ source ### AttrDict ``` python def AttrDict( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *`dict` subclass that also provides access to keys as attrs* ``` python d = AttrDict(a=1,b="two") test_eq(d.a, 1) test_eq(d['b'], 'two') test_eq(d.get('c','nope'), 'nope') d.b = 2 test_eq(d.b, 2) test_eq(d['b'], 2) d['b'] = 3 test_eq(d['b'], 3) test_eq(d.b, 3) assert 'a' in dir(d) ``` [`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict) will pretty print in Jupyter Notebooks: ``` python _test_dict = {'a':1, 'b': {'c':1, 'd':2}, 'c': {'c':1, 'd':2}, 'd': {'c':1, 'd':2}, 'e': {'c':1, 'd':2}, 'f': {'c':1, 'd':2, 'e': 4, 'f':[1,2,3,4,5]}} AttrDict(_test_dict) ``` ``` python { 'a': 1, 'b': {'c': 1, 'd': 2}, 'c': {'c': 1, 'd': 2}, 'd': {'c': 1, 'd': 2}, 'e': {'c': 1, 'd': 2}, 'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}} ``` ------------------------------------------------------------------------ source ### AttrDictDefault ``` python def AttrDictDefault( args:VAR_POSITIONAL, default_:NoneType=None, kwargs:VAR_KEYWORD ): ``` *[`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict) subclass that returns `None` for missing attrs* ``` python d = AttrDictDefault(a=1,b="two", default_='nope') test_eq(d.a, 1) test_eq(d['b'], 'two') test_eq(d.c, 'nope') ``` ------------------------------------------------------------------------ source ### NS ``` python def NS( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *`SimpleNamespace` subclass that also adds `iter` and `dict` support* This is very similar to [`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict), but since it starts with `SimpleNamespace`, it has some differences in behavior. You can use it just like `SimpleNamespace`: ``` python d = NS(**_test_dict) d ``` namespace(a=1, b={'c': 1, 'd': 2}, c={'c': 1, 'd': 2}, d={'c': 1, 'd': 2}, e={'c': 1, 'd': 2}, f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}) …but you can also index it to get/set: ``` python d['a'] ``` 1 …and iterate t: ``` python list(d) ``` ['a', 'b', 'c', 'd', 'e', 'f'] ------------------------------------------------------------------------ source ### get_annotations_ex ``` python def get_annotations_ex( obj, globals:NoneType=None, locals:NoneType=None ): ``` *Backport of py3.10 `get_annotations` that returns globals/locals* In Python 3.10 `inspect.get_annotations` was added. However previous versions of Python are unable to evaluate type annotations correctly if `from future import __annotations__` is used. Furthermore, *all* annotations are evaluated, even if only some subset are needed. [`get_annotations_ex`](https://fastcore.fast.ai/basics.html#get_annotations_ex) provides the same functionality as `inspect.get_annotations`, but works on earlier versions of Python, and returns the `globals` and `locals` needed to evaluate types. ------------------------------------------------------------------------ source ### eval_type ``` python def eval_type( t, glb, loc ): ``` *`eval` a type or collection of types, if needed, for annotations in py3.10+* In py3.10, or if `from future import __annotations__` is used, `a` is a `str`: ``` python class _T2a: pass def func(a: _T2a): pass ann,glb,loc = get_annotations_ex(func) eval_type(ann['a'], glb, loc) ``` __main__._T2a `|` is supported for defining `Union` types when using [`eval_type`](https://fastcore.fast.ai/basics.html#eval_type) even for python versions prior to 3.9: ``` python class _T2b: pass def func(a: _T2a|_T2b): pass ann,glb,loc = get_annotations_ex(func) eval_type(ann['a'], glb, loc) ``` typing.Union[__main__._T2a, __main__._T2b] ------------------------------------------------------------------------ source ### type_hints ``` python def type_hints( f ): ``` *Like `typing.get_type_hints` but returns `{}` if not allowed type* ``` python def type_hints(f): "Like `typing.get_type_hints` but returns `{}` if not allowed type" if not isinstance(f, _allowed_types): return {} ann,glb,loc = get_annotations_ex(f) return {k:_eval_type(v,glb,loc) for k,v in ann.items()} ``` For example, type `func` is allowed so [`type_hints`](https://fastcore.fast.ai/basics.html#type_hints) returns the same value as `typing.get_hints`: ``` python def f(a:int)->bool: ... # a function with type hints (allowed) exp = {'a':int,'return':bool} test_eq(type_hints(f), typing.get_type_hints(f)) test_eq(type_hints(f), exp) ``` However, `class` is not an allowed type, so [`type_hints`](https://fastcore.fast.ai/basics.html#type_hints) returns `{}`: ``` python class _T: def __init__(self, a:int=0)->bool: ... assert not type_hints(_T) ``` ------------------------------------------------------------------------ source ### annotations ``` python def annotations( o ): ``` *Annotations for `o`, or `type(o)`* This supports a wider range of situations than [`type_hints`](https://fastcore.fast.ai/basics.html#type_hints), by checking `type()` and `__init__` for annotations too: ``` python for o in _T,_T(),_T.__init__,f: test_eq(annotations(o), exp) assert not annotations(int) assert not annotations(print) ``` ------------------------------------------------------------------------ source ### anno_ret ``` python def anno_ret( func ): ``` *Get the return annotation of `func`* ``` python def f(x) -> float: return x test_eq(anno_ret(f), float) def f(x) -> typing.Tuple[float,float]: return x assert anno_ret(f)==typing.Tuple[float,float] ``` If your return annotation is `None`, [`anno_ret`](https://fastcore.fast.ai/basics.html#anno_ret) will return `NoneType` (and not `None`): ``` python def f(x) -> None: return x test_eq(anno_ret(f), NoneType) assert anno_ret(f) is not None # returns NoneType instead of None ``` If your function does not have a return type, or if you pass in `None` instead of a function, then [`anno_ret`](https://fastcore.fast.ai/basics.html#anno_ret) returns `None`: ``` python def f(x): return x test_eq(anno_ret(f), None) test_eq(anno_ret(None), None) # instead of passing in a func, pass in None ``` ------------------------------------------------------------------------ source ### signature_ex ``` python def signature_ex( obj, eval_str:bool=False ): ``` *Backport of `inspect.signature(..., eval_str=True` to \source ### union2tuple ``` python def union2tuple( t ): ``` ``` python test_eq(union2tuple(Union[int,str]), (int,str)) test_eq(union2tuple(int), int) assert union2tuple(Tuple[int,str])==Tuple[int,str] test_eq(union2tuple((int,str)), (int,str)) if UnionType: test_eq(union2tuple(int|str), (int,str)) ``` ------------------------------------------------------------------------ source ### argnames ``` python def argnames( f, frame:bool=False ): ``` *Names of arguments to function or frame `f`* ``` python test_eq(argnames(f), ['x']) ``` ------------------------------------------------------------------------ source ### with_cast ``` python def with_cast( f ): ``` *Decorator which uses any parameter annotations as preprocessing functions* ``` python @with_cast def _f(a, b:Path, c:str='', d=0): return (a,b,c,d) test_eq(_f(1, '.', 3), (1,Path('.'),'3',0)) test_eq(_f(1, '.'), (1,Path('.'),'',0)) @with_cast def _g(a:int=0)->str: return a test_eq(_g(4.0), '4') test_eq(_g(4.4), '4') test_eq(_g(2), '2') ``` ------------------------------------------------------------------------ source ### store_attr ``` python def store_attr( names:NoneType=None, but:str='', cast:bool=False, store_args:NoneType=None, attrs:VAR_KEYWORD ): ``` *Store params named in comma-separated `names` from calling context into attrs in `self`* In it’s most basic form, you can use [`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to shorten code like this: ``` python class T: def __init__(self, a,b,c): self.a,self.b,self.c = a,b,c ``` …to this: ``` python class T: def __init__(self, a,b,c): store_attr('a,b,c', self) ``` This class behaves as if we’d used the first form: ``` python t = T(1,c=2,b=3) assert t.a==1 and t.b==3 and t.c==2 ``` ``` python class T1: def __init__(self, a,b,c): store_attr() ``` In addition, it stores the attrs as a `dict` in `__stored_args__`, which you can use for display, logging, and so forth. ``` python test_eq(t.__stored_args__, {'a':1, 'b':3, 'c':2}) ``` Since you normally want to use the first argument (often called `self`) for storing attributes, it’s optional: ``` python class T: def __init__(self, a,b,c:str): store_attr('a,b,c') t = T(1,c=2,b=3) assert t.a==1 and t.b==3 and t.c==2 ``` With `cast=True` any parameter annotations will be used as preprocessing functions for the corresponding arguments: ``` python class T: def __init__(self, a:listify, b, c:str): store_attr('a,b,c', cast=True) t = T(1,c=2,b=3) assert t.a==[1] and t.b==3 and t.c=='2' ``` You can inherit from a class using [`store_attr`](https://fastcore.fast.ai/basics.html#store_attr), and just call it again to add in any new attributes added in the derived class: ``` python class T2(T): def __init__(self, d, **kwargs): super().__init__(**kwargs) store_attr('d') t = T2(d=1,a=2,b=3,c=4) assert t.a==2 and t.b==3 and t.c==4 and t.d==1 ``` You can skip passing a list of attrs to store. In this case, all arguments passed to the method are stored: ``` python class T: def __init__(self, a,b,c): store_attr() t = T(1,c=2,b=3) assert t.a==1 and t.b==3 and t.c==2 ``` ``` python class T4(T): def __init__(self, d, **kwargs): super().__init__(**kwargs) store_attr() t = T4(4, a=1,c=2,b=3) assert t.a==1 and t.b==3 and t.c==2 and t.d==4 ``` ``` python class T4: def __init__(self, *, a: int, b: float = 1): store_attr() t = T4(a=3) assert t.a==3 and t.b==1 t = T4(a=3, b=2) assert t.a==3 and t.b==2 ``` You can skip some attrs by passing `but`: ``` python class T: def __init__(self, a,b,c): store_attr(but='a') t = T(1,c=2,b=3) assert t.b==3 and t.c==2 assert not hasattr(t,'a') ``` You can also pass keywords to [`store_attr`](https://fastcore.fast.ai/basics.html#store_attr), which is identical to setting the attrs directly, but also stores them in `__stored_args__`. ``` python class T: def __init__(self): store_attr(a=1) t = T() assert t.a==1 ``` You can also use store_attr inside functions. ``` python def create_T(a, b): t = SimpleNamespace() store_attr(self=t) return t t = create_T(a=1, b=2) assert t.a==1 and t.b==2 ``` ------------------------------------------------------------------------ source ### attrdict ``` python def attrdict( o, ks:VAR_POSITIONAL, default:NoneType=None ): ``` *Dict from each `k` in `ks` to `getattr(o,k)`* ``` python class T: def __init__(self, a,b,c): store_attr() t = T(1,c=2,b=3) test_eq(attrdict(t,'b','c'), {'b':3, 'c':2}) ``` ------------------------------------------------------------------------ source ### properties ``` python def properties( cls, ps:VAR_POSITIONAL ): ``` *Change attrs in `cls` with names in `ps` to properties* ``` python class T: def a(self): return 1 def b(self): return 2 properties(T,'a') test_eq(T().a,1) test_eq(T().b(),2) ``` ------------------------------------------------------------------------ source ### camel2words ``` python def camel2words( s, space:str=' ' ): ``` *Convert CamelCase to ‘spaced words’* ``` python test_eq(camel2words('ClassAreCamel'), 'Class Are Camel') ``` ------------------------------------------------------------------------ source ### camel2snake ``` python def camel2snake( name ): ``` *Convert CamelCase to snake_case* ``` python test_eq(camel2snake('ClassAreCamel'), 'class_are_camel') test_eq(camel2snake('Already_Snake'), 'already__snake') ``` ------------------------------------------------------------------------ source ### snake2camel ``` python def snake2camel( s ): ``` *Convert snake_case to CamelCase* ``` python test_eq(snake2camel('a_b_cc'), 'ABCc') ``` ------------------------------------------------------------------------ source ### class2attr ``` python def class2attr( cls_name ): ``` *Return the snake-cased name of the class; strip ending `cls_name` if it exists.* ``` python class Parent: @property def name(self): return class2attr(self, 'Parent') class ChildOfParent(Parent): pass class ParentChildOf(Parent): pass p = Parent() cp = ChildOfParent() cp2 = ParentChildOf() test_eq(p.name, 'parent') test_eq(cp.name, 'child_of') test_eq(cp2.name, 'parent_child_of') ``` ------------------------------------------------------------------------ source ### getcallable ``` python def getcallable( o, attr ): ``` *Calls `getattr` with a default of `noop`* ``` python class Math: def addition(self,a,b): return a+b m = Math() test_eq(getcallable(m, "addition")(a=1,b=2), 3) test_eq(getcallable(m, "subtraction")(a=1,b=2), None) ``` ------------------------------------------------------------------------ source ### getattrs ``` python def getattrs( o, attrs:VAR_POSITIONAL, default:NoneType=None ): ``` *List of all `attrs` in `o`* ``` python from fractions import Fraction ``` ``` python getattrs(Fraction(1,2), 'numerator', 'denominator') ``` [1, 2] ------------------------------------------------------------------------ source ### hasattrs ``` python def hasattrs( o, attrs ): ``` *Test whether `o` contains all `attrs`* ``` python assert hasattrs(1,('imag','real')) assert not hasattrs(1,('imag','foo')) ``` ------------------------------------------------------------------------ source ### setattrs ``` python def setattrs( dest, flds, src ): ``` ``` python d = dict(a=1,bb="2",ignore=3) o = SimpleNamespace() setattrs(o, "a,bb", d) test_eq(o.a, 1) test_eq(o.bb, "2") ``` ``` python d = SimpleNamespace(a=1,bb="2",ignore=3) o = SimpleNamespace() setattrs(o, "a,bb", d) test_eq(o.a, 1) test_eq(o.bb, "2") ``` ------------------------------------------------------------------------ source ### try_attrs ``` python def try_attrs( obj, attrs:VAR_POSITIONAL ): ``` *Return first attr that exists in `obj`* ``` python test_eq(try_attrs(1, 'real'), 1) test_eq(try_attrs(1, 'foobar', 'real'), 1) ``` ## Attribute Delegation ------------------------------------------------------------------------ source ### GetAttrBase ``` python def GetAttrBase( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Basic delegation of [`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) and `__dir__`* ------------------------------------------------------------------------ source #### GetAttr ``` python def GetAttr( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`* Inherit from [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) to have attr access passed down to an instance attribute. This makes it easy to create composites that don’t require callers to know about their components. For a more detailed discussion of how this works as well as relevant context, we suggest reading the [delegated composition section of this blog article](https://www.fast.ai/2019/08/06/delegation/). You can customise the behaviour of [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) in subclasses via; - `_default` - By default, this is set to `'default'`, so attr access is passed down to `self.default` - `_default` can be set to the name of any instance attribute that does not start with dunder `__` - `_xtra` - By default, this is `None`, so all attr access is passed down - You can limit which attrs get passed down by setting `_xtra` to a list of attribute names To illuminate the utility of [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr), suppose we have the following two classes, `_WebPage` which is a superclass of `_ProductPage`, which we wish to compose like so: ``` python class _WebPage: def __init__(self, title, author="Jeremy"): self.title,self.author = title,author class _ProductPage: def __init__(self, page, price): self.page,self.price = page,price page = _WebPage('Soap', author="Sylvain") p = _ProductPage(page, 15.0) ``` How do we make it so we can just write `p.author`, instead of `p.page.author` to access the `author` attribute? We can use [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr), of course! First, we subclass [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) when defining `_ProductPage`. Next, we set `self.default` to the object whose attributes we want to be able to access directly, which in this case is the `page` argument passed on initialization: ``` python class _ProductPage(GetAttr): def __init__(self, page, price): self.default,self.price = page,price #self.default allows you to access page directly. p = _ProductPage(page, 15.0) ``` Now, we can access the `author` attribute directly from the instance: ``` python test_eq(p.author, 'Sylvain') ``` If you wish to store the object you are composing in an attribute other than `self.default`, you can set the class attribute `_data` as shown below. This is useful in the case where you might have a name collision with `self.default`: ``` python class _C(GetAttr): _default = '_data' # use different component name; `self._data` rather than `self.default` def __init__(self,a): self._data = a def foo(self): noop t = _C('Hi') test_eq(t._data, 'Hi') test_fail(lambda: t.default) # we no longer have self.default test_eq(t.lower(), 'hi') test_eq(t.upper(), 'HI') assert 'lower' in dir(t) assert 'upper' in dir(t) ``` By default, all attributes and methods of the object you are composing are retained. In the below example, we compose a `str` object with the class `_C`. This allows us to directly call string methods on instances of class `_C`, such as `str.lower()` or `str.upper()`: ``` python class _C(GetAttr): # allow all attributes and methods to get passed to `self.default` (by leaving _xtra=None) def __init__(self,a): self.default = a def foo(self): noop t = _C('Hi') test_eq(t.lower(), 'hi') test_eq(t.upper(), 'HI') assert 'lower' in dir(t) assert 'upper' in dir(t) ``` However, you can choose which attributes or methods to retain by defining a class attribute `_xtra`, which is a list of allowed attribute and method names to delegate. In the below example, we only delegate the `lower` method from the composed `str` object when defining class `_C`: ``` python class _C(GetAttr): _xtra = ['lower'] # specify which attributes get passed to `self.default` def __init__(self,a): self.default = a def foo(self): noop t = _C('Hi') test_eq(t.default, 'Hi') test_eq(t.lower(), 'hi') test_fail(lambda: t.upper()) # upper wasn't in _xtra, so it isn't available to be called assert 'lower' in dir(t) assert 'upper' not in dir(t) ``` You must be careful to properly set an instance attribute in `__init__` that corresponds to the class attribute `_default`. The below example sets the class attribute `_default` to `data`, but erroneously fails to define `self.data` (and instead defines `self.default`). Failing to properly set instance attributes leads to errors when you try to access methods directly: ``` python class _C(GetAttr): _default = 'data' # use a bad component name; i.e. self.data does not exist def __init__(self,a): self.default = a def foo(self): noop # TODO: should we raise an error when we create a new instance ... t = _C('Hi') test_eq(t.default, 'Hi') # ... or is it enough for all GetAttr features to raise errors test_fail(lambda: t.data) test_fail(lambda: t.lower()) test_fail(lambda: t.upper()) test_fail(lambda: dir(t)) ``` ------------------------------------------------------------------------ source ### delegate_attr ``` python def delegate_attr( k, to ): ``` *Use in [`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) to delegate to attr `to` without inheriting from [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr)* [`delegate_attr`](https://fastcore.fast.ai/basics.html#delegate_attr) is a functional way to delegate attributes, and is an alternative to [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr). We recommend reading the documentation of [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) for more details around delegation. You can use achieve delegation when you define [`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) by using [`delegate_attr`](https://fastcore.fast.ai/basics.html#delegate_attr): ``` python class _C: def __init__(self, o): self.o = o # self.o corresponds to the `to` argument in delegate_attr. def __getattr__(self, k): return delegate_attr(self, k, to='o') t = _C('HELLO') # delegates to a string test_eq(t.lower(), 'hello') t = _C(np.array([5,4,3])) # delegates to a numpy array test_eq(t.sum(), 12) t = _C(pd.DataFrame({'a': [1,2], 'b': [3,4]})) # delegates to a pandas.DataFrame test_eq(t.b.max(), 4) ``` ## Extensible Types [`ShowPrint`](https://fastcore.fast.ai/basics.html#showprint) is a base class that defines a `show` method, which is used primarily for callbacks in fastai that expect this method to be defined. [`Int`](https://fastcore.fast.ai/basics.html#int), [`Float`](https://fastcore.fast.ai/basics.html#float), and [`Str`](https://fastcore.fast.ai/basics.html#str) extend `int`, `float` and `str` respectively by adding an additional `show` method by inheriting from [`ShowPrint`](https://fastcore.fast.ai/basics.html#showprint). The code for [`Int`](https://fastcore.fast.ai/basics.html#int) is shown below: Examples: ``` python Int(0).show() Float(2.0).show() Str('Hello').show() ``` 0 2.0 Hello ## Collection functions Functions that manipulate popular python collections. ------------------------------------------------------------------------ source ### partition ``` python def partition( coll, f ): ``` *Partition a collection by a predicate* ``` python ts,fs = partition(range(10), mod(2)) test_eq(fs, [0,2,4,6,8]) test_eq(ts, [1,3,5,7,9]) ``` ------------------------------------------------------------------------ source ### partition_dict ``` python def partition_dict( d, f ): ``` *Partition a dict by a predicate that takes key/value params* ``` python d = {'a':1, 'b':2, 'c':3, 'd':4} ts,fs = partition_dict(d, lambda k,v: v%2) test_eq(fs, {'b':2, 'd':4}) test_eq(ts, {'a':1, 'c':3}) ``` ``` python ts,fs = partition_dict(d, lambda k,v: k in 'bc') test_eq(ts, {'b':2, 'c':3}) test_eq(fs, {'a':1, 'd':4}) ``` ------------------------------------------------------------------------ source ### flatten ``` python def flatten( o ): ``` *Concatenate all collections and items as a generator* ------------------------------------------------------------------------ source ### concat ``` python def concat( colls )->list: ``` *Concatenate all collections and items as a list* ``` python concat([(o for o in range(2)),[2,3,4], 5]) ``` [0, 1, 2, 3, 4, 5] ``` python concat([["abc", "xyz"], ["foo", "bar"]]) ``` ['abc', 'xyz', 'foo', 'bar'] ------------------------------------------------------------------------ source ### strcat ``` python def strcat( its, sep:str='' )->str: ``` *Concatenate stringified items `its`* ``` python test_eq(strcat(['a',2]), 'a2') test_eq(strcat(['a',2], ';'), 'a;2') ``` ------------------------------------------------------------------------ source ### detuplify ``` python def detuplify( x ): ``` *If `x` is a tuple with one thing, extract it* ``` python test_eq(detuplify(()),None) test_eq(detuplify([1]),1) test_eq(detuplify([1,2]), [1,2]) test_eq(detuplify(np.array([[1,2]])), np.array([[1,2]])) ``` ------------------------------------------------------------------------ source ### replicate ``` python def replicate( item, match ): ``` *Create tuple of `item` copied `len(match)` times* ``` python t = [1,1] test_eq(replicate([1,2], t),([1,2],[1,2])) test_eq(replicate(1, t),(1,1)) ``` ------------------------------------------------------------------------ source ### setify ``` python def setify( o ): ``` *Turn any list like-object into a set.* ``` python # test test_eq(setify(None),set()) test_eq(setify('abc'),{'abc'}) test_eq(setify([1,2,2]),{1,2}) test_eq(setify(range(0,3)),{0,1,2}) test_eq(setify({1,2}),{1,2}) ``` ------------------------------------------------------------------------ source ### merge ``` python def merge( ds:VAR_POSITIONAL ): ``` *Merge all dictionaries in `ds`* ``` python test_eq(merge(), {}) test_eq(merge(dict(a=1,b=2)), dict(a=1,b=2)) test_eq(merge(dict(a=1,b=2), dict(b=3,c=4), None), dict(a=1, b=3, c=4)) ``` ------------------------------------------------------------------------ source ### range_of ``` python def range_of( x ): ``` *All indices of collection `x` (i.e. `list(range(len(x)))`)* ``` python test_eq(range_of([1,1,1,1]), [0,1,2,3]) ``` ------------------------------------------------------------------------ source ### groupby ``` python def groupby( x, key, val:function=noop ): ``` *Like `itertools.groupby` but doesn’t need to be sorted, and isn’t lazy, plus some extensions* ``` python test_eq(groupby('aa ab bb'.split(), itemgetter(0)), {'a':['aa','ab'], 'b':['bb']}) ``` You can use an `int` as `key` or `val` (which uses `itemgetter`; passing a `str` will use `attrgetter`), eg: ``` python test_eq(groupby('aa ab bb'.split(), 0), {'a':['aa','ab'], 'b':['bb']}) ``` …and you can use a tuple as `key` or `val` (which creates a tuple from the provided keys or vals), eg: ``` python test_eq(groupby('aaa abc bba'.split(), 0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]}) ``` Here’s an example of how to *invert* a grouping, and using a `val` function: ``` python d = {0: [1, 3, 7], 2: [3], 3: [5], 4: [8], 5: [4], 7: [5]} groupby(((o,k) for k,v in d.items() for o in v), 0, 1) ``` {1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]} ------------------------------------------------------------------------ source ### last_index ``` python def last_index( x, o ): ``` *Finds the last index of occurence of `x` in `o` (returns -1 if no occurence)* ``` python test_eq(last_index(9, [1, 2, 9, 3, 4, 9, 10]), 5) test_eq(last_index(6, [1, 2, 9, 3, 4, 9, 10]), -1) ``` ------------------------------------------------------------------------ source ### filter_dict ``` python def filter_dict( d, func ): ``` *Filter a `dict` using `func`, applied to keys and values* ``` python letters = {o:chr(o) for o in range(65,73)} letters ``` {65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'} ``` python filter_dict(letters, lambda k,v: k<67 or v in 'FG') ``` {65: 'A', 66: 'B', 70: 'F', 71: 'G'} ------------------------------------------------------------------------ source ### filter_keys ``` python def filter_keys( d, func ): ``` *Filter a `dict` using `func`, applied to keys* ``` python filter_keys(letters, lt(67)) ``` {65: 'A', 66: 'B'} ------------------------------------------------------------------------ source ### filter_values ``` python def filter_values( d, func ): ``` *Filter a `dict` using `func`, applied to values* ``` python filter_values(letters, in_('FG')) ``` {70: 'F', 71: 'G'} ------------------------------------------------------------------------ source ### cycle ``` python def cycle( o ): ``` *Like `itertools.cycle` except creates list of `None`s if `o` is empty* ``` python test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2]) test_eq(itertools.islice(cycle([]),3), [None]*3) test_eq(itertools.islice(cycle(None),3), [None]*3) test_eq(itertools.islice(cycle(1),3), [1,1,1]) ``` ------------------------------------------------------------------------ source ### zip_cycle ``` python def zip_cycle( x, args:VAR_POSITIONAL ): ``` *Like `itertools.zip_longest` but [`cycle`](https://fastcore.fast.ai/basics.html#cycle)s through elements of all but first argument* ``` python test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')]) ``` ------------------------------------------------------------------------ source ### sorted_ex ``` python def sorted_ex( iterable, key:NoneType=None, reverse:bool=False, cmp:NoneType=None, kwargs:VAR_KEYWORD ): ``` *Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`; use `cmp` comparator function or `key` with `kwargs`* Attributes can be used for sorting by passing their name as a string: ``` python class TestObj: def __init__(self, x): self.x = x objs = [TestObj(i) for i in [3,1,2]] test_eq([o.x for o in sorted_ex(objs, 'x')], [1,2,3]) ``` Tuple/list items can be sorted by index position: ``` python items = [(1,'c'), (2,'b'), (3,'a')] test_eq(sorted_ex(items, 1), [(3,'a'), (2,'b'), (1,'c')]) ``` A custom key function transforms values: ``` python test_eq(sorted_ex([3,1,2], lambda x: -x), [3,2,1]) ``` You can use a comparison function (returning -1/1/0): ``` python test_eq(sorted_ex([3,1,2], cmp=lambda a,b: 1 if a>b else -1 if asource ### not\_ ``` python def not_( f ): ``` *Create new function that negates result of `f`* ``` python def f(a): return a>0 test_eq(f(1),True) test_eq(not_(f)(1),False) test_eq(not_(f)(a=-1),True) ``` ------------------------------------------------------------------------ source ### argwhere ``` python def argwhere( iterable, f, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *Like [`filter_ex`](https://fastcore.fast.ai/basics.html#filter_ex), but return indices for matching items* ------------------------------------------------------------------------ source ### filter_ex ``` python def filter_ex( iterable, f:function=noop, negate:bool=False, gen:bool=False, kwargs:VAR_KEYWORD ): ``` *Like `filter`, but passing `kwargs` to `f`, defaulting `f` to `noop`, and adding [`negate`](https://fastcore.fast.ai/basics.html#negate) and [`gen`](https://fastcore.fast.ai/basics.html#gen)* ------------------------------------------------------------------------ source ### range_of ``` python def range_of( a, b:NoneType=None, step:NoneType=None ): ``` *All indices of collection `a`, if `a` is a collection, otherwise `range`* ``` python test_eq(range_of([1,1,1,1]), [0,1,2,3]) test_eq(range_of(4), [0,1,2,3]) ``` ------------------------------------------------------------------------ source ### renumerate ``` python def renumerate( iterable, start:int=0 ): ``` *Same as `enumerate`, but returns index as 2nd element instead of 1st* ``` python test_eq(renumerate('abc'), (('a',0),('b',1),('c',2))) ``` ------------------------------------------------------------------------ source ### first ``` python def first( x, f:NoneType=None, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *First element of `x`, optionally filtered by `f`, or None if missing* ``` python test_eq(first(['a', 'b', 'c', 'd', 'e']), 'a') test_eq(first([False]), False) test_eq(first([False], noop), None) ``` ------------------------------------------------------------------------ source ### last ``` python def last( x, f:NoneType=None, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *Last element of `x`, optionally filtered by `f`, or None if missing* ``` python test_eq(last(['a', 'b', 'c', 'd', 'e']), 'e') test_eq(last([False]), False) test_eq(last([False], noop), None) ``` ------------------------------------------------------------------------ source ### only ``` python def only( o ): ``` *Return the only item of `o`, raise if `o` doesn’t have exactly one item* ------------------------------------------------------------------------ source ### nested_attr ``` python def nested_attr( o, attr, default:NoneType=None ): ``` *Same as `getattr`, but if `attr` includes a `.`, then looks inside nested objects* ``` python class CustomIndexable: def __init__(self): self.data = {'a':1,'b':'v','c':{'d':5}} def __getitem__(self, key): return self.data[key] custom_indexable = CustomIndexable() test_eq(nested_attr(custom_indexable,'a'),1) test_eq(nested_attr(custom_indexable,'c.d'),5) test_eq(nested_attr(custom_indexable,'e'),None) ``` class TestObj: def **init**(self): self.nested = {‘key’: \[1, 2, {‘inner’: ‘value’}\]} test_obj = TestObj() test_eq(nested_attr(test_obj, ‘nested.key.2.inner’),‘value’) test_eq(nested_attr(\[1, 2, 3\], ‘1’),2) ``` python b = {'a':1,'b':'v','c':{'d':5}} test_eq(nested_attr(b,'b'),'v') test_eq(nested_attr(b,'c.d'),5) ``` ``` python a = SimpleNamespace(b=(SimpleNamespace(c=1))) test_eq(nested_attr(a, 'b.c'), getattr(getattr(a, 'b'), 'c')) test_eq(nested_attr(a, 'b.d'), None) test_eq(nested_attr(b, 'a'), 1) ``` ------------------------------------------------------------------------ source ### nested_setdefault ``` python def nested_setdefault( o, attr, default ): ``` *Same as `setdefault`, but if `attr` includes a `.`, then looks inside nested objects* ------------------------------------------------------------------------ source ### nested_callable ``` python def nested_callable( o, attr ): ``` *Same as [`nested_attr`](https://fastcore.fast.ai/basics.html#nested_attr) but if not found will return `noop`* ``` python a = SimpleNamespace(b=(SimpleNamespace(c=1))) test_eq(nested_callable(a, 'b.c'), getattr(getattr(a, 'b'), 'c')) test_eq(nested_callable(a, 'b.d'), noop) ``` ------------------------------------------------------------------------ source ### nested_idx ``` python def nested_idx( coll, idxs:VAR_POSITIONAL ): ``` *Index into nested collections, dicts, etc, with `idxs`* ``` python a = {'b':[1,{'c':2}]} test_eq(nested_idx(a, 'nope'), None) test_eq(nested_idx(a, 'nope', 'nup'), None) test_eq(nested_idx(a, 'b', 3), None) test_eq(nested_idx(a), a) test_eq(nested_idx(a, 'b'), [1,{'c':2}]) test_eq(nested_idx(a, 'b', 1), {'c':2}) test_eq(nested_idx(a, 'b', 1, 'c'), 2) ``` ``` python a = SimpleNamespace(b=[1,{'c':2}]) test_eq(nested_idx(a, 'nope'), None) test_eq(nested_idx(a, 'nope', 'nup'), None) test_eq(nested_idx(a, 'b', 3), None) test_eq(nested_idx(a), a) test_eq(nested_idx(a, 'b'), [1,{'c':2}]) test_eq(nested_idx(a, 'b', 1), {'c':2}) test_eq(nested_idx(a, 'b', 1, 'c'), 2) ``` ------------------------------------------------------------------------ source ### set_nested_idx ``` python def set_nested_idx( coll, value, idxs:VAR_POSITIONAL ): ``` *Set value indexed like \`nested_idx* ``` python set_nested_idx(a, 3, 'b', 0) test_eq(nested_idx(a, 'b', 0), 3) ``` ------------------------------------------------------------------------ source ### val2idx ``` python def val2idx( x ): ``` *Dict from value to index* ``` python test_eq(val2idx([1,2,3]), {3:2,1:0,2:1}) ``` ------------------------------------------------------------------------ source ### uniqueify ``` python def uniqueify( x, sort:bool=False, bidir:bool=False, start:NoneType=None ): ``` *Unique elements in `x`, optional `sort`, optional return reverse correspondence, optional prepend with elements.* ``` python t = [1,1,0,5,0,3] test_eq(uniqueify(t),[1,0,5,3]) test_eq(uniqueify(t, sort=True),[0,1,3,5]) test_eq(uniqueify(t, start=[7,8,6]), [7,8,6,1,0,5,3]) v,o = uniqueify(t, bidir=True) test_eq(v,[1,0,5,3]) test_eq(o,{1:0, 0: 1, 5: 2, 3: 3}) v,o = uniqueify(t, sort=True, bidir=True) test_eq(v,[0,1,3,5]) test_eq(o,{0:0, 1: 1, 3: 2, 5: 3}) ``` ------------------------------------------------------------------------ source ### loop_first_last ``` python def loop_first_last( values ): ``` *Iterate and generate a tuple with a flag for first and last value.* ``` python test_eq(loop_first_last(range(3)), [(True,False,0), (False,False,1), (False,True,2)]) ``` ------------------------------------------------------------------------ source ### loop_first ``` python def loop_first( values ): ``` *Iterate and generate a tuple with a flag for first value.* ``` python test_eq(loop_first(range(3)), [(True,0), (False,1), (False,2)]) ``` ------------------------------------------------------------------------ source ### loop_last ``` python def loop_last( values ): ``` *Iterate and generate a tuple with a flag for last value.* ``` python test_eq(loop_last(range(3)), [(False,0), (False,1), (True,2)]) ``` ------------------------------------------------------------------------ source ### first_match ``` python def first_match( lst, f, default:NoneType=None ): ``` *First element of `lst` matching predicate `f`, or `default` if none* ``` python a = [0,2,4,5,6,7,10] test_eq(first_match(a, lambda o:o%2), 3) ``` ------------------------------------------------------------------------ source ### last_match ``` python def last_match( lst, f, default:NoneType=None ): ``` *Last element of `lst` matching predicate `f`, or `default` if none* ``` python test_eq(last_match(a, lambda o:o%2), 5) ``` ## fastuple A tuple with extended functionality. ------------------------------------------------------------------------ source #### fastuple ``` python def fastuple( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *A `tuple` with elementwise ops and more friendly **init** behavior* #### Friendly init behavior Common failure modes when trying to initialize a tuple in python: ``` py tuple(3) > TypeError: 'int' object is not iterable ``` or ``` py tuple(3, 4) > TypeError: tuple expected at most 1 arguments, got 2 ``` However, [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple) allows you to define tuples like this and in the usual way: ``` python test_eq(fastuple(3), (3,)) test_eq(fastuple(3,4), (3, 4)) test_eq(fastuple((3,4)), (3, 4)) ``` #### Elementwise operations ------------------------------------------------------------------------ source ##### fastuple.add ``` python def add( args:VAR_POSITIONAL ): ``` *`+` is already defined in `tuple` for concat, so use `add` instead* ``` python test_eq(fastuple.add((1,1),(2,2)), (3,3)) test_eq_type(fastuple(1,1).add(2), fastuple(3,3)) test_eq(fastuple('1','2').add('2'), fastuple('12','22')) ``` ------------------------------------------------------------------------ source ##### fastuple.mul ``` python def mul( args:VAR_POSITIONAL ): ``` *`*` is already defined in `tuple` for replicating, so use `mul` instead* ``` python test_eq_type(fastuple(1,1).mul(2), fastuple(2,2)) ``` #### Other Elementwise Operations Additionally, the following elementwise operations are available: - `le`: less than or equal - `eq`: equal - `gt`: greater than - `min`: minimum of ``` python test_eq(fastuple(3,1).le(1), (False, True)) test_eq(fastuple(3,1).eq(1), (False, True)) test_eq(fastuple(3,1).gt(1), (True, False)) test_eq(fastuple(3,1).min(2), (2,1)) ``` You can also do other elementwise operations like negate a [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple), or subtract two [`fastuple`](https://fastcore.fast.ai/basics.html#fastuple)s: ``` python test_eq(-fastuple(1,2), (-1,-2)) test_eq(~fastuple(1,0,1), (False,True,False)) test_eq(fastuple(1,1)-fastuple(2,2), (-1,-1)) ``` ``` python test_eq(type(fastuple(1)), fastuple) test_eq_type(fastuple(1,2), fastuple(1,2)) test_ne(fastuple(1,2), fastuple(1,3)) test_eq(fastuple(), ()) ``` ## Functions on Functions Utilities for functional programming or for defining, modifying, or debugging functions. ------------------------------------------------------------------------ source ### bind ``` python def bind( func, pargs:VAR_POSITIONAL, pkwargs:VAR_KEYWORD ): ``` *Same as `partial`, except you can use `arg0` `arg1` etc param placeholders* [`bind`](https://fastcore.fast.ai/basics.html#bind) is the same as `partial`, but also allows you to reorder positional arguments using variable name(s) `arg{i}` where i refers to the zero-indexed positional argument. [`bind`](https://fastcore.fast.ai/basics.html#bind) as implemented currently only supports reordering of up to the first 5 positional arguments. Consider the function `myfunc` below, which has 3 positional arguments. These arguments can be referenced as `arg0`, `arg1`, and `arg1`, respectively. ``` python def myfn(a,b,c,d=1,e=2): return(a,b,c,d,e) ``` In the below example we bind the positional arguments of `myfn` as follows: - The second input `14`, referenced by `arg1`, is substituted for the first positional argument. - We supply a default value of `17` for the second positional argument. - The first input `19`, referenced by `arg0`, is subsituted for the third positional argument. ``` python test_eq(bind(myfn, arg1, 17, arg0, e=3)(19,14), (14,17,19,1,3)) ``` In this next example: - We set the default value to `17` for the first positional argument. - The first input `19` refrenced by `arg0`, becomes the second positional argument. - The second input `14` becomes the third positional argument. - We override the default the value for named argument `e` to `3`. ``` python test_eq(bind(myfn, 17, arg0, e=3)(19,14), (17,19,14,1,3)) ``` This is an example of using [`bind`](https://fastcore.fast.ai/basics.html#bind) like `partial` and do not reorder any arguments: ``` python test_eq(bind(myfn)(17,19,14), (17,19,14,1,2)) ``` [`bind`](https://fastcore.fast.ai/basics.html#bind) can also be used to change default values. In the below example, we use the first input `3` to override the default value of the named argument `e`, and supply default values for the first three positional arguments: ``` python test_eq(bind(myfn, 17,19,14,e=arg0)(3), (17,19,14,1,3)) ``` ------------------------------------------------------------------------ source ### mapt ``` python def mapt( func, iterables:VAR_POSITIONAL ): ``` *Tuplified `map`* ``` python t = [0,1,2,3] test_eq(mapt(operator.neg, t), (0,-1,-2,-3)) ``` ------------------------------------------------------------------------ source ### map_ex ``` python def map_ex( iterable, f, args:VAR_POSITIONAL, gen:bool=False, kwargs:VAR_KEYWORD ): ``` *Like `map`, but use [`bind`](https://fastcore.fast.ai/basics.html#bind), and supports `str` and indexing* ``` python test_eq(map_ex(t,operator.neg), [0,-1,-2,-3]) ``` If `f` is a string then it is treated as a format string to create the mapping: ``` python test_eq(map_ex(t, '#{}#'), ['#0#','#1#','#2#','#3#']) ``` If `f` is a dictionary (or anything supporting `__getitem__`) then it is indexed to create the mapping: ``` python test_eq(map_ex(t, list('abcd')), list('abcd')) ``` You can also pass the same `arg` params that [`bind`](https://fastcore.fast.ai/basics.html#bind) accepts: ``` python def f(a=None,b=None): return b test_eq(map_ex(t, f, b=arg0), range(4)) ``` ------------------------------------------------------------------------ source ### compose ``` python def compose( funcs:VAR_POSITIONAL, order:NoneType=None ): ``` *Create a function that composes all functions in `funcs`, passing along remaining `*args` and `**kwargs` to all* ``` python f1 = lambda o,p=0: (o*2)+p f2 = lambda o,p=1: (o+1)/p test_eq(f2(f1(3)), compose(f1,f2)(3)) test_eq(f2(f1(3,p=3),p=3), compose(f1,f2)(3,p=3)) test_eq(f2(f1(3, 3), 3), compose(f1,f2)(3, 3)) f1.order = 1 test_eq(f1(f2(3)), compose(f1,f2, order="order")(3)) ``` ------------------------------------------------------------------------ source ### maps ``` python def maps( args:VAR_POSITIONAL, retain:function=noop ): ``` *Like `map`, except funcs are composed first* ``` python test_eq(maps([1]), [1]) test_eq(maps(operator.neg, [1,2]), [-1,-2]) test_eq(maps(operator.neg, operator.neg, [1,2]), [1,2]) ``` ------------------------------------------------------------------------ source ### partialler ``` python def partialler( f, args:VAR_POSITIONAL, order:NoneType=None, kwargs:VAR_KEYWORD ): ``` *Like `functools.partial` but also copies over docstring* ``` python def _f(x,a=1): "test func" return x-a _f.order=1 f = partialler(_f, 2) test_eq(f.order, 1) test_eq(f(3), -1) f = partialler(_f, a=2, order=3) test_eq(f.__doc__, "test func") test_eq(f.order, 3) test_eq(f(3), _f(3,2)) ``` ``` python class partial0: "Like `partialler`, but args passed to callable are inserted at started, instead of at end" def __init__(self, f, *args, order=None, **kwargs): self.f,self.args,self.kwargs = f,args,kwargs self.order = ifnone(order, getattr(f,'order',None)) self.__doc__ = f.__doc__ def __call__(self, *args, **kwargs): return self.f(*args, *self.args, **kwargs, **self.kwargs) ``` ``` python f = partial0(_f, 2) test_eq(f.order, 1) test_eq(f(3), 1) # NB: different to `partialler` example ``` ------------------------------------------------------------------------ source ### instantiate ``` python def instantiate( t ): ``` *Instantiate `t` if it’s a type, otherwise do nothing* ``` python test_eq_type(instantiate(int), 0) test_eq_type(instantiate(1), 1) ``` ------------------------------------------------------------------------ source ### using_attr ``` python def using_attr( f, attr ): ``` *Construct a function which applies `f` to the argument’s attribute `attr`* ``` python t = Path('/a/b.txt') f = using_attr(str.upper, 'name') test_eq(f(t), 'B.TXT') ``` ------------------------------------------------------------------------ source ### negate ``` python def negate( f ): ``` *Returns the negation of `f`* ``` python def true(): 'Returns True' return True ``` ``` python false = negate(true) print(false.__doc__) test_eq(false(), not true()) ``` Returns `not true(...)` Original: Returns True ------------------------------------------------------------------------ source ### spread ``` python def spread( f ): ``` *Wrap `f` to accept a single iterable and spread it as positional args* [`spread`](https://fastcore.fast.ai/basics.html#spread) wraps a function so it accepts a single iterable (like a tuple or list) and unpacks it as positional arguments. It can be used to replicate `itertools.starmap`: ``` python def add(a, b): return a + b pairs = [(1,2), (3,4), (5,6)] list(map(spread(add), pairs)) # [3, 7, 11] ``` [3, 7, 11] It can also be used to create `star` versions of functions such as `filter`, which aren’t otherwise in the stdlib: ``` python def is_long_segment(x1, y1, x2, y2, maxlen=4): return ((x2-x1)**2 + (y2-y1)**2)**0.5 > maxlen segments = [(0,0,2,3), (1,1,4,5), (0,0,6,8)] list(filter(spread(is_long_segment), segments)) ``` [(1, 1, 4, 5), (0, 0, 6, 8)] ------------------------------------------------------------------------ source ### dspread ``` python def dspread( f ): ``` *Wrap `f` to accept a single dict and spread it as keyword args* [`dspread`](https://fastcore.fast.ai/basics.html#dspread) is the dictionary equivalent of [`spread`](https://fastcore.fast.ai/basics.html#spread) - it wraps a function to accept a single dictionary and unpacks it as keyword arguments. For instance: ``` python def greet(name, greeting='Hello'): return f'{greeting}, {name}!' configs = [{'name': 'Alice'}, {'name': 'Bob', 'greeting': 'Hi'}] list(map(dspread(greet), configs)) # ['Hello, Alice!', 'Hi, Bob!'] ``` ['Hello, Alice!', 'Hi, Bob!'] A more realistic example showing API request configuration. Each request dictionary may have different keys present, and [`dspread`](https://fastcore.fast.ai/basics.html#dspread) handles this naturally (missing keys use the function’s defaults): ``` python def api_request(endpoint, method='GET', timeout=30, headers=None): return f"{method} {endpoint} (timeout={timeout})" requests = [ {'endpoint': '/users', 'method': 'POST', 'timeout': 60}, {'endpoint': '/data'}, {'endpoint': '/health', 'method': 'HEAD', 'timeout': 5} ] list(map(dspread(api_request), requests)) ``` ['POST /users (timeout=60)', 'GET /data (timeout=30)', 'HEAD /health (timeout=5)'] ### Self (with an *uppercase* S) A Concise Way To Create Lambdas This is a concise way to create lambdas that are calling methods on an object (note the capitalization!) `Self.sum()`, for instance, is a shortcut for `lambda o: o.sum()`. ``` python f = Self.sum() x = np.array([3.,1]) test_eq(f(x), 4.) # This is equivalent to above f = lambda o: o.sum() x = np.array([3.,1]) test_eq(f(x), 4.) f = Self.argmin() arr = np.array([1,2,3,4,5]) test_eq(f(arr), arr.argmin()) f = Self.sum().is_integer() x = np.array([3.,1]) test_eq(f(x), True) f = Self.sum().real.is_integer() x = np.array([3.,1]) test_eq(f(x), True) f = Self.imag() test_eq(f(3), 0) f = Self[1] test_eq(f(x), 1) ``` `Self` is also callable, which creates a function which calls any function passed to it, using the arguments passed to `Self`: ``` python def f(a, b=3): return a+b+2 def g(a, b=3): return a*b fg = Self(1,b=2) list(map(fg, [f,g])) ``` [5, 2] ## Patching ------------------------------------------------------------------------ source ### copy_func ``` python def copy_func( f ): ``` *Copy a non-builtin function (NB `copy.copy` does not work for this)* Sometimes it may be desirable to make a copy of a function that doesn’t point to the original object. When you use Python’s built in `copy.copy` or `copy.deepcopy` to copy a function, you get a reference to the original object: ``` python import copy as cp ``` ``` python def foo(): pass a = cp.copy(foo) b = cp.deepcopy(foo) a.someattr = 'hello' # since a and b point at the same object, updating a will update b test_eq(b.someattr, 'hello') assert a is foo and b is foo ``` However, with [`copy_func`](https://fastcore.fast.ai/basics.html#copy_func), you can retrieve a copy of a function without a reference to the original object: ``` python c = copy_func(foo) # c is an indpendent object assert c is not foo ``` ``` python def g(x, *, y=3): return x+y test_eq(copy_func(g)(4), 7) ``` ------------------------------------------------------------------------ source ### patch_to ``` python def patch_to( cls, as_prop:bool=False, cls_method:bool=False, set_prop:bool=False, nm:NoneType=None, glb:NoneType=None ): ``` *Decorator: add `f` to `cls`* The `@patch_to` decorator allows you to [monkey patch](https://stackoverflow.com/questions/5626193/what-is-monkey-patching) a function into a class as a method: ``` python class _T3(int): pass @patch_to(_T3) def func1(self, a): return self+a t = _T3(1) # we initialized `t` to a type int = 1 test_eq(t.func1(2), 3) # we add 2 to `t`, so 2 + 1 = 3 ``` You can access instance properties in the usual way via `self`: ``` python class _T4(): def __init__(self, g): self.g = g @patch_to(_T4) def greet(self, x): return self.g + x t = _T4('hello ') # this sets self.g = 'hello ' test_eq(t.greet('world'), 'hello world') #t.greet('world') will append 'world' to 'hello ' ``` You can instead specify that the method should be a class method by setting `cls_method=True`: ``` python class _T5(int): attr = 3 # attr is a class attribute we will access in a later method @patch_to(_T5, cls_method=True) def func(cls, x): return cls.attr + x # you can access class attributes in the normal way test_eq(_T5.func(4), 7) ``` Additionally you can specify that the function you want to patch should be a class attribute with `as_prop=True`: ``` python @patch_to(_T5, as_prop=True) def add_ten(self): return self + 10 t = _T5(4) test_eq(t.add_ten, 14) ``` Once you have a property, you can assign a setter with `set_prop=True`: ``` python class _T2(): def __init__(self, val): self._val = val @patch_to(_T2, as_prop=True) def val(self): return self._val t = _T2(2) test_eq(t.val, 2) @patch_to(_T2, set_prop=True) def val(self, val): self._val = val t.val = 3 test_eq(t.val, 3) ``` Instead of passing one class to the `@patch_to` decorator, you can pass multiple classes in a tuple to simulteanously patch more than one class with the same method: ``` python class _T6(int): pass class _T7(int): pass @patch_to((_T6,_T7)) def func_mult(self, a): return self*a t = _T6(2) test_eq(t.func_mult(4), 8) t = _T7(2) test_eq(t.func_mult(4), 8) ``` You can also rename the function in the patched class: ``` python class _T8(int): pass @patch_to(_T8, nm='add_value') def func2(self, a): return self+a t = _T8(1) test_eq(t.add_value(2), 3) test_eq(_T8.add_value.__name__, 'add_value') assert not hasattr(t, 'func2') ``` ------------------------------------------------------------------------ source ### patch ``` python def patch( f:NoneType=None, as_prop:bool=False, cls_method:bool=False, set_prop:bool=False, nm:NoneType=None ): ``` *Decorator: add `f` to the first parameter’s class (based on f’s type annotations)* `@patch` is an alternative to `@patch_to` that allows you similarly monkey patch class(es) by using [type annotations](https://docs.python.org/3/library/typing.html): ``` python class _T8(int): pass @patch def func(self:_T8, a): return self+a t = _T8(1) # we initilized `t` to a type int = 1 test_eq(t.func(3), 4) # we add 3 to `t`, so 3 + 1 = 4 test_eq(t.func.__qualname__, '_T8.func') ``` Similarly to [`patch_to`](https://fastcore.fast.ai/basics.html#patch_to), you can supply a union of classes instead of a single class in your type annotations to patch multiple classes: ``` python class _T9(int): pass @patch def func2(x:_T8|_T9, a): return x*a # will patch both _T8 and _T9 t = _T8(2) test_eq(t.func2(4), 8) test_eq(t.func2.__qualname__, '_T8.func2') t = _T9(2) test_eq(t.func2(4), 8) test_eq(t.func2.__qualname__, '_T9.func2') ``` Just like [`patch_to`](https://fastcore.fast.ai/basics.html#patch_to) decorator you can use `as_prop`, `set_prop`, and `cls_method` parameters with [`patch`](https://fastcore.fast.ai/basics.html#patch) decorator: ``` python @patch(as_prop=True) def add_ten(self:_T5): return self + 10 t = _T5(4) test_eq(t.add_ten, 14) ``` ``` python class _T2(): def __init__(self, val): self._val = val @patch(as_prop=True) def val(self:_T2): return self._val t = _T2(2) test_eq(t.val, 2) @patch(set_prop=True) def val(self:_T2, val): self._val = val t.val = 3 test_eq(t.val, 3) ``` ``` python class _T5(int): attr = 3 # attr is a class attribute we will access in a later method @patch(cls_method=True) def func(cls:_T5, x): return cls.attr + x # you can access class attributes in the normal way test_eq(_T5.func(4), 7) ``` ``` python class _T8(int): pass @patch(nm='add_value') def func2(self:_T8, a): return self+a t = _T8(1) test_eq(t.add_value(2), 3) test_eq(_T8.add_value.__name__, 'add_value') assert not hasattr(t, 'func2') ``` Patching `classmethod` shouldn’t affect how python’s inheritance works ``` python class FastParent: pass @patch(cls_method=True) def type_cls(cls: FastParent): return cls class FastChild(FastParent): pass parent = FastParent() test_eq(parent.type_cls(), FastParent) child = FastChild() test_eq(child.type_cls(), FastChild) ``` ## Other Helpers ------------------------------------------------------------------------ source ### compile_re ``` python def compile_re( pat ): ``` *Compile `pat` if it’s not None* ``` python assert compile_re(None) is None assert compile_re('a').match('ab') ``` ------------------------------------------------------------------------ source #### ImportEnum ``` python def ImportEnum( args:VAR_POSITIONAL, kwds:VAR_KEYWORD ): ``` *An `Enum` that can have its values imported* ``` python _T = ImportEnum('_T', {'foobar':1, 'goobar':2}) _T.imports() test_eq(foobar, _T.foobar) test_eq(goobar, _T.goobar) ``` ------------------------------------------------------------------------ source #### StrEnum ``` python def StrEnum( args:VAR_POSITIONAL, kwds:VAR_KEYWORD ): ``` *An [`ImportEnum`](https://fastcore.fast.ai/basics.html#importenum) that behaves like a `str`* ------------------------------------------------------------------------ source ### str_enum ``` python def str_enum( name, vals:VAR_POSITIONAL ): ``` *Simplified creation of [`StrEnum`](https://fastcore.fast.ai/basics.html#strenum) types* ------------------------------------------------------------------------ source #### ValEnum ``` python def ValEnum( args:VAR_POSITIONAL, kwds:VAR_KEYWORD ): ``` *An [`ImportEnum`](https://fastcore.fast.ai/basics.html#importenum) that stringifies using values* ``` python _T = str_enum('_T', 'a', 'b') test_eq(f'{_T.a}', 'a') test_eq(_T.a, 'a') test_eq(list(_T.__members__), ['a','b']) print(_T.a, _T.a.upper()) ``` a A ------------------------------------------------------------------------ source #### Stateful ``` python def Stateful( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *A base class/mixin for objects that should not serialize all their state* ``` python class _T(Stateful): def __init__(self): super().__init__() self.a=1 self._state['test']=2 t = _T() t2 = pickle.loads(pickle.dumps(t)) test_eq(t.a,1) test_eq(t._state['test'],2) test_eq(t2.a,1) test_eq(t2._state,{}) ``` Override `_init_state` to do any necessary setup steps that are required during `__init__` or during deserialization (e.g. `pickle.load`). Here’s an example of how [`Stateful`](https://fastcore.fast.ai/basics.html#stateful) simplifies the official Python example for [Handling Stateful Objects](https://docs.python.org/3/library/pickle.html#handling-stateful-objects). ``` python class TextReader(Stateful): """Print and number lines in a text file.""" _stateattrs=('file',) def __init__(self, filename): self.filename,self.lineno = filename,0 super().__init__() def readline(self): self.lineno += 1 line = self.file.readline() if line: return f"{self.lineno}: {line.strip()}" def _init_state(self): self.file = open(self.filename) for _ in range(self.lineno): self.file.readline() ``` ``` python reader = TextReader("00_test.ipynb") print(reader.readline()) print(reader.readline()) new_reader = pickle.loads(pickle.dumps(reader)) print(reader.readline()) ``` 1: { 2: "cells": [ 3: { ------------------------------------------------------------------------ source ### NotStr ``` python def NotStr( s ): ``` *Behaves like a `str`, but isn’t an instance of one* ``` python s = NotStr("hello") assert not isinstance(s, str) test_eq(s, 'hello') test_eq(s*2, 'hellohello') test_eq(len(s), 5) test_eq(s[:2], "he") test_eq(s[2], "l") ``` ------------------------------------------------------------------------ source #### PrettyString ``` python def PrettyString( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Little hack to get strings to show properly in Jupyter.* Allow strings with special characters to render properly in Jupyter. Without calling `print()` strings with special characters are displayed like so: ``` python with_special_chars='a string\nwith\nnew\nlines and\ttabs' with_special_chars ``` 'a string\nwith\nnew\nlines and\ttabs' We can correct this with [`PrettyString`](https://fastcore.fast.ai/basics.html#prettystring): ``` python PrettyString(with_special_chars) ``` a string with new lines and tabs ------------------------------------------------------------------------ source ### even_mults ``` python def even_mults( start, stop, n ): ``` *Build log-stepped array from `start` to [`stop`](https://fastcore.fast.ai/basics.html#stop) in `n` steps.* ``` python test_eq(even_mults(2,8,3), [2,4,8]) test_eq(even_mults(2,32,5), [2,4,8,16,32]) test_eq(even_mults(2,8,1), 8) ``` ------------------------------------------------------------------------ source ### num_cpus ``` python def num_cpus( ): ``` *Get number of cpus* ``` python num_cpus() ``` 16 ------------------------------------------------------------------------ source ### add_props ``` python def add_props( f, g:NoneType=None, n:int=2 ): ``` *Create properties passing each of `range(n)` to f* ``` python class _T(): a,b = add_props(lambda i,x:i*2) t = _T() test_eq(t.a,0) test_eq(t.b,2) ``` ``` python class _T(): def __init__(self, v): self.v=v def _set(i, self, v): self.v[i] = v a,b = add_props(lambda i,x: x.v[i], _set) t = _T([0,2]) test_eq(t.a,0) test_eq(t.b,2) t.a = t.a+1 t.b = 3 test_eq(t.a,1) test_eq(t.b,3) ``` ------------------------------------------------------------------------ source ### str2bool ``` python def str2bool( s ): ``` *Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,[`true`](https://fastcore.fast.ai/basics.html#true),`on`,`1`-\>`True`)* True values are ‘y’, ‘yes’, ‘t’, ‘true’, ‘on’, and ‘1’; false values are ‘n’, ‘no’, ‘f’, ‘false’, ‘off’, and ‘0’. Raises `ValueError` if ‘val’ is anything else. ``` python for o in "y YES t True on 1".split(): assert str2bool(o) for o in "n no FALSE off 0".split(): assert not str2bool(o) for o in 0,None,'',False: assert not str2bool(o) for o in 1,True: assert str2bool(o) ``` ------------------------------------------------------------------------ source ### str2int ``` python def str2int( s )->int: ``` *Convert `s` to an `int`* ------------------------------------------------------------------------ source ### str2float ``` python def str2float( s:str ): ``` *Convert `s` to a float* ------------------------------------------------------------------------ source ### str2list ``` python def str2list( s:str ): ``` *Convert `s` to a list* ------------------------------------------------------------------------ source ### str2date ``` python def str2date( s:str )->date: ``` *`date.fromisoformat` with empty string handling* ------------------------------------------------------------------------ source ### to_date ``` python def to_date( arg ): ``` ------------------------------------------------------------------------ source ### to_list ``` python def to_list( arg ): ``` ------------------------------------------------------------------------ source ### to_float ``` python def to_float( arg ): ``` ------------------------------------------------------------------------ source ### to_int ``` python def to_int( arg ): ``` ------------------------------------------------------------------------ source ### to_bool ``` python def to_bool( arg ): ``` ------------------------------------------------------------------------ source ### typed ``` python def typed( _func:NoneType=None, cast:bool=False ): ``` *Decorator to check param and return types at runtime, with optional casting* [`typed`](https://fastcore.fast.ai/basics.html#typed) validates argument types at **runtime**. This is in contrast to [MyPy](http://mypy-lang.org/) which only offers static type checking. For example, a `TypeError` will be raised if we try to pass an integer into the first argument of the below function: ``` python @typed def discount(price:int, pct:float) -> float: return (1-pct) * price with ExceptionExpected(TypeError): discount(100.0, .1) ``` You can have automatic casting based on heuristics by specifying `typed(cast=True)`. If casting is not possible, a `TypeError` is raised. ``` python @typed(cast=True) def discount(price:int, pct:float) -> float: return (1-pct) * price assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100 assert 90.0 == discount(' 100 ', .1) # will auto cast the str "100" to the int 100 with ExceptionExpected(TypeError): discount("a", .1) ``` We can also optionally allow multiple types by enumarating the types in a tuple as illustrated below: ``` python @typed def discount(price:int|float, pct:float): return (1-pct) * price assert 90.0 == discount(100.0, .1) @typed(cast=True) def discount(price:int|None, pct:float): return (1-pct) * price assert 90.0 == discount(100.0, .1) ``` We currently do not support union types when casting. ``` python @typed(cast=True) def discount(price:int|float, pct:float): return (1-pct) * price with ExceptionExpected(AssertionError): assert 90.0 == discount("100.0", .1) ``` [`typed`](https://fastcore.fast.ai/basics.html#typed) works with classes, too: ``` python class Foo: @typed def __init__(self, a:int, b: int, c:str): pass @typed(cast=True) def test(cls, d:str): return d with ExceptionExpected(TypeError): Foo(1, 2, 3) assert isinstance(Foo(1,2, 'a string').test(10), str) ``` It also works with custom types. ``` python @typed def test_foo(foo: Foo): pass with ExceptionExpected(TypeError): test_foo(1) test_foo(Foo(1, 2, 'a string')) ``` ``` python class Bar: @typed def __init__(self, a:int): self.a = a @typed(cast=True) def test_bar(bar: Bar): return bar assert isinstance(test_bar(1), Bar) test_eq(test_bar(1).a, 1) with ExceptionExpected(TypeError): test_bar("foobar") ``` ------------------------------------------------------------------------ source ### exec_new ``` python def exec_new( code ): ``` *Execute `code` in a new environment and return it* ``` python g = exec_new('a=1') test_eq(g['a'], 1) ``` ------------------------------------------------------------------------ source ### exec_import ``` python def exec_import( mod, sym ): ``` *Import `sym` from `mod` in a new environment* ------------------------------------------------------------------------ source ### sig_with_params ``` python def sig_with_params( sig, remove:NoneType=None, keep:NoneType=None, updates:VAR_KEYWORD ): ``` [`sig_with_params`](https://fastcore.fast.ai/basics.html#sig_with_params) lets you modify a function signature by adding, replacing, or removing parameters. This is useful when creating wrapper functions or decorators that need to adjust the signature of the wrapped function. You can remove parameters by name: ``` python from inspect import signature, Parameter ``` ``` python def foo(a, b, c=3): pass sig = signature(foo) new_sig = sig_with_params(sig, remove=['b']) test_eq(list(new_sig.parameters.keys()), ['a', 'c']) ``` You can also add new parameters: ``` python new_param = Parameter('d', Parameter.KEYWORD_ONLY, default=4) new_sig = sig_with_params(sig, d=new_param) test_eq(list(new_sig.parameters.keys()), ['a', 'b', 'c', 'd']) ``` ------------------------------------------------------------------------ source ### fdelegates ``` python def fdelegates( to ): ``` This is a simplified version of [`fastcore.meta.delegates`](https://fastcore.fast.ai/meta.html#delegates) that supports only regular functions. ## Notebook functions ------------------------------------------------------------------------ ### ipython_shell ``` python def ipython_shell( ): ``` *Same as `get_ipython` but returns `False` if not in IPython* ------------------------------------------------------------------------ ### in_ipython ``` python def in_ipython( ): ``` *Check if code is running in some kind of IPython environment* ------------------------------------------------------------------------ ### in_colab ``` python def in_colab( ): ``` *Check if the code is running in Google Colaboratory* ------------------------------------------------------------------------ ### in_jupyter ``` python def in_jupyter( ): ``` *Check if the code is running in a jupyter notebook* ------------------------------------------------------------------------ ### in_notebook ``` python def in_notebook( ): ``` *Check if the code is running in a jupyter notebook* These variables are available as booleans in `fastcore.basics` as `IN_IPYTHON`, `IN_JUPYTER`, `IN_COLAB` and `IN_NOTEBOOK`. ``` python IN_IPYTHON, IN_JUPYTER, IN_COLAB, IN_NOTEBOOK ``` (True, True, False, True) --- # Source: https://fastcore.fast.ai/docments.html.md # Docments [`docments`](https://fastcore.fast.ai/docments.html#docments) provides programmatic access to comments in function parameters and return types. It can be used to create more developer-friendly documentation, CLI, etc tools. ## Why? Without docments, if you want to document your parameters, you have to repeat param names in docstrings, since they’re already in the function signature. The parameters have to be kept synchronized in the two places as you change your code. Readers of your code have to look back and forth between two places to understand what’s happening. So it’s more work for you, and for your users. Furthermore, to have parameter documentation formatted nicely without docments, you have to use special magic docstring formatting, often with [odd quirks](https://stackoverflow.com/questions/62167540/why-do-definitions-have-a-space-before-the-colon-in-numpy-docstring-sections), which is a pain to create and maintain, and awkward to read in code. For instance, using [numpy-style documentation](https://numpydoc.readthedocs.io/en/latest/format.html): ``` python def add_np(a:int, b:int=0)->int: """The sum of two numbers. Used to demonstrate numpy-style docstrings. Parameters ---------- a : int the 1st number to add b : int the 2nd number to add (default: 0) Returns ------- int the result of adding `a` to `b`""" return a+b ``` By comparison, here’s the same thing using docments: ``` python def add( a:int, # the 1st number to add b=0, # the 2nd number to add )->int: # the result of adding `a` to `b` "The sum of two numbers." return a+b ``` ## Numpy docstring helper functions [`docments`](https://fastcore.fast.ai/docments.html#docments) also supports numpy-style docstrings, or a mix or numpy-style and docments parameter documentation. The functions in this section help get and parse this information. ------------------------------------------------------------------------ source ### docstring ``` python def docstring( sym ): ``` *Get docstring for `sym` for functions ad classes* ``` python test_eq(docstring(add), "The sum of two numbers.") ``` ------------------------------------------------------------------------ source ### parse_docstring ``` python def parse_docstring( sym ): ``` *Parse a numpy-style docstring in `sym`* ``` python # parse_docstring(add_np) ``` ------------------------------------------------------------------------ source ### isdataclass ``` python def isdataclass( s ): ``` *Check if `s` is a dataclass but not a dataclass’ instance* ------------------------------------------------------------------------ source ### get_dataclass_source ``` python def get_dataclass_source( s ): ``` *Get source code for dataclass `s`* ------------------------------------------------------------------------ source ### get_source ``` python def get_source( s ): ``` *Get source code for string, function object or dataclass `s`* ``` python parms = _param_locs(add) parms ``` {2: 'a', 3: 'b', 4: 'return'} ``` python _get_comment(2, 'a', {2: ' the 1st number to add'}, parms) ``` 'the 1st number to add' ------------------------------------------------------------------------ source ### get_name ``` python def get_name( obj ): ``` *Get the name of `obj`* ``` python test_eq(get_name(in_ipython), 'in_ipython') test_eq(get_name(L.map), 'map') ``` ------------------------------------------------------------------------ source ### qual_name ``` python def qual_name( obj ): ``` *Get the qualified name of `obj`* ``` python assert qual_name(docscrape) == 'fastcore.docscrape' ``` ## Docments Let’s manually go through each step of `_docments` to see what it does: ``` python def _b( z:str='b', # Last ): return b, a @delegates(_b) def _c( b:str, # Ignore a:int=2 ): return b, a # Third @delegates(_c) def _d( c:int, # First b:str, # Second **kwargs )->int: # Return an int return c, _c(b, **kwargs) ``` ``` python s = _d nps = parse_docstring(s) if isclass(s) and not is_dataclass(s): s = s.__init__ comments = {o.start[0]:_clean_comment(o.string) for o in _tokens(s) if o.type==COMMENT} comments ``` {3: ' First', 4: ' Second', 6: ' Return an int'} ``` python parms = _param_locs(s, returns=True, args_kwargs=True) or {} parms ``` {3: 'c', 4: 'b', 5: 'kwargs', 6: 'return'} ``` python docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()} docs ``` {'c': 'First', 'b': 'Second', 'kwargs': None, 'return': 'Return an int'} ``` python sig = signature(s, eval_str=True) res = {name:_get_full(p, docs) for name,p in sig.parameters.items()} res ``` {'c': {'docment': 'First', 'anno': int, 'default': inspect._empty}, 'b': {'docment': 'Second', 'anno': str, 'default': inspect._empty}, 'a': {'docment': None, 'anno': int, 'default': 2}, 'z': {'docment': None, 'anno': str, 'default': 'b'}} ``` python res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty) res = _merge_docs(res, nps) res ``` {'c': {'docment': 'First', 'anno': int, 'default': inspect._empty}, 'b': {'docment': 'Second', 'anno': str, 'default': inspect._empty}, 'a': {'docment': None, 'anno': int, 'default': 2}, 'z': {'docment': None, 'anno': str, 'default': 'b'}, 'return': {'docment': 'Return an int', 'anno': int, 'default': inspect._empty}} ``` python _d.__delwrap__ ``` ------------------------------------------------------------------------ source ### docments ``` python def docments( s, full:bool=False, eval_str:bool=False, returns:bool=True, args_kwargs:bool=False ): ``` *Get docments for `s`* ``` python docments(_d) ``` ``` python {'a': None, 'b': 'Second', 'c': 'First', 'return': 'Return an int', 'z': 'Last'} ``` ``` python docments(_d, full=True) ``` ``` python { 'a': {'anno': , 'default': 2, 'docment': None}, 'b': { 'anno': , 'default': , 'docment': 'Second'}, 'c': { 'anno': , 'default': , 'docment': 'First'}, 'return': { 'anno': , 'default': , 'docment': 'Return an int'}, 'z': {'anno': , 'default': 'b', 'docment': 'Last'}} ``` The returned `dict` has parameter names as keys, docments as values. The return value comment appears in the `return`, unless `returns=False`. Using the `add` definition above, we get: ``` python def add( a:int, # the 1st number to add b=0, # the 2nd number to add )->int: # the result of adding `a` to `b` "The sum of two numbers." return a+b ``` ``` python docments(add) ``` ``` python { 'a': 'the 1st number to add', 'b': 'the 2nd number to add', 'return': 'the result of adding `a` to `b`'} ``` `args_kwargs=True` adds args and kwargs docs too: ``` python def add( a:int, # the 1st number to add *args, # some args b=0, # the 2nd number to add **kwargs, # Passed to the `example` function )->int: # the result of adding `a` to `b` "The sum of two numbers." return a+b ``` ``` python docments(add, args_kwargs=True) ``` ``` python { 'a': 'the 1st number to add', 'args': 'some args', 'b': 'the 2nd number to add', 'kwargs': 'Passed to the `example` function', 'return': 'the result of adding `a` to `b`'} ``` If you pass `full=True`, the values are `dict` of defaults, types, and docments as values. Note that the type annotation is inferred from the default value, if the annotation is empty and a default is supplied. (Note that for `full`, `args_kwargs=True` is always set too.) ``` python docments(add, full=True) ``` ``` python { 'a': { 'anno': , 'default': , 'docment': 'the 1st number to add'}, 'args': { 'anno': <_ParameterKind.VAR_POSITIONAL: 2>, 'default': , 'docment': 'some args'}, 'b': { 'anno': , 'default': 0, 'docment': 'the 2nd number to add'}, 'kwargs': { 'anno': <_ParameterKind.VAR_KEYWORD: 4>, 'default': , 'docment': None}, 'return': { 'anno': , 'default': , 'docment': 'the result of adding `a` to `b`'}} ``` To evaluate stringified annotations (from python 3.10), use `eval_str`: ``` python docments(add, full=True, eval_str=True)['a'] ``` ``` python { 'anno': , 'default': , 'docment': 'the 1st number to add'} ``` ``` python docments(add, full=True)['a'] ``` ``` python { 'anno': , 'default': , 'docment': 'the 1st number to add'} ``` If you need more space to document a parameter, place one or more lines of comments above the parameter, or above the return type. You can mix-and-match these docment styles: ``` python def add( # The first operand a:int, # This is the second of the operands to the *addition* operator. # Note that passing a negative value here is the equivalent of the *subtraction* operator. b:int, )->int: # The result is calculated using Python's builtin `+` operator. "Add `a` to `b`" return a+b ``` ``` python docments(add) ``` ``` python { 'a': 'The first operand', 'b': 'This is the second of the operands to the *addition* operator.\n' 'Note that passing a negative value here is the equivalent of the ' '*subtraction* operator.', 'return': "The result is calculated using Python's builtin `+` operator."} ``` Docments works with async functions, too: ``` python async def add_async( # The first operand a:int, # This is the second of the operands to the *addition* operator. # Note that passing a negative value here is the equivalent of the *subtraction* operator. b:int, )->int: # The result is calculated using Python's builtin `+` operator. "Add `a` to `b`" return a+b ``` ``` python test_eq(docments(add_async), docments(add)) ``` You can also use docments with classes and methods: ``` python class Adder: "An addition calculator" def __init__(self, a:int, # First operand b:int, # 2nd operand ): self.a,self.b = a,b def calculate(self )->int: # Integral result of addition operator "Add `a` to `b`" return a+b ``` ``` python docments(Adder) ``` ``` python {'a': 'First operand', 'b': '2nd operand', 'return': None, 'self': None} ``` ``` python docments(Adder.calculate) ``` ``` python {'return': 'Integral result of addition operator', 'self': None} ``` docments can also be extracted from numpy-style docstrings: ``` python print(add_np.__doc__) ``` The sum of two numbers. Used to demonstrate numpy-style docstrings. Parameters ---------- a : int the 1st number to add b : int the 2nd number to add (default: 0) Returns ------- int the result of adding `a` to `b` ``` python docments(add_np) ``` ``` python { 'a': 'the 1st number to add', 'b': 'the 2nd number to add (default: 0)', 'return': 'the result of adding `a` to `b`'} ``` You can even mix and match docments and numpy parameters: ``` python def add_mixed(a:int, # the first number to add b )->int: # the result """The sum of two numbers. Parameters ---------- b : int the 2nd number to add (default: 0)""" return a+b ``` ``` python docments(add_mixed, full=True) ``` ``` python { 'a': { 'anno': , 'default': , 'docment': 'the first number to add'}, 'b': { 'anno': , 'default': , 'docment': 'the 2nd number to add (default: 0)'}, 'return': { 'anno': , 'default': , 'docment': 'the result'}} ``` You can use docments with dataclasses, however if the class was defined in online notebook, docments will not contain parameters’ comments. This is because the source code is not available in the notebook. After converting the notebook to a module, the docments will be available. Thus, documentation will have correct parameters’ comments. Docments even works with [`delegates`](https://fastcore.fast.ai/meta.html#delegates): ``` python from fastcore.meta import delegates ``` ``` python def _a(a:str=None): return a # First @delegates(_a) def _b(b:str, # Second **kwargs ): # Return nothing return b, (_a(**kwargs)) docments(_b) ``` ``` python {'a': 'First', 'b': 'Second', 'return': None} ``` ``` python docments(_b, full=True) ``` ``` python { 'a': {'anno': , 'default': None, 'docment': 'First'}, 'b': { 'anno': , 'default': , 'docment': 'Second'}, 'return': { 'anno': , 'default': , 'docment': None}} ``` Builtins just return an empty dict: ``` python docments(str) ``` ``` python {'args': None, 'kwargs': None, 'return': None, 'self': None} ``` ## Extract docstrings ------------------------------------------------------------------------ source ### sig_source ``` python def sig_source( obj ): ``` *Full source of signature line(s) for a function or class.* ``` python print(sig_source(flexiclass)) ``` def flexiclass( cls # The class to convert ) -> dataclass: ``` python def simple(x: dict[str, int]): return x print(sig_source(simple)) ``` def simple(x: dict[str, int]): return x ``` python def multi(a, b=1, c=2, d=3): return a print(sig_source(multi)) ``` def multi(a, b=1, c=2, d=3): ------------------------------------------------------------------------ source ### extract_docstrings ``` python def extract_docstrings( code ): ``` *Create a dict from function/class/method names to tuples of docstrings and param lists* ``` python sample_code = """ "This is a module." def top_func(a, b, *args, **kw): "This is top-level." pass class SampleClass: "This is a class." def __init__(self, x, y): "Constructor for SampleClass." pass def method1(self, param1): "This is method1." pass def _private_method(self): "This should not be included." pass class AnotherClass: def __init__(self, a, b): "This class has no separate docstring." pass""" exp = {'_module': ('This is a module.', ''), 'top_func': ('This is top-level.', 'a, b, *args, **kw'), 'SampleClass': ('This is a class.', 'self, x, y'), 'SampleClass.method1': ('This is method1.', 'self, param1'), 'AnotherClass': ('This class has no separate docstring.', 'self, a, b')} test_eq(extract_docstrings(sample_code), exp) ``` ## Rendering docment Tables Render nicely formatted tables that shows [`docments`](https://fastcore.fast.ai/docments.html#docments) for any function or method. ------------------------------------------------------------------------ source ### DocmentTbl ``` python def DocmentTbl( obj, verbose:bool=True, returns:bool=True ): ``` *Compute the docment table string* [`DocmentTbl`](https://fastcore.fast.ai/docments.html#docmenttbl) can render a markdown table showing [`docments`](https://fastcore.fast.ai/docments.html#docments) if appropriate. This is an example of how a [`docments`](https://fastcore.fast.ai/docments.html#docments) table will render for a function: ``` python def _f(a, # description of param a b=True, # description of param b c:str=None ) -> int: ... _dm = DocmentTbl(_f) _dm ```
Type Default Details
a description of param a
b bool True description of param b
c str None
Returns int
If one column in the table has no information, for example because there are no default values, that column will not be shown. In the below example, the **Default** column, will not be shown. Additionally, if the return of the function is not annotated the **Returns** row will not be rendered: ``` python def _f(a, b:int, #param b c:str='foo' #param c )->str: # Result of doing it "Do a thing" ... ``` ``` python _dm2 = DocmentTbl(_f) _dm2 ```
Type Default Details
a
b int param b
c str foo param c
Returns str Result of doing it
[`DocmentTbl`](https://fastcore.fast.ai/docments.html#docmenttbl) also works on classes. By default, the `__init__` will be rendered: ``` python class _Test: def __init__(self, a, # description of param a b=True, # description of param b c:str=None): ... def foo(self, c:int, # description of param c d=True, # description of param d ): ... ``` ``` python DocmentTbl(_Test) ```
Type Default Details
a description of param a
b bool True description of param b
c str None
You can also pass a method to be rendered as well: ``` python DocmentTbl(_Test.foo) ```
Type Default Details
c int description of param c
d bool True description of param d
------------------------------------------------------------------------ source ### DocmentList ``` python def DocmentList( obj ): ``` *Initialize self. See help(type(self)) for accurate signature.* ``` python DocmentList(_f) ``` - `a` - `b:int`   *param b* - `c:str=foo`   *param c* - `return:str`   *Result of doing it* ``` python print(_fmt_sig('foo', [('a:int', 'first'), ('b:str', None), ('c', 'third')], ')->int:', 80)) ``` def foo( a:int, # first b:str, c, # third )->int: ------------------------------------------------------------------------ source ### DocmentText ``` python def DocmentText( obj, maxline:int=110, docstring:bool=True ): ``` *Initialize self. See help(type(self)) for accurate signature.* ``` python DocmentText(_f).params ``` [('a', None), ('b:int', 'param b'), ("c:str='foo'", 'param c')] ``` python DocmentText(_f) ``` ``` python def _f( a, b:int, # param b c:str='foo', # param c )->str: # Result of doing it "Do a thing" ``` ``` python def _g( a, b:int, cccccccccccccccccccc:int, ccccccccdccccccccccc:int, cccccccccccecccccccc:int, cccccccfcccccccccc:int, ccccccccccccgccccc:int, # hi c:str='foo' )->str: "Do a thing" DocmentText(_g, maxline=80, docstring=False) ``` ``` python def _g( a, b:int, cccccccccccccccccccc:int, ccccccccdccccccccccc:int, cccccccccccecccccccc:int, cccccccfcccccccccc:int, ccccccccccccgccccc:int, # hi c:str='foo' )->str: ``` ``` python DocmentText(partial(_g, 1), maxline=80, docstring=False) ``` ``` python def _g[partial: 1]( b:int, cccccccccccccccccccc:int, ccccccccdccccccccccc:int, cccccccccccecccccccc:int, cccccccfcccccccccc:int, ccccccccccccgccccc:int, c:str='foo' )->str: ``` ------------------------------------------------------------------------ source ### sig2str ``` python def sig2str( func, maxline:int=110 ): ``` *Generate function signature with docments as comments* ``` python print(sig2str(_d)) ``` def _d( c:int, # First b:str, # Second a:int=2, z:str='b', # Last )->int: # Return an int ## Documentation For An Object Render the signature as well as the [`docments`](https://fastcore.fast.ai/docments.html#docments) to show complete documentation for an object. ------------------------------------------------------------------------ source ### ShowDocRenderer ``` python def ShowDocRenderer( sym, name:str | None=None, title_level:int=3, maxline:int=110 ): ``` *Show documentation for `sym`* ------------------------------------------------------------------------ source ### MarkdownRenderer ``` python def MarkdownRenderer( sym, name:str | None=None, title_level:int=3, maxline:int=110 ): ``` *Markdown renderer for `show_doc`* ``` python def _f(a, b:callable=print, #param b c:str='foo' #param c )->str: # Result of doing it "Do a thing" ... MarkdownRenderer(_f) ``` ``` python def _f( a, b:callable=print, # param b c:str='foo', # param c )->str: # Result of doing it ``` *Do a thing* ``` python def f(a:int=0 # aa ): pass @delegates(f) def g( b:int|str, # bb **kwargs ): return kwargs ``` ``` python MarkdownRenderer(g) ``` ``` python def g( b:int | str, # bb a:int=0, # aa ): ``` ``` python MarkdownRenderer(next) ``` ``` python def next(iterator, default=..., /) ``` *Return the next item from the iterator.* If default is given and the iterator is exhausted, it is returned instead of raising StopIteration. --- # Source: https://gist.githubusercontent.com/hamelsmu/ea9e0519d9a94a4203bcc36043eb01c5/raw/6c0c96a2823d67aecc103206d6ab21c05dcd520a/fastcore:_an_underrated_python_library.md # fastcore: An Underrated Python Library A unique python library that extends the python programming language and provides utilities that enhance productivity. Sep 1, 2020 • Hamel Husain • 14 min read __fastcore fastai # Background __ I recently embarked on a journey to sharpen my python skills: I wanted to learn advanced patterns, idioms, and techniques. I started with reading books on advanced Python, however, the information didn't seem to stick without having somewhere to apply it. I also wanted the ability to ask questions from an expert while I was learning -- which is an arrangement that is hard to find! That's when it occurred to me: What if I could find an open source project that has fairly advanced python code and write documentation and tests? I made a bet that if I did this it would force me to learn everything very deeply, and the maintainers would be appreciative of my work and be willing to answer my questions. And that's exactly what I did over the past month! I'm pleased to report that it has been the most efficient learning experience I've ever experienced. I've discovered that writing documentation forced me to deeply understand not just what the code does but also _why the code works the way it does_ , and to explore edge cases while writing tests. Most importantly, I was able to ask questions when I was stuck, and maintainers were willing to devote extra time knowing that their mentorship was in service of making their code more accessible! It turns out the library I choose, fastcore is some of the most fascinating Python I have ever encountered as its purpose and goals are fairly unique. For the uninitiated, fastcore is a library on top of which many fast.ai projects are built on. Most importantly, fastcore extends the python programming language and strives to eliminate boilerplate and add useful functionality for common tasks. In this blog post, I'm going to highlight some of my favorite tools that fastcore provides, rather than sharing what I learned about python. My goal is to pique your interest in this library, and hopefully motivate you to check out the documentation after you are done to learn more! # Why fastcore is interesting __ 1. **Get exposed to ideas from other languages without leaving python:** I’ve always heard that it is beneficial to learn other languages in order to become a better programmer. From a pragmatic point of view, I’ve found it difficult to learn other languages because I could never use them at work. Fastcore extends python to include patterns found in languages as diverse as Julia, Ruby and Haskell. Now that I understand these tools I am motivated to learn other languages. 2. **You get a new set of pragmatic tools** : fastcore includes utilities that will allow you to write more concise expressive code, and perhaps solve new problems. 3. **Learn more about the Python programming language:** Because fastcore extends the python programming language, many advanced concepts are exposed during the process. For the motivated, this is a great way to see how many of the internals of python work. # A whirlwind tour through fastcore __ Here are some things you can do with fastcore that immediately caught my attention. * * * ## Making **kwargs transparent __ Whenever I see a function that has the argument****kwargs** , I cringe a little. This is because it means the API is obfuscated and I have to read the source code to figure out what valid parameters might be. Consider the below example: ``` def baz(a, b=2, c=3, d=4): return a + b + c def foo(c, a, **kwargs): return c + baz(a, **kwargs) inspect.signature(foo) ``` ``` ``` Without reading the source code, it might be hard for me to know that `foo` also accepts and additional parameters `b` and `d`. We can fix this with `delegates`: ``` def baz(a, b=2, c=3, d=4): return a + b + c @delegates(baz) # this decorator will pass down keyword arguments from baz def foo(c, a, **kwargs): return c + baz(a, **kwargs) inspect.signature(foo) ``` ``` ``` You can customize the behavior of this decorator. For example, you can have your cake and eat it too by passing down your arguments and also keeping `**kwargs`: ``` @delegates(baz, keep=True) def foo(c, a, **kwargs): return c + baz(a, **kwargs) inspect.signature(foo) ``` ``` ``` You can also exclude arguments. For example, we exclude argument `d` from delegation: ``` def basefoo(a, b=2, c=3, d=4): pass @delegates(basefoo, but=['d']) # exclude `d` def foo(c, a, **kwargs): pass inspect.signature(foo) ``` ``` ``` You can also delegate between classes: ``` class BaseFoo: def __init__(self, e, c=2): pass @delegates()# since no argument was passsed here we delegate to the superclass class Foo(BaseFoo): def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs) inspect.signature(Foo) ``` ``` ``` For more information, read the docs on delegates. * * * ## Avoid boilerplate when setting instance attributes __ Have you ever wondered if it was possible to avoid the boilerplate involved with setting attributes in`__init__`? ``` class Test: def __init__(self, a, b ,c): self.a, self.b, self.c = a, b, c ``` Ouch! That was painful. Look at all the repeated variable names. Do I really have to repeat myself like this when defining a class? Not Anymore! Checkout store_attr: ``` class Test: def __init__(self, a, b, c): store_attr() t = Test(5,4,3) assert t.b == 4 ``` You can also exclude certain attributes: ``` class Test: def __init__(self, a, b, c): store_attr(but=['c']) t = Test(5,4,3) assert t.b == 4 assert not hasattr(t, 'c') ``` There are many more ways of customizing and using `store_attr` than I highlighted here. Check out the docs for more detail. P.S. you might be thinking that Python dataclasses also allow you to avoid this boilerplate. While true in some cases, `store_attr` is more flexible.1 1\. For example, store_attr does not rely on inheritance, which means you won't get stuck using multiple inheritance when using this with your own classes. Also, unlike dataclasses, store_attr does not require python 3.7 or higher. Furthermore, you can use store_attr anytime in the object lifecycle, and in any location in your class to customize the behavior of how and when variables are stored.↩ * * * ## Avoiding subclassing boilerplate __ One thing I hate about python is the`__super__().__init__()` boilerplate associated with subclassing. For example: ``` class ParentClass: def __init__(self): self.some_attr = 'hello' class ChildClass(ParentClass): def __init__(self): super().__init__() cc = ChildClass() assert cc.some_attr == 'hello' # only accessible b/c you used super ``` We can avoid this boilerplate by using the metaclass PrePostInitMeta. We define a new class called `NewParent` that is a wrapper around the `ParentClass`: ``` class NewParent(ParentClass, metaclass=PrePostInitMeta): def __pre_init__(self, *args, **kwargs): super().__init__() class ChildClass(NewParent): def __init__(self):pass sc = ChildClass() assert sc.some_attr == 'hello' ``` * * * ## Type Dispatch __ Type dispatch, orMultiple dispatch, allows you to change the way a function behaves based upon the input types it receives. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y: ``` collide_with(x::Asteroid, y::Asteroid) = ... # deal with asteroid hitting asteroid collide_with(x::Asteroid, y::Spaceship) = ... # deal with asteroid hitting spaceship collide_with(x::Spaceship, y::Asteroid) = ... # deal with spaceship hitting asteroid collide_with(x::Spaceship, y::Spaceship) = ... # deal with spaceship hitting spaceship ``` Type dispatch can be especially useful in data science, where you might allow different input types (i.e. Numpy arrays and Pandas dataframes) to a function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks. Unfortunately, Python does not support this out-of-the box. Fortunately, there is the @typedispatch decorator to the rescue. This decorator relies upon type hints in order to route inputs the correct version of the function: ``` @typedispatch def f(x:str, y:str): return f'{x}{y}' @typedispatch def f(x:np.ndarray): return x.sum() @typedispatch def f(x:int, y:int): return x+y ``` Below is a demonstration of type dispatch at work for the function `f`: ``` f('Hello ', 'World!') ``` ``` 'Hello World!' ``` ``` f(2,3) ``` ``` 5 ``` ``` f(np.array([5,5,5,5])) ``` ``` 20 ``` There are limitations of this feature, as well as other ways of using this functionality that you can read about here. In the process of learning about typed dispatch, I also found a python library called multipledispatch made by Mathhew Rocklin (the creator of Dask). After using this feature, I am now motivated to learn languages like Julia to discover what other paradigms I might be missing. * * * ## A better version of functools.partial __ `functools.partial` is a great utility that creates functions from other functions that lets you set default values. Lets take this function for example that filters a list to only contain values >= `val`: ``` test_input = [1,2,3,4,5,6] def f(arr, val): "Filter a list to remove any values that are less than val." return [x for x in arr if x >= val] f(test_input, 3) ``` ``` [3, 4, 5, 6] ``` You can create a new function out of this function using `partial` that sets the default value to 5: ``` filter5 = partial(f, val=5) filter5(test_input) ``` ``` [5, 6] ``` One problem with `partial` is that it removes the original docstring and replaces it with a generic docstring: ``` filter5.__doc__ ``` ``` 'partial(func, *args, **keywords) - new function with partial application\n of the given arguments and keywords.\n' ``` fastcore.utils.partialler fixes this, and makes sure the docstring is retained such that the new API is transparent: ``` filter5 = partialler(f, val=5) filter5.__doc__ ``` ``` 'Filter a list to remove any values that are less than val.' ``` * * * ## Composition of functions __ A technique that is pervasive in functional programming languages is function composition, whereby you chain a bunch of functions together to achieve some kind of result. This is especially useful when applying various data transformations. Consider a toy example where I have three functions: (1) Removes elements of a list less than 5 (from the prior section) (2) adds 2 to each number (3) sums all the numbers: ``` def add(arr, val): return [x + val for x in arr] def arrsum(arr): return sum(arr) # See the previous section on partialler add2 = partialler(add, val=2) transform = compose(filter5, add2, arrsum) transform([1,2,3,4,5,6]) ``` ``` 15 ``` But why is this useful? You might me thinking, I can accomplish the same thing with: ``` arrsum(add2(filter5([1,2,3,4,5,6]))) ``` You are not wrong! However, composition gives you a convenient interface in case you want to do something like the following: ``` def fit(x, transforms:list): "fit a model after performing transformations" x = compose(*transforms)(x) y = [np.mean(x)] * len(x) # its a dumb model. Don't judge me return y # filters out elements < 5, adds 2, then predicts the mean fit(x=[1,2,3,4,5,6], transforms=[filter5, add2]) ``` ``` [7.5, 7.5] ``` For more information about `compose`, read the docs. * * * ## A more useful `__repr__`__ In python,`__repr__` helps you get information about an object for logging and debugging. Below is what you get by default when you define a new class. (Note: we are using `store_attr`, which was discussed earlier). ``` class Test: def __init__(self, a, b=2, c=3): store_attr() # `store_attr` was discussed previously Test(1) ``` ``` <__main__.Test at 0x7ffcd766cee0> ``` We can use basic_repr to quickly give us a more sensible default: ``` class Test: def __init__(self, a, b=2, c=3): store_attr() __repr__ = basic_repr('a,b,c') Test(2) ``` ``` Test(a=2, b=2, c=3) ``` * * * ## Monkey Patching With A Decorator __ It can be convenient tomonkey patch with a decorator, which is especially helpful when you want to patch an external library you are importing. We can use the decorator @patch from `fastcore.foundation` along with type hints like so: ``` class MyClass(int): pass @patch def func(self:MyClass, a): return self+a mc = MyClass(3) ``` Now, `MyClass` has an additional method named `func`: ``` mc.func(10) ``` ``` 13 ``` Still not convinced? I'll show you another example of this kind of patching in the next section. * * * ## A better pathlib.Path __ When you seethese extensions to pathlib.path you won't ever use vanilla pathlib again! A number of additional methods have been added to pathlib, such as: * `Path.readlines`: same as `with open('somefile', 'r') as f: f.readlines()` * `Path.read`: same as `with open('somefile', 'r') as f: f.read()` * `Path.save`: saves file as pickle * `Path.load`: loads pickle file * `Path.ls`: shows the contents of the path as a list. * etc. Read more about this here. Here is a demonstration of `ls`: ``` from fastcore.utils import * from pathlib import Path p = Path('.') p.ls() # you don't get this with vanilla Pathlib.Path!! ``` ``` (#7) [Path('2020-09-01-fastcore.ipynb'),Path('README.md'),Path('fastcore_imgs'),Path('2020-02-20-test.ipynb'),Path('.ipynb_checkpoints'),Path('2020-02-21-introducing-fastpages.ipynb'),Path('my_icons')] ``` Wait! What's going on here? We just imported `pathlib.Path` \- why are we getting this new functionality? Thats because we imported the `fastcore.utils` module, which patches this module via the `@patch` decorator discussed earlier. Just to drive the point home on why the `@patch` decorator is useful, I'll go ahead and add another method to `Path` right now: ``` @patch def fun(self:Path): return "This is fun!" p.fun() ``` ``` 'This is fun!' ``` That is magical, right? I know! That's why I'm writing about it! * * * ## An Even More Concise Way To Create Lambdas __ `Self`, with an uppercase S, is an even more concise way to create lambdas that are calling methods on an object. For example, let's create a lambda for taking the sum of a Numpy array: ``` arr=np.array([5,4,3,2,1]) f = lambda a: a.sum() assert f(arr) == 15 ``` You can use `Self` in the same way: ``` f = Self.sum() assert f(arr) == 15 ``` Let's create a lambda that does a groupby and max of a Pandas dataframe: ``` import pandas as pd df=pd.DataFrame({'Some Column': ['a', 'a', 'b', 'b', ], 'Another Column': [5, 7, 50, 70]}) f = Self.groupby('Some Column').mean() f(df) ``` | Another Column ---|--- Some Column | a | 6 b | 60 Read more about `Self` in the docs). * * * ## Notebook Functions __ These are simple but handy, and allow you to know whether or not code is executing in a Jupyter Notebook, Colab, or an Ipython Shell: ``` from fastcore.imports import in_notebook, in_colab, in_ipython in_notebook(), in_colab(), in_ipython() ``` ``` (True, False, True) ``` This is useful if you are displaying certain types of visualizations, progress bars or animations in your code that you may want to modify or toggle depending on the environment. * * * ## A Drop-In Replacement For List __ You might be pretty happy with Python's`list`. This is one of those situations that you don't know you needed a better list until someone showed one to you. Enter `L`, a list like object with many extra goodies. The best way I can describe `L` is to pretend that `list` and `numpy` had a pretty baby: define a list (check out the nice `__repr__` that shows the length of the list!) ``` L(1,2,3) ``` ``` (#3) [1,2,3] ``` Shuffle a list: ``` p = L.range(20).shuffle() p ``` ``` (#20) [8,7,5,12,14,16,2,15,19,6...] ``` Index into a list: ``` p[2,4,6] ``` ``` (#3) [5,14,2] ``` L has sensible defaults, for example appending an element to a list: ``` 1 + L(2,3,4) ``` ``` (#4) [1,2,3,4] ``` There is much more `L` has to offer. Read the docs to learn more. # But Wait ... There's More!__ There are more things I would like to show you about fastcore, but there is no way they would reasonably fit into a blog post. Here is a list of some of my favorite things that I didn't demo in this blog post: ## Utilities __ TheBasics section contain many shortcuts to perform common tasks or provide an additional interface to what standard python provides. * mk_class: quickly add a bunch of attributes to a class * wrap_class: add new methods to a class with a simple decorator * groupby: similar to Scala's groupby * merge: merge dicts * fasttuple: a tuple on steroids * Infinite Lists: useful for padding and testing * chunked: for batching and organizing stuff ## Multiprocessing __ TheMultiprocessing section extends python's multiprocessing library by offering features like: * progress bars * ability to pause to mitigate race conditions with external services * processing things in batches on each worker, ex: if you have a vectorized operation to perform in chunks ## Functional Programming __ Thefunctional programming section is my favorite part of this library. * maps: a map that also composes functions * mapped: A more robust `map` * using_attr: compose a function that operates on an attribute ## Transforms __ Transforms is a collection of utilities for creating data transformations and associated pipelines. These transformation utilities build upon many of the building blocks discussed in this blog post. ## Further Reading __ **It should be noted that you should read themain page of the docs first, followed by the section on tests to fully understand the documentation.** * The fastcore documentation site. * The fastcore GitHub repo. * Blog post on delegation. # Shameless plug: fastpages __ This blog post was written entirely in a Jupyter Notebook, which GitHub automatically converted into to a blog post! Sound interesting?Check out fastpages. --- # Source: https://fastcore.fast.ai/foundation.html.md # Foundation ## Foundational Functions ------------------------------------------------------------------------ source ### working_directory ``` python def working_directory( path ): ``` *Change working directory to `path` and return to previous on exit.* ------------------------------------------------------------------------ source ### add_docs ``` python def add_docs( cls, cls_doc:NoneType=None, docs:VAR_KEYWORD ): ``` *Copy values from [`docs`](https://fastcore.fast.ai/foundation.html#docs) to `cls` docstrings, and confirm all public methods are documented* [`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in [our style guide](https://docs.fast.ai/dev/style.html). Suppose you have the following undocumented class: ``` python class T: def foo(self): pass def bar(self): pass ``` You can add documentation to this class like so: ``` python add_docs(T, cls_doc="A docstring for the class.", foo="The foo method.", bar="The bar method.") ``` Now, docstrings will appear as expected: ``` python test_eq(T.__doc__, "A docstring for the class.") test_eq(T.foo.__doc__, "The foo method.") test_eq(T.bar.__doc__, "The bar method.") ``` [`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error: ``` python class T: def foo(self): pass def bar(self): pass f=lambda: add_docs(T, "A docstring for the class.", foo="The foo method.") test_fail(f, contains="Missing docs") ``` ------------------------------------------------------------------------ source ### docs ``` python def docs( cls ): ``` *Decorator version of [`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs), using `_docs` dict* Instead of using [`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs), you can use the decorator [`docs`](https://fastcore.fast.ai/foundation.html#docs) as shown below. Note that the docstring for the class can be set with the argument `cls_doc`: ``` python @docs class _T: def f(self): pass def g(cls): pass _docs = dict(cls_doc="The class docstring", f="The docstring for method f.", g="A different docstring for method g.") test_eq(_T.__doc__, "The class docstring") test_eq(_T.f.__doc__, "The docstring for method f.") test_eq(_T.g.__doc__, "A different docstring for method g.") ``` For either the [`docs`](https://fastcore.fast.ai/foundation.html#docs) decorator or the [`add_docs`](https://fastcore.fast.ai/foundation.html#add_docs) function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the `_docs` attribute: ``` python @docs class _T: "The class docstring" def f(self): pass _docs = dict(f="The docstring for method f.") test_eq(_T.__doc__, "The class docstring") test_eq(_T.f.__doc__, "The docstring for method f.") ``` ------------------------------------------------------------------------ ### is_iter ``` python def is_iter( o ): ``` *Test whether `o` can be used in a `for` loop* ``` python assert is_iter([1]) assert not is_iter(array(1)) assert is_iter(array([1,2])) assert (o for o in range(3)) ``` ------------------------------------------------------------------------ source ### coll_repr ``` python def coll_repr( c, max_n:int=250 ): ``` *String repr of up to `max_n` items of (possibly lazy) collection `c`* [`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) is used to provide a more informative [`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) about list-like objects. [`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) and is used by [`L`](https://fastcore.fast.ai/foundation.html#l) to build a `__repr__` that displays the length of a list in addition to a preview of a list. ``` python test_eq(coll_repr(range(1000),10), '(#1000) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9...]') test_eq(coll_repr(range(1000), 5), '(#1000) [0, 1, 2, 3, 4...]') test_eq(coll_repr(range(10), 5), '(#10) [0, 1, 2, 3, 4...]') test_eq(coll_repr(range(5), 5), '[0, 1, 2, 3, 4]') ``` ------------------------------------------------------------------------ source ### is_bool ``` python def is_bool( x ): ``` *Check whether `x` is a bool or None* ------------------------------------------------------------------------ source ### mask2idxs ``` python def mask2idxs( mask ): ``` *Convert bool mask or index list to index [`L`](https://fastcore.fast.ai/foundation.html#l)* ``` python test_eq(mask2idxs([False,True,False,True]), [1,3]) test_eq(mask2idxs(array([False,True,False,True])), [1,3]) test_eq(mask2idxs(array([1,2,3])), [1,2,3]) ``` ------------------------------------------------------------------------ source ### cycle ``` python def cycle( o ): ``` *Like `itertools.cycle` except creates list of `None`s if `o` is empty* ``` python test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2]) test_eq(itertools.islice(cycle([]),3), [None]*3) test_eq(itertools.islice(cycle(None),3), [None]*3) test_eq(itertools.islice(cycle(1),3), [1,1,1]) ``` ------------------------------------------------------------------------ source ### zip_cycle ``` python def zip_cycle( x, args:VAR_POSITIONAL ): ``` *Like `itertools.zip_longest` but [`cycle`](https://fastcore.fast.ai/basics.html#cycle)s through elements of all but first argument* ``` python test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')]) ``` ------------------------------------------------------------------------ source ### is_indexer ``` python def is_indexer( idx ): ``` *Test whether `idx` will index a single item in a list* You can, for example index a single item in a list with an integer or a 0-dimensional numpy array: ``` python assert is_indexer(1) assert is_indexer(np.array(1)) ``` However, you cannot index into single item in a list with another list or a numpy array with ndim \> 0. ``` python assert not is_indexer([1, 2]) assert not is_indexer(np.array([[1, 2], [3, 4]])) ``` ------------------------------------------------------------------------ source ### product ``` python def product( xs ): ``` *The product of elements of `xs`, with `None`s removed* ``` python product([None, 3, 4, 5]) ``` 60 ``` python product([]) ``` 1 ``` python sum([]) ``` 0 ### flatmap ------------------------------------------------------------------------ source ### flatmap ``` python def flatmap( f, xs, kwargs:VAR_KEYWORD ): ``` *Apply f to each element and flatten the results into a single list.* [`flatmap`](https://fastcore.fast.ai/foundation.html#flatmap) is a fundamental operation in functional programming that combines mapping and flattening into a single step. Where `map` applies a function to each element and returns a list of results, [`flatmap`](https://fastcore.fast.ai/foundation.html#flatmap) goes further: it expects the function to return a sequence for each element, then concatenates all those sequences into one flat list, which is useful for operations where each input naturally produces zero, one, or many outputs. `flatmap(f, xs)` is just a named abstraction for the list comprehension `[y for x in xs for y in f(x)]`. Giving it a name makes the intent clearer and the code more readable. ``` python flatmap(range, range(4)) ``` [0, 0, 1, 0, 1, 2] Compare `map` (which nests results) with [`flatmap`](https://fastcore.fast.ai/foundation.html#flatmap) (which flattens them): ``` python list(map(str.split, ["hello world", "foo bar"])) # nested ``` [['hello', 'world'], ['foo', 'bar']] ``` python flatmap(str.split, ["hello world", "flatmap rocks"]) ``` ['hello', 'world', 'flatmap', 'rocks'] Common use cases include: parsing structured text (splitting lines into words), expanding nested data (extracting all emails from a list of contacts), filtering with transformation (keeping and transforming only valid items), and traversing hierarchies (listing files across multiple directories). The pattern elegantly handles “optional” results too—return an empty list to skip an item, or a single-element list to include it. This avoids the nested lists you’d get from `map` followed by a separate [`flatten`](https://fastcore.fast.ai/basics.html#flatten), and expresses the intent more directly. Below we show a few examples. Parse CSV-like lines into all values: ``` python flatmap(Self.split(','), ["a,b,c", "d,e"]) ``` ['a', 'b', 'c', 'd', 'e'] Return `[]` to skip an item, `[x]` to keep it, or `[x, y, ...]` to expand it: ``` python flatmap(lambda x: [x*10] if x else [], [1, 0, 2]) # skips zeros ``` [10, 20] ``` python dat = [{'emails': ['a@x.com','b@x.com']}, {'emails': []}, {'emails': ['c@x.com']}] flatmap(Self['emails'], dat) ``` ['a@x.com', 'b@x.com', 'c@x.com'] All files in multiple directories: ``` python flatmap(Self.iterdir(), [Path('files'), Path('images')]) ``` [Path('files/test.txt.bz2'), Path('images/mnist3.png'), Path('images/att_00000.png'), Path('images/att_00005.png'), Path('images/att_00007.png'), Path('images/att_00006.png'), Path('images/puppy.jpg')] Pair each item with its factors: ``` python def factpairs(n): return [(n,i) for i in range(1,n+1) if n%i==0] flatmap(factpairs, [6,10]) ``` [(6, 1), (6, 2), (6, 3), (6, 6), (10, 1), (10, 2), (10, 5), (10, 10)] You can also use kwargs, for instance to apply `str.split` with a custom separator: ``` python flatmap(str.split, ["a-b-c", "d-e"], sep="-") ``` ['a', 'b', 'c', 'd', 'e'] ## [`L`](https://fastcore.fast.ai/foundation.html#l) helpers ------------------------------------------------------------------------ source ### CollBase ``` python def CollBase( items ): ``` *Base class for composing a list of `items`* `ColBase` is a base class that emulates the functionality of a python `list`: ``` python class _T(CollBase): pass l = _T([1,2,3,4,5]) test_eq(len(l), 5) # __len__ test_eq(l[-1], 5); test_eq(l[0], 1) #__getitem__ l[2] = 100; test_eq(l[2], 100) # __set_item__ del l[0]; test_eq(len(l), 4) # __delitem__ test_eq(str(l), '[2, 100, 4, 5]') # __repr__ ``` ------------------------------------------------------------------------ source ### L ``` python def L( items:NoneType=None, rest:VAR_POSITIONAL, use_list:bool=False, match:NoneType=None ): ``` *Behaves like a list of `items` but can also index with list of indices or masks* [`L`](https://fastcore.fast.ai/foundation.html#l) is a drop in replacement for a python `list`. Inspired by [NumPy](http://www.numpy.org/), [`L`](https://fastcore.fast.ai/foundation.html#l), supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code. ### Examples and overview ``` python from fastcore.utils import gt ``` Read [this overview section](https://fastcore.fast.ai/tour.html#L) for a quick tutorial of [`L`](https://fastcore.fast.ai/foundation.html#l), as well as background on the name. You can create an [`L`](https://fastcore.fast.ai/foundation.html#l) from an existing iterable (e.g. a list, range, etc) and access or modify it with an int list/tuple index, mask, int, or slice. All `list` methods can also be used with [`L`](https://fastcore.fast.ai/foundation.html#l). ``` python t = L(range(12)) test_eq(t, list(range(12))) test_ne(t, list(range(11))) t[3] = "h" test_eq(t[3], "h") t[3,5] = ("j","k") test_eq(t[3,5], ["j","k"]) test_eq(t, L(t)) test_eq(L(L(1,2),[3,4]), ([1,2],[3,4])) t[0:3] = [1, 2, 3] test_eq(t[0:3], [1, 2, 3]) t ``` [1, 2, 3, 'j', 4, 'k', 6, 7, 8, 9, 10, 11] Any [`L`](https://fastcore.fast.ai/foundation.html#l) is a `Sequence` so you can use it with methods like `random.sample`: ``` python assert isinstance(t, Sequence) ``` ``` python import random ``` ``` python random.seed(0) random.sample(t, 3) ``` [6, 11, 1] There are optimized indexers for arrays, tensors, and DataFrames. ``` python import pandas as pd ``` ``` python arr = np.arange(9).reshape(3,3) t = L(arr, use_list=None) test_eq(t[1,2], arr[[1,2]]) df = pd.DataFrame({'a':[1,2,3]}) t = L(df, use_list=None) test_eq(t[1,2], L(pd.DataFrame({'a':[2,3]}, index=[1,2]), use_list=None)) ``` You can also modify an [`L`](https://fastcore.fast.ai/foundation.html#l) with `append`, `+`, and `*`. ``` python t = L() test_eq(t, []) t.append(1) test_eq(t, [1]) t += [3,2] test_eq(t, [1,3,2]) t = t + [4] test_eq(t, [1,3,2,4]) t = 5 + t test_eq(t, [5,1,3,2,4]) test_eq(L(1,2,3), [1,2,3]) test_eq(L(1,2,3), L(1,2,3)) t = L(1)*5 test_eq(~L([True,False,False]), L([False,True,True])) ``` An [`L`](https://fastcore.fast.ai/foundation.html#l) can be constructed from anything iterable, although tensors and arrays will not be iterated over on construction, unless you pass `use_list` to the constructor. ``` python test_eq(L([1,2,3]),[1,2,3]) test_eq(L(L([1,2,3])),[1,2,3]) test_ne(L([1,2,3]),[1,2,]) test_eq(L('abc'),['abc']) test_eq(L(range(0,3)),[0,1,2]) test_eq(L(o for o in range(0,3)),[0,1,2]) test_eq(L(array(0)),[array(0)]) test_eq(L([array(0),array(1)]),[array(0),array(1)]) test_eq(L(array([0.,1.1]))[0],array([0.,1.1])) test_eq(L(array([0.,1.1]), use_list=True), [array(0.),array(1.1)]) # `use_list=True` to unwrap arrays/arrays ``` If `match` is not `None` then the created list is same len as `match`, either by: - If `len(items)==1` then `items` is replicated, - Otherwise an error is raised if `match` and `items` are not already the same size. ``` python test_eq(L(1,match=[1,2,3]),[1,1,1]) test_eq(L([1,2],match=[2,3]),[1,2]) test_fail(lambda: L([1,2],match=[1,2,3])) ``` If you create an [`L`](https://fastcore.fast.ai/foundation.html#l) from an existing [`L`](https://fastcore.fast.ai/foundation.html#l) then you’ll get back the original object (since [`L`](https://fastcore.fast.ai/foundation.html#l) uses the [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) metaclass). ``` python test_is(L(t), t) ``` An [`L`](https://fastcore.fast.ai/foundation.html#l) is considred equal to a list if they have the same elements. It’s never considered equal to a `str` a `set` or a `dict` even if they have the same elements/keys. ``` python test_eq(L(['a', 'b']), ['a', 'b']) test_ne(L(['a', 'b']), 'ab') test_ne(L(['a', 'b']), {'a':1, 'b':2}) ``` ### [`L`](https://fastcore.fast.ai/foundation.html#l) Methods ------------------------------------------------------------------------ source ### L.\_\_getitem\_\_ ``` python def __getitem__( idx ): ``` *Retrieve `idx` (can be list of indices, or mask, or int) items* ``` python t = L(range(12)) test_eq(t[1,2], [1,2]) # implicit tuple test_eq(t[[1,2]], [1,2]) # list test_eq(t[:3], [0,1,2]) # slice test_eq(t[[False]*11 + [True]], [11]) # mask test_eq(t[array(3)], 3) ``` ------------------------------------------------------------------------ source ### L.\_\_setitem\_\_ ``` python def __setitem__( idx, o ): ``` *Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable)* ``` python t[4,6] = 0 test_eq(t[4,6], [0,0]) t[4,6] = [1,2] test_eq(t[4,6], [1,2]) ``` ------------------------------------------------------------------------ source ### L.unique ``` python def unique( sort:bool=False, bidir:bool=False, start:NoneType=None ): ``` *Unique items, in stable order* ``` python test_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3]) ``` ------------------------------------------------------------------------ source ### L.val2idx ``` python def val2idx( ): ``` *Dict from value to index* ``` python test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1}) ``` ------------------------------------------------------------------------ source ### L.range ``` python def range( a, b:NoneType=None, step:NoneType=None ): ``` *Class Method: Same as `range`, but returns [`L`](https://fastcore.fast.ai/foundation.html#l). Can pass collection for `a`, to use `len(a)`* ``` python test_eq_type(L.range([1,1,1]), L(range(3))) test_eq_type(L.range(5,2,2), L(range(5,2,2))) ``` ------------------------------------------------------------------------ source ### L.enumerate ``` python def enumerate( ): ``` *Same as `enumerate`* ``` python test_eq(L('a','b','c').enumerate(), [(0,'a'),(1,'b'),(2,'c')]) ``` ------------------------------------------------------------------------ source ### L.renumerate ``` python def renumerate( ): ``` *Same as [`renumerate`](https://fastcore.fast.ai/basics.html#renumerate)* ``` python test_eq(L('a','b','c').renumerate(), [('a', 0), ('b', 1), ('c', 2)]) ``` ------------------------------------------------------------------------ source ### L.split ``` python def split( s, sep:NoneType=None, maxsplit:int=-1 ): ``` *Class Method: Same as `str.split`, but returns an [`L`](https://fastcore.fast.ai/foundation.html#l)* [`L.split`](https://fastcore.fast.ai/foundation.html#l.split) is a class method that works like `str.split`, but returns an [`L`](https://fastcore.fast.ai/foundation.html#l) instead of a list: ``` python test_eq(L.split('a b c'), ['a','b','c']) test_eq(L.split('a-b-c', '-'), ['a','b','c']) test_eq(L.split('a-b-c', '-', maxsplit=1), ['a','b-c']) ``` ------------------------------------------------------------------------ source ### L.splitlines ``` python def splitlines( s, keepends:bool=False ): ``` *Class Method: Same as `str.splitlines`, but returns an [`L`](https://fastcore.fast.ai/foundation.html#l)* [`L.splitlines`](https://fastcore.fast.ai/foundation.html#l.splitlines) is a class method that works like `str.splitlines`, but returns an [`L`](https://fastcore.fast.ai/foundation.html#l) instead of a list: ``` python test_eq(L.splitlines('a\nb\nc'), ['a','b','c']) test_eq(L.splitlines('a\nb\nc', keepends=True), ['a\n','b\n','c']) ``` ------------------------------------------------------------------------ source ### curryable ``` python def curryable( f ): ``` The [`curryable`](https://fastcore.fast.ai/foundation.html#curryable) decorator enables a powerful pattern: methods decorated with it can be called either as instance methods (the normal way) or as class methods that return a partial function. For instance, consider processing nested data structures. Without curryable, you’d write: ``` python L(lines).map(lambda x: L(x).map(int)) ``` With curryable, you can write: ``` python L(lines).map(L.map(int)) ``` When you call `L.map(int)` on the class (not an instance), the decorator returns a `functools.partial` that waits for an iterable to be passed in later. This pattern is especially valuable for data parsing pipelines where you’re frequently mapping transformations over nested structures. The curried form reads more naturally and composes well with other curried functions like [`splitter()`](https://fastcore.fast.ai/foundation.html#splitter) and [`linesplitter()`](https://fastcore.fast.ai/foundation.html#linesplitter). ------------------------------------------------------------------------ ### map ``` python def map( f, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with `f` applied to all `items`, passing `args` and `kwargs` to `f`* ``` python test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3]) ``` If `f` is a string then it is treated as a format string to create the mapping: ``` python test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#']) ``` If `f` is a dictionary (or anything supporting `__getitem__`) then it is indexed to create the mapping: ``` python test_eq(L.range(4).map(list('abcd')), list('abcd')) ``` You can also pass the same `arg` params that [`bind`](https://fastcore.fast.ai/basics.html#bind) accepts: ``` python def f(a=None,b=None): return b test_eq(L.range(4).map(f, b=arg0), range(4)) ``` ------------------------------------------------------------------------ source ### splitter ``` python def splitter( sep:NoneType=None, maxsplit:int=-1 ): ``` *Create a partial function that splits strings into [`L`](https://fastcore.fast.ai/foundation.html#l)* A curried version of [`L.split`](https://fastcore.fast.ai/foundation.html#l.split), useful for mapping over collections of strings. For instance to split some lines with the same separator: ``` python data = '''1,2,3 4,5,6 7,8,9''' grid = L.splitlines(data).map(splitter(',')) grid ``` [['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9']] As mentioned in the [`curryable`](https://fastcore.fast.ai/foundation.html#curryable) discussion, `map` can be curried. This can work well together with [`L.splitlines`](https://fastcore.fast.ai/foundation.html#l.splitlines) output: ``` python intgrid = grid.map(L.map(int)) intgrid ``` [[1, 2, 3], [4, 5, 6], [7, 8, 9]] Although in this particular example numpy has a useful shortcut: ``` python np.genfromtxt(data.splitlines(), delimiter=',', dtype=int) ``` array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) ------------------------------------------------------------------------ source ### linesplitter ``` python def linesplitter( keepends:bool=False ): ``` *Create a partial function that splits strings by lines into [`L`](https://fastcore.fast.ai/foundation.html#l)* A curried version of [`L.splitlines`](https://fastcore.fast.ai/foundation.html#l.splitlines), useful for splitting multi-line strings into [`L`](https://fastcore.fast.ai/foundation.html#l)s when mapping over a collection. ``` python L(['a\nb\nc', 'd\ne']).map(linesplitter()) ``` [['a', 'b', 'c'], ['d', 'e']] ------------------------------------------------------------------------ source ### groupby ``` python def groupby( key, val:function=noop ): ``` *Same as [`fastcore.basics.groupby`](https://fastcore.fast.ai/basics.html#groupby)* ``` python words = L.split('aaa abc bba') test_eq(words.groupby(0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]}) ``` [`L.groupby`](https://fastcore.fast.ai/foundation.html#l.groupby) can also be used in curried form, which is useful when you need to apply the same grouping operation across multiple collections. ``` python L([['a1','b2','a3'], ['x1','y2','x3']]).map(L.groupby(0)) ``` [{'a': ['a1', 'a3'], 'b': ['b2']}, {'x': ['x1', 'x3'], 'y': ['y2']}] ------------------------------------------------------------------------ ### starmap ``` python def starmap( f, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Like `map`, but use `itertools.starmap`* [`L.starmap`](https://fastcore.fast.ai/foundation.html#l.starmap) applies a function to each element, unpacking tuples as arguments: ``` python test_eq(L([(1,2),(3,4)]).starmap(operator.add), [3,7]) test_eq(L([(1,2,3),(4,5,6)]).starmap(lambda a,b,c: a+b*c), [7,34]) ``` The curried form of [`L.starmap`](https://fastcore.fast.ai/foundation.html#l.starmap) is useful when you need to apply the same starmap operation across nested structures. For example, when you have a list of lists of tuples and want to apply a function that unpacks each tuple: ``` python nested = L([[(1,2),(3,4)], [(5,6),(7,8)]]) nested.map(L.starmap(operator.mul)) ``` [[2, 12], [30, 56]] ------------------------------------------------------------------------ ### rstarmap ``` python def rstarmap( f, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Like `starmap`, but reverse the order of args* [`L.rstarmap`](https://fastcore.fast.ai/foundation.html#l.rstarmap) is like `starmap`, but reverses the order of unpacked arguments: ``` python test_eq(L((1,2),(3,4)).rstarmap(operator.sub), [1,1]) # 2-1, 4-3 test_eq(L(('a','b'),('c','d')).rstarmap('{}{}'.format), ['ba','dc']) ``` The curried form of [`L.rstarmap`](https://fastcore.fast.ai/foundation.html#l.rstarmap) is useful when mapping over nested structures where you need reversed argument order. This commonly occurs when processing pairs where the second element should be the first argument to a function: ``` python nested = L([[('x',1),('y',2)], [('z',3)]]) nested.map(L.rstarmap('{}{}'.format)) ``` [['1x', '2y'], ['3z']] ------------------------------------------------------------------------ source ### L.map_dict ``` python def map_dict( f:function=noop, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Like `map`, but creates a dict from `items` to function results* ``` python test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4}) test_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4}) ``` ------------------------------------------------------------------------ source ### L.zip ``` python def zip( cycled:bool=False ): ``` *Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with `zip(*items)`* ``` python t = L([[1,2,3],'abc']) test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')]) ``` ``` python t = L([[1,2,3,4],['a','b','c']]) test_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')]) test_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')]) ``` ------------------------------------------------------------------------ source ### L.map_zip ``` python def map_zip( f, args:VAR_POSITIONAL, cycled:bool=False, kwargs:VAR_KEYWORD ): ``` *Combine `zip` and `starmap`* ``` python t = L([1,2,3],[2,3,4]) test_eq(t.map_zip(operator.mul), [2,6,12]) ``` ------------------------------------------------------------------------ source ### L.zipwith ``` python def zipwith( rest:VAR_POSITIONAL, cycled:bool=False ): ``` *Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with `self` zip with each of `*rest`* ``` python b = [[0],[1],[2,2]] t = L([1,2,3]).zipwith(b) test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])]) ``` ------------------------------------------------------------------------ source ### L.map_zipwith ``` python def map_zipwith( f, rest:VAR_POSITIONAL, cycled:bool=False, kwargs:VAR_KEYWORD ): ``` *Combine `zipwith` and `starmap`* ``` python test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12]) ``` ------------------------------------------------------------------------ ### filter ``` python def filter( f:function=noop, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *Create new [`L`](https://fastcore.fast.ai/foundation.html#l) filtered by predicate `f`, passing `args` and `kwargs` to `f`* ``` python t = L(range(12)) test_eq(t.filter(lambda o:o<5), [0,1,2,3,4]) test_eq(t.filter(lambda o:o<5, negate=True), [5,6,7,8,9,10,11]) ``` [`L.filter`](https://fastcore.fast.ai/foundation.html#l.filter) can be used as a curried class method, returning a partial that filters any iterable and wraps the result in an [`L`](https://fastcore.fast.ai/foundation.html#l). This is useful when mapping a filter operation over nested collections. ``` python intgrid ``` [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ``` python intgrid.map(L.filter(ge(5))) ``` [[], [5, 6], [7, 8, 9]] ------------------------------------------------------------------------ ### starfilter ``` python def starfilter( f, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *Like `filter`, but unpacks elements as args to `f`* [`L.starfilter`](https://fastcore.fast.ai/foundation.html#l.starfilter) is like `filter`, but unpacks tuple elements as arguments to the predicate: ``` python test_eq(L((1,2),(3,1),(2,3)).starfilter(lt), [(1,2),(2,3)]) test_eq(L((1,2),(3,1),(2,3)).starfilter(lt, negate=True), [(3,1)]) ``` Curried [`L.starfilter`](https://fastcore.fast.ai/foundation.html#l.starfilter) is useful when mapping a starfilter operation over nested collections—each inner collection gets filtered by unpacking its tuples as arguments to the predicate, eg to filter pairs where first \< second, across multiple lists of pairs: ``` python nested = L([[(1,5),(3,2)], [(4,6),(9,1)]]) nested.map(L.starfilter(lt)) ``` [[(1, 5)], [(4, 6)]] ------------------------------------------------------------------------ ### rstarfilter ``` python def rstarfilter( f, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *Like `starfilter`, but reverse the order of args* [`L.rstarfilter`](https://fastcore.fast.ai/foundation.html#l.rstarfilter) is like `starfilter`, but reverses the order of unpacked arguments (and can also be curried): ``` python test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt), [(2,1),(3,2)]) # 1<2, 3<1 fails, 2<3 test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt, negate=True), [(1,3)]) ``` ------------------------------------------------------------------------ source ### argwhere ``` python def argwhere( f, negate:bool=False, kwargs:VAR_KEYWORD ): ``` *Like `filter`, but return indices for matching items* ``` python t = L([0,1,2,3,4,99,0]) test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6]) ``` ------------------------------------------------------------------------ ### starargwhere ``` python def starargwhere( f, negate:bool=False ): ``` *Like [`argwhere`](https://fastcore.fast.ai/basics.html#argwhere), but unpacks elements as args to `f`* [`L.starargwhere`](https://fastcore.fast.ai/foundation.html#l.starargwhere) is like [`argwhere`](https://fastcore.fast.ai/basics.html#argwhere), but unpacks tuple elements as arguments to the predicate (it is also curryable): ``` python test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt), [0,2]) test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt, negate=True), [1]) ``` ------------------------------------------------------------------------ ### rstarargwhere ``` python def rstarargwhere( f, negate:bool=False ): ``` *Like `starargwhere`, but reverse the order of args* [`L.rstarargwhere`](https://fastcore.fast.ai/foundation.html#l.rstarargwhere) is like `starargwhere`, but reverses the order of unpacked arguments (it is also curryable): ``` python test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt), [0,2]) # 1<2, 3<1 fails, 2<3 test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt, negate=True), [1]) ``` ------------------------------------------------------------------------ ### argfirst ``` python def argfirst( f, negate:bool=False ): ``` *Return index of first matching item* ``` python test_eq(t.argfirst(lambda o:o>4), 5) test_eq(t.argfirst(lambda o:o>4,negate=True),0) ``` Curried [`L.argfirst`](https://fastcore.fast.ai/foundation.html#l.argfirst) returns a partial function that finds the index of the first matching item in any iterable. This is useful when mapping over nested collections to find the first match in each. ``` python nested = L([[1,2,8,4], [5,9,7], [1,1,1]]) nested.map(L.argfirst(gt(5))) ``` [2, 1, None] ------------------------------------------------------------------------ ### starargfirst ``` python def starargfirst( f, negate:bool=False ): ``` *Like `argfirst`, but unpacks elements as args to `f`* [`L.starargfirst`](https://fastcore.fast.ai/foundation.html#l.starargfirst) is like `argfirst`, but unpacks tuple elements as arguments to the predicate (and is curryable): ``` python test_eq(L((3,1),(1,2),(2,3)).starargfirst(lt), 1) test_eq(L((1,2),(3,1),(2,3)).starargfirst(lt, negate=True), 1) ``` ------------------------------------------------------------------------ ### rstarargfirst ``` python def rstarargfirst( f, negate:bool=False ): ``` *Like `starargfirst`, but reverse the order of args* [`L.rstarargfirst`](https://fastcore.fast.ai/foundation.html#l.rstarargfirst) is like `starargfirst`, but reverses the order of unpacked arguments (and is curryable): ``` python test_eq(L((1,3),(2,1),(3,2)).rstarargfirst(lt), 1) # 3<1 fails, 1<2 test_eq(L((2,1),(1,3),(3,2)).rstarargfirst(lt, negate=True), 1) ``` ------------------------------------------------------------------------ source ### L.itemgot ``` python def itemgot( idxs:VAR_POSITIONAL ): ``` *Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with item `idx` of all `items`* ``` python t = L([['x', [0]], ['y', [1]], ['z', [2,2]]]) test_eq(t.itemgot(1), b) ``` ------------------------------------------------------------------------ source ### L.attrgot ``` python def attrgot( k, default:NoneType=None ): ``` *Create new [`L`](https://fastcore.fast.ai/foundation.html#l) with attr `k` (or value `k` for dicts) of all `items`.* ``` python # Example when items are not a dict a = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)] test_eq(L(a).attrgot('b'), [4,2]) #Example of when items are a dict b =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}] test_eq(L(b).attrgot('id'), [15, 17]) ``` ------------------------------------------------------------------------ ### sorted ``` python def sorted( key:NoneType=None, reverse:bool=False, cmp:NoneType=None, kwargs:VAR_KEYWORD ): ``` *New [`L`](https://fastcore.fast.ai/foundation.html#l) sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`* ``` python test_eq(L(a).sorted('a').attrgot('b'), [2,4]) ``` Curried [`L.sorted`](https://fastcore.fast.ai/foundation.html#l.sorted) returns a partial function that sorts any iterable by the given key. This is useful when mapping a sort operation over nested collections—each inner collection gets sorted independently using the same key. ``` python nested = L([[(3,'c'),(1,'a'),(2,'b')], [(6,'f'),(4,'d')]]) nested.map(L.sorted(0)) ``` [[(1, 'a'), (2, 'b'), (3, 'c')], [(4, 'd'), (6, 'f')]] ------------------------------------------------------------------------ ### starsorted ``` python def starsorted( key, reverse:bool=False ): ``` *Like `sorted`, but unpacks elements as args to `key`* [`L.starsorted`](https://fastcore.fast.ai/foundation.html#l.starsorted) is like `sorted`, but unpacks tuple elements as arguments to the key function: ``` python test_eq(L((3,1),(1,2),(2,0)).starsorted(operator.sub), [(1,2),(3,1),(2,0)]) # sorted by a-b: 2, 2, -1 test_eq(L((1,2),(3,1),(2,3)).starsorted(operator.add), [(1,2),(3,1),(2,3)]) # sorted by a+b: 3, 4, 5 ``` ------------------------------------------------------------------------ ### rstarsorted ``` python def rstarsorted( key, reverse:bool=False ): ``` *Like `starsorted`, but reverse the order of args* [`L.rstarsorted`](https://fastcore.fast.ai/foundation.html#l.rstarsorted) is like `starsorted`, but reverses the order of unpacked arguments: ``` python test_eq(L((1,3),(2,1),(0,2)).rstarsorted(operator.sub), [(2,1),(1,3),(0,2)]) # sorted by b-a: 0, 2, 2 test_eq(L((2,1),(1,3),(3,2)).rstarsorted(operator.sub), [(2,1),(3,2),(1,3)]) # sorted by b-a: -1, -1, 2 ``` ------------------------------------------------------------------------ source ### L.concat ``` python def concat( ): ``` *Concatenate all elements of list* ``` python test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7)) ``` ------------------------------------------------------------------------ source ### L.copy ``` python def copy( ): ``` *Same as `list.copy`, but returns an [`L`](https://fastcore.fast.ai/foundation.html#l)* ``` python t = L([0,1,2,3],4,L(5,6)).copy() test_eq(t.concat(), range(7)) ``` ------------------------------------------------------------------------ source ### L.shuffle ``` python def shuffle( ): ``` *Same as `random.shuffle`, but not inplace* [`L.shuffle`](https://fastcore.fast.ai/foundation.html#l.shuffle) returns a new shuffled [`L`](https://fastcore.fast.ai/foundation.html#l), leaving the original unchanged: ``` python t = L(1,2,3,4,5) s = t.shuffle() test_eq(set(s), set(t)) # same elements test_eq(t, [1,2,3,4,5]) # original unchanged ``` ------------------------------------------------------------------------ ### reduce ``` python def reduce( f, initial:NoneType=None ): ``` *Wrapper for `functools.reduce`* ``` python test_eq(L(1,2,3,4).reduce(operator.add), 10) test_eq(L(1,2,3,4).reduce(operator.mul, 10), 240) ``` Curried [`L.reduce`](https://fastcore.fast.ai/foundation.html#l.reduce) returns a partial function that reduces any iterable using the given function. This is useful when mapping a reduction over nested collections—each inner collection gets reduced independently using the same operation. ``` python nested = L([[1,2,3], [4,5], [6,7,8,9]]) nested.map(L.reduce(operator.add)) ``` [6, 9, 30] ------------------------------------------------------------------------ ### starreduce ``` python def starreduce( f, initial:NoneType=None ): ``` *Like `reduce`, but unpacks elements as args to `f`* [`L.starreduce`](https://fastcore.fast.ai/foundation.html#l.starreduce) is like `reduce`, but unpacks tuple elements as additional arguments to `f` (after accumulator): ``` python test_eq(L((1,2),(3,4),(5,6)).starreduce(lambda acc,a,b: acc+a*b, 0), 44) # 0+1*2+3*4+5*6 test_eq(L(('a',1),('b',2)).starreduce(lambda acc,k,v: {**acc, k:v}, {}), {'a':1,'b':2}) ``` E.g implement a dot product: ``` python def dot(a,b): return a.zipwith(b).starreduce(lambda acc,a,b: acc+a*b, 0) dot(L(1,3,5), L(2,4,6)) ``` 44 ------------------------------------------------------------------------ ### rstarreduce ``` python def rstarreduce( f, initial:NoneType=None ): ``` *Like `starreduce`, but reverse the order of unpacked args* [`L.rstarreduce`](https://fastcore.fast.ai/foundation.html#l.rstarreduce) is like `starreduce`, but reverses the order of unpacked arguments: ------------------------------------------------------------------------ source ### L.sum ``` python def sum( ): ``` *Sum of the items* ``` python test_eq(L(1,2,3,4).sum(), 10) test_eq(L().sum(), 0) ``` ------------------------------------------------------------------------ source ### L.product ``` python def product( ): ``` *Product of the items* ``` python test_eq(L(1,2,3,4).product(), 24) test_eq(L().product(), 1) ``` ------------------------------------------------------------------------ source ### L.map_first ``` python def map_first( f:function=noop, g:function=noop, args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *First element of `map_filter`* ``` python t = L(0,1,2,3) test_eq(t.map_first(lambda o:o*2 if o>2 else None), 6) ``` ------------------------------------------------------------------------ source ### L.setattrs ``` python def setattrs( attr, val ): ``` *Call `setattr` on all items* ``` python t = L(SimpleNamespace(),SimpleNamespace()) t.setattrs('foo', 'bar') test_eq(t.attrgot('foo'), ['bar','bar']) ``` ------------------------------------------------------------------------ source ### L.flatmap ``` python def flatmap( f, kwargs:VAR_KEYWORD ): ``` *Apply f to each element and flatten the results into a single L.* [`L.flatmap`](https://fastcore.fast.ai/foundation.html#l.flatmap) is the method version of the [`flatmap`](https://fastcore.fast.ai/foundation.html#flatmap) function, allowing you to call it directly on an [`L`](https://fastcore.fast.ai/foundation.html#l) instance. It applies a function to each element and flattens the results into a single [`L`](https://fastcore.fast.ai/foundation.html#l). This is useful for operations where each input naturally produces zero, one, or many outputs. ``` python test_eq(L("a,b,c", "d,e").flatmap(Self.split(',')), ['a', 'b', 'c', 'd', 'e']) ``` or alternatively use kwargs: ``` python test_eq(L("a-b-c", "d-e").flatmap(str.split, sep='-'), ['a', 'b', 'c', 'd', 'e']) ``` As an alternative, you can just chain `map` and [`concat`](https://fastcore.fast.ai/basics.html#concat): ``` python L("a,b,c", "d,e").map(Self.split(',')).concat() ``` ['a', 'b', 'c', 'd', 'e'] ``` python L("a-b-c", "d-e").map(str.split, sep='-').concat() ``` ['a', 'b', 'c', 'd', 'e'] ### itertools wrappers ------------------------------------------------------------------------ source ### L.cycle ``` python def cycle( ): ``` *Same as `itertools.cycle`* [`L.cycle`](https://fastcore.fast.ai/foundation.html#l.cycle) returns an infinite iterator that cycles through the elements: ``` python test_eq(list(itertools.islice(L(1,2,3).cycle(), 7)), [1,2,3,1,2,3,1]) ``` ------------------------------------------------------------------------ ### takewhile ``` python def takewhile( f ): ``` *Same as `itertools.takewhile`* [`L.takewhile`](https://fastcore.fast.ai/foundation.html#l.takewhile) returns elements from the beginning of the list while the predicate is true: ``` python test_eq(L(1,2,3,4,5,1,2).takewhile(lambda x: x<4), [1,2,3]) test_eq(L(1,2,3,11).takewhile(lt(10)), [1,2,3]) ``` Curried [`L.takewhile`](https://fastcore.fast.ai/foundation.html#l.takewhile) returns a partial function that takes elements from the beginning of any iterable while the predicate holds. This is useful when mapping over nested collections—each inner collection gets truncated at the first failing element using the same predicate. ``` python nested = L([[1,2,5,3], [2,3,8,1], [9,1,2]]) nested.map(L.takewhile(lt(5))) ``` [[1, 2], [2, 3], []] ------------------------------------------------------------------------ ### dropwhile ``` python def dropwhile( f ): ``` *Same as `itertools.dropwhile`* [`L.dropwhile`](https://fastcore.fast.ai/foundation.html#l.dropwhile) skips elements from the beginning while the predicate is true, then returns the rest: ``` python test_eq(L(1,2,3,4,5,1,2).dropwhile(lt(4)), [4,5,1,2]) test_eq(L(1,2,3).dropwhile(lt(10)), []) ``` ------------------------------------------------------------------------ ### startakewhile ``` python def startakewhile( f ): ``` *Like `takewhile`, but unpacks elements as args to `f`* [`L.startakewhile`](https://fastcore.fast.ai/foundation.html#l.startakewhile) is like `takewhile`, but unpacks tuple elements as arguments to the predicate: ``` python test_eq(L((1,2),(2,3),(4,1),(5,6)).startakewhile(lambda a,b: asource ### L.pairwise ``` python def pairwise( ): ``` *Same as `itertools.pairwise`* [`L.pairwise`](https://fastcore.fast.ai/foundation.html#l.pairwise) returns consecutive overlapping pairs: ``` python test_eq(L(1,2,3,4).pairwise(), [(1,2),(2,3),(3,4)]) test_eq(L(list('abcd')).pairwise(), [('a','b'),('b','c'),('c','d')]) ``` ------------------------------------------------------------------------ source ### L.batched ``` python def batched( n ): ``` *Same as `itertools.batched` (but also works on older Python versions* [`L.batched`](https://fastcore.fast.ai/foundation.html#l.batched) splits into chunks of size `n`: ``` python test_eq(L(1,2,3,4,5).batched(2), [(1,2),(3,4),(5,)]) test_eq(L(list('abcdefg')).batched(3), [('a','b','c'),('d','e','f'),('g',)]) ``` ------------------------------------------------------------------------ source ### L.compress ``` python def compress( selectors ): ``` *Same as `itertools.compress`* [`L.compress`](https://fastcore.fast.ai/foundation.html#l.compress) filters elements using a boolean selector: ``` python test_eq(L(list('abcd')).compress([1,0,1,0]), ['a','c']) test_eq(L(1,2,3,4,5).compress([True,False,True,False,True]), [1,3,5]) ``` ------------------------------------------------------------------------ source ### L.permutations ``` python def permutations( r:NoneType=None ): ``` *Same as `itertools.permutations`* [`L.permutations`](https://fastcore.fast.ai/foundation.html#l.permutations) returns all permutations of length `r` (defaults to full length): ``` python test_eq(L(1,2,3).permutations(), [(1,2,3),(1,3,2),(2,1,3),(2,3,1),(3,1,2),(3,2,1)]) test_eq(L(list('abc')).permutations(2), [('a','b'),('a','c'),('b','a'),('b','c'),('c','a'),('c','b')]) ``` ------------------------------------------------------------------------ source ### L.combinations ``` python def combinations( r ): ``` *Same as `itertools.combinations`* [`L.combinations`](https://fastcore.fast.ai/foundation.html#l.combinations) returns all combinations of length `r`: ``` python test_eq(L(1,2,3,4).combinations(2), [(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)]) test_eq(L(list('abcd')).combinations(3), [('a','b','c'),('a','b','d'),('a','c','d'),('b','c','d')]) ``` ------------------------------------------------------------------------ source ### partition ``` python def partition( f:function=noop, kwargs:VAR_KEYWORD ): ``` *Split into two [`L`](https://fastcore.fast.ai/foundation.html#l)s based on predicate `f`: (true_items, false_items)* [`L.partition`](https://fastcore.fast.ai/foundation.html#l.partition) splits a list into two [`L`](https://fastcore.fast.ai/foundation.html#l)s based on a predicate—items where `f` returns true, and items where it returns false: ``` python t,f = L(1,2,3,4,5,6).partition(lambda x: x%2==0) test_eq(t, [2,4,6]) test_eq(f, [1,3,5]) evens,odds = L.range(10).partition(lambda x: x%2==0) test_eq(evens, [0,2,4,6,8]) test_eq(odds, [1,3,5,7,9]) ``` Curried [`L.partition`](https://fastcore.fast.ai/foundation.html#l.partition) returns a partial function that splits any iterable into two [`L`](https://fastcore.fast.ai/foundation.html#l)s based on a predicate. This is useful when mapping over nested collections—each inner collection gets partitioned independently using the same predicate, returning a tuple of (true_items, false_items) for each. ``` python nested = L([[1,2,3,4,5], [10,15,20,25], [3,6,9]]) nested.map(L.partition(gt(5))) ``` [([], [1, 2, 3, 4, 5]), ([10, 15, 20, 25], []), ([6, 9], [3])] ------------------------------------------------------------------------ ### starpartition ``` python def starpartition( f, kwargs:VAR_KEYWORD ): ``` *Like [`partition`](https://fastcore.fast.ai/basics.html#partition), but unpacks elements as args to `f`* [`L.starpartition`](https://fastcore.fast.ai/foundation.html#l.starpartition) is like [`partition`](https://fastcore.fast.ai/basics.html#partition), but unpacks tuple elements as arguments to the predicate: ``` python asc,desc = L((1,2),(3,1),(2,4),(5,3)).starpartition(lt) test_eq(asc, [(1,2),(2,4)]) # a < b test_eq(desc, [(3,1),(5,3)]) # a >= b ``` ------------------------------------------------------------------------ ### rstarpartition ``` python def rstarpartition( f, kwargs:VAR_KEYWORD ): ``` *Like `starpartition`, but reverse the order of args* [`L.rstarpartition`](https://fastcore.fast.ai/foundation.html#l.rstarpartition) is like `starpartition`, but reverses the order of unpacked arguments: ``` python asc,desc = L((2,1),(1,3),(4,2),(3,5)).rstarpartition(lt) test_eq(asc, [(2,1),(4,2)]) # b < a (i.e., 1<2, 2<4) test_eq(desc, [(1,3),(3,5)]) # b >= a ``` ------------------------------------------------------------------------ source ### L.flatten ``` python def flatten( ): ``` *Recursively flatten nested iterables (except strings)* [`L.flatten`](https://fastcore.fast.ai/foundation.html#l.flatten) recursively flattens nested iterables into a single [`L`](https://fastcore.fast.ai/foundation.html#l). Strings are treated as atomic (not iterated over): ``` python test_eq(L([[1,2],[3,[4,5]]]).flatten(), [1,2,3,4,5]) test_eq(L([1,[2,[3,[4]]]]).flatten(), [1,2,3,4]) test_eq(L(['a',['b','c'],'d']).flatten(), ['a','b','c','d']) # strings not flattened test_eq(L([1,2,3]).flatten(), [1,2,3]) # already flat ``` ## Config ------------------------------------------------------------------------ source ### save_config_file ``` python def save_config_file( file, d, kwargs:VAR_KEYWORD ): ``` *Write settings dict to a new config file, or overwrite the existing one.* ------------------------------------------------------------------------ source ### read_config_file ``` python def read_config_file( file, kwargs:VAR_KEYWORD ): ``` Config files are saved and read using Python’s `configparser.ConfigParser`, inside the `DEFAULT` section. ``` python _d = dict(user='fastai', lib_name='fastcore', some_path='test', some_bool=True, some_num=3) try: save_config_file('tmp.ini', _d) res = read_config_file('tmp.ini') finally: os.unlink('tmp.ini') dict(res) ``` {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} ------------------------------------------------------------------------ source ### find_file_parents ``` python def find_file_parents( fname, frompath:NoneType=None ): ``` *Search `cfg_path` and its parents to find `cfg_name`* ``` python save_config_file('../tmp.ini', _d) try: found = find_file_parents('tmp.ini') finally: os.unlink('../tmp.ini') found ``` Path('/Users/jhoward/aai-ws/fastcore') ------------------------------------------------------------------------ source ### Config ``` python def Config( cfg_path, cfg_name, create:NoneType=None, save:bool=True, extra_files:NoneType=None, types:NoneType=None, cfg_kwargs:VAR_KEYWORD ): ``` *Reading and writing `ConfigParser` ini files* [`Config`](https://fastcore.fast.ai/foundation.html#config) is a convenient wrapper around `ConfigParser` ini files with a single section (`DEFAULT`). Instantiate a [`Config`](https://fastcore.fast.ai/foundation.html#config) from an ini file at `cfg_path/cfg_name`: ``` python save_config_file('../tmp.ini', _d) try: cfg = Config('..', 'tmp.ini') finally: os.unlink('../tmp.ini') cfg ``` {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} You can create a new file if one doesn’t exist by providing a [`create`](https://fastcore.fast.ai/tools.html#create) dict: ``` python try: cfg = Config('..', 'tmp.ini', create=_d) finally: os.unlink('../tmp.ini') cfg ``` {'user': 'fastai', 'lib_name': 'fastcore', 'some_path': 'test', 'some_bool': 'True', 'some_num': '3'} If you additionally pass `save=False`, the [`Config`](https://fastcore.fast.ai/foundation.html#config) will contain the items from [`create`](https://fastcore.fast.ai/tools.html#create) without writing a new file: ``` python cfg = Config('..', 'tmp.ini', create=_d, save=False) test_eq(cfg.user,'fastai') assert not Path('../tmp.ini').exists() ``` You can also pass in `ConfigParser` `kwargs` to change the behavior of how your configuration file will be parsed. For example, by default, inline comments are not handled by [`Config`](https://fastcore.fast.ai/foundation.html#config). However, if you pass in the `inline_comment_prefixes` with whatever your comment symbol is, you’ll overwrite this behavior. ``` python # Create a complete example config file with comments cfg_str = """\ [DEFAULT] user = fastai # inline comment # Library configuration lib_name = fastcore # Paths some_path = test # Feature flags some_bool = True # Numeric settings some_num = # missing value """ with open('../tmp.ini', 'w') as f: f.write(cfg_str) ``` ``` python # Now read it back to verify try: cfg = Config('..', 'tmp.ini', inline_comment_prefixes=('#')) finally: os.unlink('../tmp.ini') test_eq(cfg.user,'fastai') test_eq(cfg.some_num,'') ``` ------------------------------------------------------------------------ source ### Config.get ``` python def get( k, default:NoneType=None ): ``` Keys can be accessed as attributes, items, or with `get` and an optional default: ``` python test_eq(cfg.user,'fastai') test_eq(cfg['some_path'], 'test') test_eq(cfg.get('foo','bar'),'bar') ``` Extra files can be read *before* `cfg_path/cfg_name` using `extra_files`, in the order they appear: ``` python with tempfile.TemporaryDirectory() as d: a = Config(d, 'a.ini', {'a':0,'b':0}) b = Config(d, 'b.ini', {'a':1,'c':0}) c = Config(d, 'c.ini', {'a':2,'d':0}, extra_files=[a.config_file,b.config_file]) test_eq(c.d, {'a':'2','b':'0','c':'0','d':'0'}) ``` If you pass a dict `types`, then the values of that dict will be used as types to instantiate all values returned. `Path` is a special case – in that case, the path returned will be relative to the path containing the config file (assuming the value is relative). `bool` types use [`str2bool`](https://fastcore.fast.ai/basics.html#str2bool) to convert to boolean. ``` python _types = dict(some_path=Path, some_bool=bool, some_num=int) cfg = Config('..', 'tmp.ini', create=_d, save=False, types=_types) test_eq(cfg.user,'fastai') test_eq(cfg['some_path'].resolve(), (Path('..')/'test').resolve()) test_eq(cfg.get('some_num'), 3) ``` ------------------------------------------------------------------------ source ### Config.find ``` python def find( cfg_name, cfg_path:NoneType=None, kwargs:VAR_KEYWORD ): ``` *Search `cfg_path` and its parents to find `cfg_name`* You can use [`Config.find`](https://fastcore.fast.ai/foundation.html#config.find) to search subdirectories for a config file, starting in the current path if no path is specified: --- # Source: https://fastcore.fast.ai/meta.html.md # Meta ``` python from fastcore.foundation import * from nbdev.showdoc import * from fastcore.nb_imports import * from fastcore.test import * ``` See this [blog post](https://realpython.com/python-metaclasses/) for more information about metaclasses. - [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) preserves information that enables [intropsection of signatures](https://www.python.org/dev/peps/pep-0362/#:~:text=Python%20has%20always%20supported%20powerful,fully%20reconstruct%20the%20function's%20signature.) (i.e. tab completion in IDEs) when certain types of inheritence would otherwise obfuscate this introspection. - [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) ensures that the classes defined with it run `__pre_init__` and `__post_init__` (without having to write `self.__pre_init__()` and `self.__post_init__()` in the actual `init` - [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) gives the [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) functionality and ensures classes defined with it don’t re-create an object of their type whenever it’s passed to the constructor - [`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) ensures classes defined with it can easily be casted form objects they subclass. ------------------------------------------------------------------------ source ### test_sig ``` python def test_sig( f, b ): ``` *Test the signature of an object* ``` python def func_1(h,i,j): pass def func_2(h,i=3, j=[5,6]): pass class T: def __init__(self, a, b): pass test_sig(func_1, '(h, i, j)') test_sig(func_2, '(h, i=3, j=[5, 6])') test_sig(T, '(a, b)') ``` ------------------------------------------------------------------------ source ### FixSigMeta ``` python def FixSigMeta( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *A metaclass that fixes the signature on classes that override `__new__`* When you inherit from a class that defines `__new__`, or a metaclass that defines `__call__`, the signature of your `__init__` method is obfuscated such that tab completion no longer works. [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) fixes this issue and restores signatures. To understand what [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) does, it is useful to inspect an object’s signature. You can inspect the signature of an object with `inspect.signature`: ``` python class T: def __init__(self, a, b, c): pass inspect.signature(T) ``` This corresponds to tab completion working in the normal way: Tab completion in a Jupyter Notebook. However, when you inherhit from a class that defines `__new__` or a metaclass that defines `__call__` this obfuscates the signature by overriding your class with the signature of `__new__`, which prevents tab completion from displaying useful information: ``` python class Foo: def __new__(self, **args): pass class Bar(Foo): def __init__(self, d, e, f): pass inspect.signature(Bar) ``` Tab completion in a Jupyter Notebook. Finally, the signature and tab completion can be restored by inheriting from the metaclass [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) as shown below: ``` python class Bar(Foo, metaclass=FixSigMeta): def __init__(self, d, e, f): pass test_sig(Bar, '(d, e, f)') inspect.signature(Bar) ``` Tab completion in a Jupyter Notebook. If you need to define a metaclass that overrides `__call__` (as done in [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta)), you need to inherit from [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) instead of `type` when constructing the metaclass to preserve the signature in `__init__`. Be careful not to override `__new__` when doing this: ``` python class TestMeta(FixSigMeta): # __new__ comes from FixSigMeta def __call__(cls, *args, **kwargs): pass class T(metaclass=TestMeta): def __init__(self, a, b): pass test_sig(T, '(a, b)') ``` On the other hand, if you fail to inherit from [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) when inheriting from a metaclass that overrides `__call__`, your signature will reflect that of `__call__` instead (which is often undesirable): ``` python class GenericMeta(type): "A boilerplate metaclass that doesn't do anything for testing." def __new__(cls, name, bases, dict): return super().__new__(cls, name, bases, dict) def __call__(cls, *args, **kwargs): pass class T2(metaclass=GenericMeta): def __init__(self, a, b): pass # We can avoid this by inheriting from the metaclass `FixSigMeta` test_sig(T2, '(*args, **kwargs)') ``` ------------------------------------------------------------------------ source ### PrePostInitMeta ``` python def PrePostInitMeta( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *A metaclass that calls optional `__pre_init__` and `__post_init__` methods* `__pre_init__` and `__post_init__` are useful for initializing variables or performing tasks prior to or after `__init__` being called, respectively. Fore example: ``` python class _T(metaclass=PrePostInitMeta): def __pre_init__(self): self.a = 0; def __init__(self,b=0): self.b = self.a + 1; assert self.b==1 def __post_init__(self): self.c = self.b + 2; assert self.c==3 t = _T() test_eq(t.a, 0) # set with __pre_init__ test_eq(t.b, 1) # set with __init__ test_eq(t.c, 3) # set with __post_init__ ``` One use for [`PrePostInitMeta`](https://fastcore.fast.ai/meta.html#prepostinitmeta) is avoiding the `__super__().__init__()` boilerplate associated with subclassing, such as used in [`AutoInit`](https://fastcore.fast.ai/meta.html#autoinit). ------------------------------------------------------------------------ source ### AutoInit ``` python def AutoInit( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Same as `object`, but no need for subclasses to call `super().__init__`* This is normally used as a [mixin](https://www.residentmar.io/2019/07/07/python-mixins.html), eg: ``` python class TestParent(): def __init__(self): self.h = 10 class TestChild(AutoInit, TestParent): def __init__(self): self.k = self.h + 2 t = TestChild() test_eq(t.h, 10) # h=10 is initialized in the parent class test_eq(t.k, 12) ``` ------------------------------------------------------------------------ source ### NewChkMeta ``` python def NewChkMeta( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Metaclass to avoid recreating object passed to constructor* [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) is used when an object of the same type is the first argument to your class’s constructor (i.e. the `__init__` function), and you would rather it not create a new object but point to the same exact object. This is used in [`L`](https://fastcore.fast.ai/foundation.html#l), for example, to avoid creating a new object when the object is already of type [`L`](https://fastcore.fast.ai/foundation.html#l). This allows the users to defenisvely instantiate an [`L`](https://fastcore.fast.ai/foundation.html#l) object and just return a reference to the same object if it already happens to be of type [`L`](https://fastcore.fast.ai/foundation.html#l). For example, the below class `_T` **optionally** accepts an object `o` as its first argument. A new object is returned upon instantiation per usual: ``` python class _T(): "Testing" def __init__(self, o): # if `o` is not an object without an attribute `foo`, set foo = 1 self.foo = getattr(o,'foo',1) ``` ``` python t = _T(3) test_eq(t.foo,1) # 1 was not of type _T, so foo = 1 t2 = _T(t) #t1 is of type _T assert t is not t2 # t1 and t2 are different objects ``` However, if we want `_T` to return a reference to the same object when passed an an object of type `_T` we can inherit from the [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) class as illustrated below: ``` python class _T(metaclass=NewChkMeta): "Testing with metaclass NewChkMeta" def __init__(self, o=None, b=1): # if `o` is not an object without an attribute `foo`, set foo = 1 self.foo = getattr(o,'foo',1) self.b = b ``` We can now test `t` and `t2` are now pointing at the same object when using this new definition of `_T`: ``` python t = _T(3) test_eq(t.foo,1) # 1 was not of type _T, so foo = 1 t2 = _T(t) # t2 will now reference t test_is(t, t2) # t and t2 are the same object t2.foo = 5 # this will also change t.foo to 5 because it is the same object test_eq(t.foo, 5) test_eq(t2.foo, 5) ``` However, there is one exception to how [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) works. **If you pass any additional arguments in the constructor a new object is returned**, even if the first object is of the same type. For example, consider the below example where we pass the additional argument `b` into the constructor: ``` python t3 = _T(t, b=1) assert t3 is not t t4 = _T(t) # without any arguments the constructor will return a reference to the same object assert t4 is t ``` Finally, it should be noted that [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta) as well as all other metaclases in this section, inherit from [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta). This means class signatures will always be preserved when inheriting from this metaclass (see docs for [`FixSigMeta`](https://fastcore.fast.ai/meta.html#fixsigmeta) for more details): ``` python test_sig(_T, '(o=None, b=1)') ``` ------------------------------------------------------------------------ source ### BypassNewMeta ``` python def BypassNewMeta( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Metaclass: casts `x` to this class if it’s of type `cls._bypass_type`* [`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) is identical to [`NewChkMeta`](https://fastcore.fast.ai/meta.html#newchkmeta), except for checking for a class as the same type, we instead check for a class of type specified in attribute `_bypass_type`. In NewChkMeta, objects of the same type passed to the constructor (without arguments) would result into a new variable referencing the same object. However, with [`BypassNewMeta`](https://fastcore.fast.ai/meta.html#bypassnewmeta) this only occurs if the type matches the `_bypass_type` of the class you are defining: ``` python class _TestA: pass class _TestB: pass class _T(_TestA, metaclass=BypassNewMeta): _bypass_type=_TestB def __init__(self,x): self.x=x ``` In the below example, `t` does not refer to `t2` because `t` is of type `_TestA` while `_T._bypass_type` is of type `TestB`: ``` python t = _TestA() t2 = _T(t) assert t is not t2 ``` However, if `t` is set to `_TestB` to match `_T._bypass_type`, then both `t` and `t2` will refer to the same object. ``` python t = _TestB() t2 = _T(t) t2.new_attr = 15 test_is(t, t2) # since t2 just references t these will be the same test_eq(t.new_attr, t2.new_attr) # likewise, chaning an attribute on t will also affect t2 because they both point to the same object. t.new_attr = 9 test_eq(t2.new_attr, 9) ``` ## Metaprogramming ------------------------------------------------------------------------ source ### empty2none ``` python def empty2none( p ): ``` *Replace `Parameter.empty` with `None`* ------------------------------------------------------------------------ source ### anno_dict ``` python def anno_dict( f ): ``` *`__annotation__ dictionary with`empty`cast to`None\`, returning empty if doesn’t exist* ``` python def _f(a:int, b:L)->str: ... test_eq(anno_dict(_f), {'a': int, 'b': L, 'return': str}) ``` ------------------------------------------------------------------------ source ### use_kwargs_dict ``` python def use_kwargs_dict( keep:bool=False, kwargs:VAR_KEYWORD ): ``` *Decorator: replace `**kwargs` in signature with `names` params* Replace all `**kwargs` with named arguments like so: ``` python @use_kwargs_dict(y=1,z=None) def foo(a, b=1, **kwargs): pass test_sig(foo, '(a, b=1, *, y=1, z=None)') ``` Add named arguments, but optionally keep `**kwargs` by setting `keep=True`: ``` python @use_kwargs_dict(y=1,z=None, keep=True) def foo(a, b=1, **kwargs): pass test_sig(foo, '(a, b=1, *, y=1, z=None, **kwargs)') ``` ------------------------------------------------------------------------ source ### use_kwargs ``` python def use_kwargs( names, keep:bool=False ): ``` *Decorator: replace `**kwargs` in signature with `names` params* [`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) is different than [`use_kwargs_dict`](https://fastcore.fast.ai/meta.html#use_kwargs_dict) as it only replaces `**kwargs` with named parameters without any default values: ``` python @use_kwargs(['y', 'z']) def foo(a, b=1, **kwargs): pass test_sig(foo, '(a, b=1, *, y=None, z=None)') ``` You may optionally keep the `**kwargs` argument in your signature by setting `keep=True`: ``` python @use_kwargs(['y', 'z'], keep=True) def foo(a, *args, b=1, **kwargs): pass test_sig(foo, '(a, *args, b=1, y=None, z=None, **kwargs)') ``` ``` python from fastcore.basics import * ``` ``` python listify('a') ``` ['a'] ------------------------------------------------------------------------ source ### delegates ``` python def delegates( to:function=None, # Delegatee keep:bool=False, # Keep `kwargs` in decorated function? but:list=None, # Exclude these parameters from signature sort_args:bool=False, # Sort arguments alphabetically, doesn't work with call_parse ): ``` *Decorator: replace `**kwargs` in signature with params from `to`* ``` python def baz(a, b:int=2, c:int=3): return a + b + c def foo(c, a, **kwargs): return c + baz(a, **kwargs) assert foo(c=1, a=1) == 7 ``` The problem with this approach is the api for `foo` is obfuscated. Users cannot introspect what the valid arguments for `**kwargs` are without reading the source code. When a user tries tries to introspect the signature of `foo`, they are presented with this: ``` python inspect.signature(foo) ``` We can address this issue by using the decorator [`delegates`](https://fastcore.fast.ai/meta.html#delegates) to include parameters from other functions. For example, if we apply the [`delegates`](https://fastcore.fast.ai/meta.html#delegates) decorator to `foo` to include parameters from `baz`: ``` python @delegates(baz) def foo(c, a, **kwargs): return c + baz(a, **kwargs) test_sig(foo, '(c, a, *, b: int = 2)') inspect.signature(foo) ``` We can optionally decide to keep `**kwargs` by setting `keep=True`: ``` python @delegates(baz, keep=True) def foo(c, a, **kwargs): return c + baz(a, **kwargs) inspect.signature(foo) ``` It is important to note that **only parameters with default parameters are included**. For example, in the below scenario only `c`, but NOT `e` and `d` are included in the signature of `foo` after applying [`delegates`](https://fastcore.fast.ai/meta.html#delegates): ``` python def basefoo(e, d, c=2): pass @delegates(basefoo) def foo(a, b=1, **kwargs): pass inspect.signature(foo) # e and d are not included b/c they don't have default parameters. ``` The reason that required arguments (i.e. those without default parameters) are automatically excluded is that you should be explicitly implementing required arguments into your function’s signature rather than relying on [`delegates`](https://fastcore.fast.ai/meta.html#delegates). Additionally, you can exclude specific parameters from being included in the signature with the `but` parameter. In the example below, we exclude the parameter `d`: ``` python def basefoo(e, c=2, d=3): pass @delegates(basefoo, but= ['d']) def foo(a, b=1, **kwargs): pass test_sig(foo, '(a, b=1, *, c=2)') inspect.signature(foo) ``` You can also use [`delegates`](https://fastcore.fast.ai/meta.html#delegates) between methods in a class. Here is an example of [`delegates`](https://fastcore.fast.ai/meta.html#delegates) with class methods: ``` python # example 1: class methods class _T(): @classmethod def foo(cls, a=1, b=2): pass @classmethod @delegates(foo) def bar(cls, c=3, **kwargs): pass test_sig(_T.bar, '(c=3, *, a=1, b=2)') ``` Here is the same example with instance methods: ``` python # example 2: instance methods class _T(): def foo(self, a=1, b=2): pass @delegates(foo) def bar(self, c=3, **kwargs): pass t = _T() test_sig(t.bar, '(c=3, *, a=1, b=2)') ``` You can also delegate between classes. By default, the [`delegates`](https://fastcore.fast.ai/meta.html#delegates) decorator will delegate to the superclass: ``` python class BaseFoo: def __init__(self, e, c=2): pass @delegates()# since no argument was passsed here we delegate to the superclass class Foo(BaseFoo): def __init__(self, a, b=1, **kwargs): super().__init__(**kwargs) test_sig(Foo, '(a, b=1, *, c=2)') ``` ------------------------------------------------------------------------ source ### method ``` python def method( f ): ``` *Mark `f` as a method* The [`method`](https://fastcore.fast.ai/meta.html#method) function is used to change a function’s type to a method. In the below example we change the type of `a` from a function to a method: ``` python def a(x=2): return x + 1 assert type(a).__name__ == 'function' a = method(a) assert type(a).__name__ == 'method' ``` You can also sort the arguments by setting the `sort_args` parameter to `True`. Here’s a function with arguments not in alphabetical order. ``` python def unsortedfunc(c=3,a=1,b=2): pass unsortedfunc ``` We can sort them using the `sort_args` parameter: ``` python @delegates(unsortedfunc, sort_args=True) def sortedfunc(**kwargs): pass test_sig(sortedfunc, '(*, a=1, b=2, c=3)') sortedfunc ``` ------------------------------------------------------------------------ source ### funcs_kwargs ``` python def funcs_kwargs( as_method:bool=False ): ``` *Replace methods in `cls._methods` with those from `kwargs`* The `func_kwargs` decorator allows you to add a list of functions or methods to an existing class. You must set this list as a class attribute named `_methods` when defining your class. Additionally, you must incldue the `**kwargs` argument in the `___init__` method of your class. After defining your class this way, you can add functions to your class upon instantation as illusrated below. For example, we define class `T` to allow adding the function `b` to class `T` as follows (note that this function is stored as an attribute of `T` and doesn’t have access to `cls` or `self`): ``` python @funcs_kwargs class T: _methods=['b'] # allows you to add method b upon instantiation def __init__(self, f=1, **kwargs): pass # don't forget to include **kwargs in __init__ def a(self): return 1 def b(self): return 2 t = T() test_eq(t.a(), 1) test_eq(t.b(), 2) ``` Because we defined the class `T` this way, the signature of `T` indicates the option to add the function or method(s) specified in `_methods`. In this example, `b` is added to the signature: ``` python test_sig(T, '(f=1, *, b=None)') inspect.signature(T) ``` You can now add the function `b` to class `T` upon instantiation: ``` python def _new_func(): return 5 t = T(b = _new_func) test_eq(t.b(), 5) ``` If you try to add a function with a name not listed in `_methods` it will be ignored. In the below example, the attempt to add a function named `a` is ignored: ``` python t = T(a = lambda:3) test_eq(t.a(), 1) # the attempt to add a is ignored and uses the original method instead. ``` Note that you can also add methods not defined in the original class as long it is specified in the `_methods` attribute: ``` python @funcs_kwargs class T: _methods=['c'] def __init__(self, f=1, **kwargs): pass t = T(c = lambda: 4) test_eq(t.c(), 4) ``` Until now, these examples showed how to add functions stored as an instance attribute without access to `self`. However, if you need access to `self` you can set `as_method=True` in the `func_kwargs` decorator to add a method instead: ``` python def _f(self,a=1): return self.num + a # access the num attribute from the instance @funcs_kwargs(as_method=True) class T: _methods=['b'] num = 5 t = T(b = _f) # adds method b test_eq(t.b(5), 10) # self.num + 5 = 10 ``` Here is an example of how you might use this functionality with inheritence: ``` python def _f(self,a=1): return self.num * a #multiply instead of add class T2(T): def __init__(self,num): super().__init__(b = _f) # add method b from the super class self.num=num t = T2(num=3) test_eq(t.b(a=5), 15) # 3 * 5 = 15 test_sig(T2, '(num)') ``` ------------------------------------------------------------------------ source ### splice_sig ``` python def splice_sig( wrapper, fn, skips:VAR_POSITIONAL ): ``` *Replace `*args`/`**kwargs` in wrapper’s sig with fn’s params (minus skips)* [`splice_sig`](https://fastcore.fast.ai/meta.html#splice_sig) is a utility for building decorators that need to “wrap” a function while adding their own parameters. It takes a wrapper function with `*args`/`**kwargs` placeholders and splices in the wrapped function’s parameters at that position. #### The Problem When writing decorators that add context parameters (like `id`, `dname`, `log_changed`), you face awkward choices: 1. **Hardcode the signature** — brittle, doesn’t adapt to decorated functions 2. **Manual signature surgery** — verbose, error-prone, requires bookkeeping 3. **Separate “template” functions** — extra indirection, easy to desync #### The Solution Define your wrapper with `*args`/`**kwargs` at the splice point: ``` python def wrapper(id, *args, dname='', **kw): # ↑ pre ↑ splice ↑ post ``` Then call `splice_sig(wrapper, fn, skips)` to: 1. Take params **before** `*args` from the wrapper (“pre”) 2. Take params from `fn`, excluding `skips` (“middle”) 3. Take keyword-only params **after** `*args` from the wrapper (“post”) 4. Apply `wraps(fn)` to preserve metadata Key Points: - The wrapper’s `*args` marks where the inner function’s params get inserted - Params before `*args` become positional; keyword-only params after stay keyword-only - `skips` removes params from `fn` (typically the “data” param your wrapper provides) - `wraps(fn)` is applied automatically, preserving `__name__`, `__doc__`, etc. #### Examples *Simple logging decorator:* ``` python def logged(fn): "Add verbose flag that logs input/output" def wrapper(text, *args, verbose=False, **kw): result = fn(text, *args, **kw) if verbose: print(f'{fn.__name__}({text!r}) → {result!r}') return result return splice_sig(wrapper, fn, 'text') @logged def shout(text, times=1): "Uppercase and repeat" return text.upper() * times ``` ``` python shout('hi', 2, verbose=True) ``` shout('hi') → 'HIHI' 'HIHI' The signature combines the function and the wrapper: ``` python shout? ``` ``` python def shout( text, times:int=1, verbose:bool=False ): ``` Uppercase and repeat **File:** `/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_65408/3741654590.py` **Type:** function *API client wrapper:* ``` python def api_call(fn): "Decorator adding auth and retry params to API transform functions" def wrapper(endpoint, *args, auth_token=None, retries=3, **kw): for attempt in range(retries): resp = httpx.get(endpoint, headers={'Authorization': auth_token}) if resp.ok: break return fn(resp.json(), *args, **kw) return splice_sig(wrapper, fn, 'data',) @api_call def extract_ids(data, key='id'): "Get `key` value from items in `data['results']`" return [item[key] for item in data.get('results', [])] ``` ``` python extract_ids? ``` ``` python def extract_ids( endpoint, key:str='id', auth_token:NoneType=None, retries:int=3 ): ``` Get `key` value from items in `data['results']` **File:** `/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_65408/3604814096.py` **Type:** function --- # Source: https://fastcore.fast.ai/net.html.md # Network functionality ``` python from fastcore.test import * from nbdev.showdoc import * from fastcore.nb_imports import * ``` ## URLs ------------------------------------------------------------------------ source ### urlquote ``` python def urlquote( url ): ``` *Update url’s path with `urllib.parse.quote`* ``` python urlquote("https://github.com/fastai/fastai/compare/master@{1.day.ago}…master") ``` 'https://github.com/fastai/fastai/compare/master@%7B1.day.ago%7D%E2%80%A6master' ``` python urlquote("https://www.google.com/search?q=你好") ``` 'https://www.google.com/search?q=%E4%BD%A0%E5%A5%BD' ------------------------------------------------------------------------ source ### urlwrap ``` python def urlwrap( url, data:NoneType=None, headers:NoneType=None ): ``` *Wrap `url` in a urllib `Request` with [`urlquote`](https://fastcore.fast.ai/net.html#urlquote)* ------------------------------------------------------------------------ source #### HTTP4xxClientError ``` python def HTTP4xxClientError( url, code, msg, hdrs, fp ): ``` *Base class for client exceptions (code 4xx) from `url*` functions* ------------------------------------------------------------------------ source #### HTTP5xxServerError ``` python def HTTP5xxServerError( url, code, msg, hdrs, fp ): ``` *Base class for server exceptions (code 5xx) from `url*` functions* ------------------------------------------------------------------------ source ### urlopener ``` python def urlopener( ): ``` ------------------------------------------------------------------------ source ### urlopen ``` python def urlopen( url, data:NoneType=None, headers:NoneType=None, timeout:NoneType=None, kwargs:VAR_KEYWORD ): ``` *Like `urllib.request.urlopen`, but first [`urlwrap`](https://fastcore.fast.ai/net.html#urlwrap) the `url`, and encode `data`* With [`urlopen`](https://fastcore.fast.ai/net.html#urlopen), the body of the response will also be returned in addition to the message if there is an error: ``` python try: urlopen('https://api.github.com/v3') except HTTPError as e: print(e.code, e.msg) assert 'documentation_url' in e.msg ``` 404 Not Found ====Error Body==== { "message": "Not Found", "documentation_url": "https://docs.github.com/rest", "status": "404" } ------------------------------------------------------------------------ source ### urlread ``` python def urlread( url, data:NoneType=None, headers:NoneType=None, decode:bool=True, return_json:bool=False, return_headers:bool=False, timeout:NoneType=None, kwargs:VAR_KEYWORD ): ``` *Retrieve `url`, using `data` dict or `kwargs` to `POST` if present* ------------------------------------------------------------------------ source ### urljson ``` python def urljson( url, data:NoneType=None, headers:NoneType=None, timeout:NoneType=None ): ``` *Retrieve `url` and decode json* ``` python test_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent']) ``` ------------------------------------------------------------------------ source ### urlcheck ``` python def urlcheck( url, headers:NoneType=None, timeout:int=10 ): ``` ------------------------------------------------------------------------ source ### urlclean ``` python def urlclean( url ): ``` *Remove fragment, params, and querystring from `url` if present* ``` python test_eq(urlclean('http://a.com/b?c=1#d'), 'http://a.com/b') ``` ------------------------------------------------------------------------ source ### urlretrieve ``` python def urlretrieve( url, filename:NoneType=None, reporthook:NoneType=None, data:NoneType=None, headers:NoneType=None, timeout:NoneType=None ): ``` *Same as `urllib.request.urlretrieve` but also works with `Request` objects* ------------------------------------------------------------------------ source ### urldest ``` python def urldest( url, dest:NoneType=None ): ``` ------------------------------------------------------------------------ source ### urlsave ``` python def urlsave( url, dest:NoneType=None, reporthook:NoneType=None, headers:NoneType=None, timeout:NoneType=None ): ``` *Retrieve `url` and save based on its name* ``` python #skip with tempfile.TemporaryDirectory() as d: urlsave('http://www.google.com/index.html', d) ``` ------------------------------------------------------------------------ source ### urlvalid ``` python def urlvalid( x ): ``` *Test if `x` is a valid URL* ``` python assert urlvalid('http://www.google.com/') assert not urlvalid('www.google.com/') assert not urlvalid(1) ``` ------------------------------------------------------------------------ source ### urlrequest ``` python def urlrequest( url, verb, headers:NoneType=None, route:NoneType=None, query:NoneType=None, data:NoneType=None, json_data:bool=True ): ``` *`Request` for `url` with optional route params replaced by `route`, plus `query` string, and post `data`* ``` python hdr = {'Hdr1':'1', 'Hdr2':'2'} req = urlrequest('http://example.com/{foo}/1', 'POST', headers=hdr, route={'foo':'3'}, query={'q':'4'}, data={'d':'5'}) test_eq(req.headers, hdr) test_eq(req.full_url, 'http://example.com/3/1?q=4') test_eq(req.method, 'POST') test_eq(req.data, b'{"d": "5"}') ``` ``` python req = urlrequest('http://example.com/{foo}/1', 'POST', data={'d':'5','e':'6'}, headers=hdr, json_data=False) test_eq(req.data, b'd=5&e=6') ``` ------------------------------------------------------------------------ source ### Request.summary ``` python def summary( skip:NoneType=None )->dict: ``` *Summary containing full_url, headers, method, and data, removing `skip` from headers* ``` python req.summary(skip='Hdr1') ``` {'full_url': 'http://example.com/{foo}/1', 'method': 'POST', 'data': b'd=5&e=6', 'headers': {'Hdr2': '2'}} ------------------------------------------------------------------------ source ### urlsend ``` python def urlsend( url, verb, headers:NoneType=None, decode:bool=True, route:NoneType=None, query:NoneType=None, data:NoneType=None, json_data:bool=True, return_json:bool=True, return_headers:bool=False, debug:NoneType=None, timeout:NoneType=None ): ``` *Send request with [`urlrequest`](https://fastcore.fast.ai/net.html#urlrequest), converting result to json if `return_json`* ------------------------------------------------------------------------ source ### do_request ``` python def do_request( url, post:bool=False, headers:NoneType=None, data:VAR_KEYWORD ): ``` *Call GET or json-encoded POST on `url`, depending on `post`* ## Basic client/server ------------------------------------------------------------------------ source ### start_server ``` python def start_server( port, host:NoneType=None, dgram:bool=False, reuse_addr:bool=True, n_queue:NoneType=None ): ``` *Create a `socket` server on `port`, with optional `host`, of type `dgram`* You can create a TCP client and server pass an int as `port` and optional `host`. `host` defaults to your main network interface if not provided. You can create a Unix socket client and server by passing a string to `port`. A `SOCK_STREAM` socket is created by default, unless you pass `dgram=True`, in which case a `SOCK_DGRAM` socket is created. `n_queue` sets the listening queue size. ------------------------------------------------------------------------ source ### start_client ``` python def start_client( port, host:NoneType=None, dgram:bool=False ): ``` *Create a `socket` client on `port`, with optional `host`, of type `dgram`* ------------------------------------------------------------------------ source ### tobytes ``` python def tobytes( s:str )->bytes: ``` *Convert `s` into HTTP-ready bytes format* ``` python test_eq(tobytes('foo\nbar'), b'foo\r\nbar') ``` ------------------------------------------------------------------------ source ### http_response ``` python def http_response( body:NoneType=None, status:int=200, hdrs:NoneType=None, kwargs:VAR_KEYWORD ): ``` *Create an HTTP-ready response, adding `kwargs` to `hdrs`* ``` python exp = b'HTTP/1.1 200 OK\r\nUser-Agent: me\r\nContent-Length: 4\r\n\r\nbody' test_eq(http_response('body', 200, User_Agent='me'), exp) ``` ------------------------------------------------------------------------ source ### recv_once ``` python def recv_once( host:str='localhost', port:int=8000 ): ``` *Spawn a thread to receive a single HTTP request and store in `d['r']`* --- # Source: https://fastcore.fast.ai/parallel.html.md # Parallel ``` python from fastcore.test import * from nbdev.showdoc import * from fastcore.nb_imports import * ``` ------------------------------------------------------------------------ source ### threaded ``` python def threaded( process:bool=False ): ``` *Run `f` in a `Thread` (or `Process` if `process=True`), and returns it* ``` python @threaded def _1(): time.sleep(0.05) print("second") return 5 @threaded def _2(): time.sleep(0.01) print("first") a = _1() _2() time.sleep(0.1) ``` first second After the thread is complete, the return value is stored in the `result` attr. ``` python a.result ``` 5 ------------------------------------------------------------------------ source ### startthread ``` python def startthread( f ): ``` *Like [`threaded`](https://fastcore.fast.ai/parallel.html#threaded), but start thread immediately* ``` python @startthread def _(): time.sleep(0.05) print("second") @startthread def _(): time.sleep(0.01) print("first") time.sleep(0.1) ``` first second ------------------------------------------------------------------------ source ### startproc ``` python def startproc( f ): ``` *Like `threaded(True)`, but start Process immediately* ``` python @startproc def _(): time.sleep(0.05) print("second") @startproc def _(): time.sleep(0.01) print("first") time.sleep(0.1) ``` first second ------------------------------------------------------------------------ source ### parallelable ``` python def parallelable( param_name, num_workers, f:NoneType=None ): ``` ------------------------------------------------------------------------ source #### ThreadPoolExecutor ``` python def ThreadPoolExecutor( max_workers:int=4, on_exc:builtin_function_or_method=print, pause:int=0, kwargs:VAR_KEYWORD ): ``` *Same as Python’s ThreadPoolExecutor, except can pass `max_workers==0` for serial execution* ------------------------------------------------------------------------ source #### ProcessPoolExecutor ``` python def ProcessPoolExecutor( max_workers:int=4, on_exc:builtin_function_or_method=print, pause:int=0, mp_context:NoneType=None, initializer:NoneType=None, initargs:tuple=(), max_tasks_per_child:NoneType=None ): ``` *Same as Python’s ProcessPoolExecutor, except can pass `max_workers==0` for serial execution* ------------------------------------------------------------------------ source ### parallel ``` python def parallel( f, items, args:VAR_POSITIONAL, n_workers:int=4, total:NoneType=None, progress:NoneType=None, pause:int=0, method:NoneType=None, threadpool:bool=False, timeout:NoneType=None, chunksize:int=1, kwargs:VAR_KEYWORD ): ``` *Applies `func` in parallel to `items`, using `n_workers`* ``` python inp,exp = range(50),range(1,51) test_eq(parallel(_add_one, inp, n_workers=2), exp) test_eq(parallel(_add_one, inp, threadpool=True, n_workers=2), exp) test_eq(parallel(_add_one, inp, n_workers=1, a=2), range(2,52)) test_eq(parallel(_add_one, inp, n_workers=0), exp) test_eq(parallel(_add_one, inp, n_workers=0, a=2), range(2,52)) ``` Use the `pause` parameter to ensure a pause of `pause` seconds between processes starting. This is in case there are race conditions in starting some process, or to stagger the time each process starts, for example when making many requests to a webserver. Set `threadpool=True` to use [`ThreadPoolExecutor`](https://fastcore.fast.ai/parallel.html#threadpoolexecutor) instead of [`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor). ``` python from datetime import datetime ``` ``` python def print_time(i): time.sleep(random.random()/1000) print(i, datetime.now()) parallel(print_time, range(5), n_workers=2, pause=0.25); ``` 0 2026-01-29 10:59:46.704165 1 2026-01-29 10:59:46.954594 2 2026-01-29 10:59:47.204557 3 2026-01-29 10:59:47.455629 4 2026-01-29 10:59:47.707717 ------------------------------------------------------------------------ source ### parallel_async ``` python def parallel_async( f, items, args:VAR_POSITIONAL, n_workers:int=16, timeout:NoneType=None, chunksize:int=1, on_exc:builtin_function_or_method=print, kwargs:VAR_KEYWORD ): ``` *Applies `f` to `items` in parallel using asyncio and a semaphore to limit concurrency.* ``` python import asyncio ``` ``` python async def print_time_async(i): wait = random.random() await asyncio.sleep(wait) print(i, datetime.now(), wait) await parallel_async(print_time_async, range(6), n_workers=3); ``` 0 2026-01-29 10:59:48.007998 0.21615721710303581 3 2026-01-29 10:59:48.027639 0.018363443750668673 2 2026-01-29 10:59:48.487320 0.6952425451756079 1 2026-01-29 10:59:48.550808 0.7519131063729974 5 2026-01-29 10:59:48.703458 0.21434694863260173 4 2026-01-29 10:59:48.914600 0.8860713333269471 ------------------------------------------------------------------------ source ### run_procs ``` python def run_procs( f, f_done, args ): ``` *Call `f` for each item in `args` in parallel, yielding `f_done`* ------------------------------------------------------------------------ source ### parallel_gen ``` python def parallel_gen( cls, items, n_workers:int=4, kwargs:VAR_KEYWORD ): ``` *Instantiate `cls` in `n_workers` procs & call each on a subset of `items` in parallel.* ``` python # class _C: # def __call__(self, o): return ((i+1) for i in o) # items = range(5) # res = L(parallel_gen(_C, items, n_workers=0)) # idxs,dat1 = zip(*res.sorted(itemgetter(0))) # test_eq(dat1, range(1,6)) # res = L(parallel_gen(_C, items, n_workers=3)) # idxs,dat2 = zip(*res.sorted(itemgetter(0))) # test_eq(dat2, dat1) ``` `cls` is any class with `__call__`. It will be passed `args` and `kwargs` when initialized. Note that `n_workers` instances of `cls` are created, one in each process. `items` are then split in `n_workers` batches and one is sent to each `cls`. The function then returns a generator of tuples of item indices and results. ``` python class TestSleepyBatchFunc: "For testing parallel processes that run at different speeds" def __init__(self): self.a=1 def __call__(self, batch): for k in batch: time.sleep(random.random()/4) yield k+self.a x = np.linspace(0,0.99,20) res = L(parallel_gen(TestSleepyBatchFunc, x, n_workers=2)) test_eq(res.sorted().itemgot(1), x+1) ``` ``` python # #|hide # from subprocess import Popen, PIPE # # test num_workers > 0 in scripts works when python process start method is spawn # process = Popen(["python", "parallel_test.py"], stdout=PIPE) # _, err = process.communicate(timeout=10) # exit_code = process.wait() # test_eq(exit_code, 0) ``` --- # Source: https://fastcore.fast.ai/script.html.md # Script - CLI Part of [fast.ai](https://www.fast.ai)’s toolkit for delightful developer experiences. ## Overview Sometimes, you want to create a quick script, either for yourself, or for others. But in Python, that involves a whole lot of boilerplate and ceremony, especially if you want to support command line arguments, provide help, and other niceties. You can use [argparse](https://docs.python.org/3/library/argparse.html) for this purpose, which comes with Python, but it’s complex and verbose. `fastcore.script` makes life easier. There are much fancier modules to help you write scripts (we recommend [Python Fire](https://github.com/google/python-fire), and [Click](https://click.palletsprojects.com/en/7.x/) is also popular), but fastcore.script is very fast and very simple. In fact, it’s \<50 lines of code! Basically, it’s just a little wrapper around `argparse` that uses modern Python features and some thoughtful defaults to get rid of the boilerplate. For full details, see the [docs](https://fastcore.script.fast.ai) for `core`. ## Example Here’s a complete example (available in `examples/test_fastcore.py`): ``` python from fastcore.script import * @call_parse def main(msg:str, # The message upper:bool): # Convert to uppercase? "Print `msg`, optionally converting to uppercase" print(msg.upper() if upper else msg) ``` If you copy that info a file and run it, you’ll see: $ examples/test_fastcore.py --help usage: test_fastcore.py [-h] [--upper] msg Print `msg`, optionally converting to uppercase positional arguments: msg The message optional arguments: -h, --help show this help message and exit --upper Convert to uppercase? (default: False) As you see, we didn’t need any `if __name__ == "__main__"`, we didn’t have to parse arguments, we just wrote a function, added a decorator to it, and added some annotations to our function’s parameters. As a bonus, we can also use this function directly from a REPL such as Jupyter Notebook - it’s not just for command line scripts! You should provide a default (after the `=`) for any *optional* parameters. If you don’t provide a default for a parameter, then it will be a *positional* parameter.
> **Boolean Arguments Default to False** > > Arguments of type `bool` or > [`store_true`](https://fastcore.fast.ai/script.html#store_true) > default to `False` regardless of whether you provide a default or not. > Use [`bool_arg`](https://fastcore.fast.ai/script.html#bool_arg) as the > type instead of `bool` if you want to set a default value of True. For > example: > > ``` python > @call_parse > def main(msg:str="Hi", # The message > upper:bool_arg=True): # Convert to uppercase? > ```
## Param annotations If you want to use the full power of `argparse`, you can do so by using [`Param`](https://fastcore.fast.ai/script.html#param) annotations instead of type annotations and [docments](https://fastcore.fast.ai/docments.html), like so: ``` python from fastcore.script import * @call_parse def main(msg:Param("The message", str), upper:Param("Convert to uppercase?", store_true)): "Print `msg`, optionally converting to uppercase" print(msg.upper() if upper else msg) ``` If you use this approach, then each parameter in your function should have an annotation `Param(...)` (as in the example above). You can pass the following when calling [`Param`](https://fastcore.fast.ai/script.html#param): `help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` . Except for `opt`, all of these are just passed directly to `argparse`, so you have all the power of that module at your disposal. Generally you’ll want to pass at least `help` (since this is provided as the help string for that parameter) and `type` (to ensure that you get the type of data you expect). `opt` is a bool that defines whether a param is optional or required (positional) - but you’ll generally not need to set this manually, because fastcore.script will set it for you automatically based on *default* values. ## setuptools scripts There’s a really nice feature of pip/setuptools that lets you create commandline scripts directly from functions, makes them available in the `PATH`, and even makes your scripts cross-platform (e.g. in Windows it creates an exe). fastcore.script supports this feature too. The trick to making a function available as a script is to add a `console_scripts` section to your setup file, of the form: `script_name=module:function_name`. E.g. in this case we use: `test_fastcore.script=fastcore.script.test_cli:main`. With this, you can then just type `test_fastcore.script` at any time, from any directory, and your script will be called (once it’s installed using one of the methods below). You don’t actually have to write a `setup.py` yourself. Instead, just use [nbdev](https://nbdev.fast.ai). Then modify `settings.ini` as appropriate for your module/script. To install your script directly, you can type `pip install -e .`. Your script, when installed this way (it’s called an [editable install](http://codumentary.blogspot.com/2014/11/python-tip-of-year-pip-install-editable.html)), will automatically be up to date even if you edit it - there’s no need to reinstall it after editing. With nbdev you can even make your module and script available for installation directly from pip and conda by running `make release`. ## API details ------------------------------------------------------------------------ source ### store_true ``` python def store_true( ): ``` *Placeholder to pass to [`Param`](https://fastcore.fast.ai/script.html#param) for [`store_true`](https://fastcore.fast.ai/script.html#store_true) action* ------------------------------------------------------------------------ source ### store_false ``` python def store_false( ): ``` *Placeholder to pass to [`Param`](https://fastcore.fast.ai/script.html#param) for [`store_false`](https://fastcore.fast.ai/script.html#store_false) action* ------------------------------------------------------------------------ source ### bool_arg ``` python def bool_arg( v ): ``` *Use as `type` for [`Param`](https://fastcore.fast.ai/script.html#param) to get `bool` behavior* ------------------------------------------------------------------------ source ### clean_type_str ``` python def clean_type_str( x:str ): ``` ``` python class Test: pass test_eq(clean_type_str(argparse.ArgumentParser), 'argparse.ArgumentParser') test_eq(clean_type_str(Test), 'Test') test_eq(clean_type_str(int), 'int') test_eq(clean_type_str(float), 'float') test_eq(clean_type_str(store_false), 'store_false') ``` ------------------------------------------------------------------------ source ### Param ``` python def Param( help:str='', type:NoneType=None, opt:bool=True, action:NoneType=None, nargs:NoneType=None, const:NoneType=None, choices:NoneType=None, required:NoneType=None, default:NoneType=None, version:NoneType=None ): ``` *A parameter in a function used in [`anno_parser`](https://fastcore.fast.ai/script.html#anno_parser) or [`call_parse`](https://fastcore.fast.ai/script.html#call_parse)* ``` python test_eq(repr(Param("Help goes here")), '') test_eq(repr(Param("Help", int)), 'int ') test_eq(repr(Param(help=None, type=int)), 'int') test_eq(repr(Param(help=None, type=None)), '') ``` Each parameter in your function should have an annotation `Param(...)`. You can pass the following when calling [`Param`](https://fastcore.fast.ai/script.html#param): `help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required`, `version` (i.e. it takes the same parameters as `argparse.ArgumentParser.add_argument`, plus `opt`). Except for `opt`, all of these are just passed directly to `argparse`, so you have all the power of that module at your disposal. Generally you’ll want to pass at least `help` (since this is provided as the help string for that parameter) and `type` (to ensure that you get the type of data you expect). `opt` is a bool that defines whether a param is optional or required (positional) - but you’ll generally not need to set this manually, because fastcore.script will set it for you automatically based on *default* values. You should provide a default (after the `=`) for any *optional* parameters. If you don’t provide a default for a parameter, then it will be a *positional* parameter. Param’s `__repr__` also allows for more informative function annotation when looking up the function’s doc using shift+tab. You see the type annotation (if there is one) and the accompanying help documentation with it. ``` python def f(required:Param("Required param", int), a:Param("param 1", bool_arg), b:Param("param 2", str)="test"): "my docs" ... ``` ``` python help(f) ``` Help on function f in module __main__: f(required: int , a: bool_arg , b: str = 'test') my docs ``` python p = Param(help="help", type=int) p.set_default(1) test_eq(p.kwargs, {'help': 'help (default: 1)', 'type': int, 'default': 1}) ``` ------------------------------------------------------------------------ source ### anno_parser ``` python def anno_parser( func, prog:str=None ): ``` *Look at params (annotated with [`Param`](https://fastcore.fast.ai/script.html#param)) in func and return an `ArgumentParser`* This converts a function with parameter annotations of type [`Param`](https://fastcore.fast.ai/script.html#param) into an `argparse.ArgumentParser` object. Function arguments with a default provided are optional, and other arguments are positional. ``` python _en = str_enum('_en', 'aa','bb','cc') def f(required:Param("Required param", int), a:Param("param 1", bool_arg), v:Param("Print version", action='version', version='%(prog)s 2.0.0'), b:Param("param 2", str)="test", c:Param("param 3", _en)=_en.aa): "my docs" ... p = anno_parser(f, 'progname') p.print_help() ``` usage: progname [-h] [--v] [--b B] [--c {aa,bb,cc}] required a my docs positional arguments: required Required param a param 1 options: -h, --help show this help message and exit --v Print version --b B param 2 (default: test) --c {aa,bb,cc} param 3 (default: aa) We can also check the version and help flags are working. ``` python try: p.parse_args(['--v']) except: pass ``` progname 2.0.0 ``` python try: p.parse_args(['-h']) except: pass ``` usage: progname [-h] [--v] [--b B] [--c {aa,bb,cc}] required a my docs positional arguments: required Required param a param 1 options: -h, --help show this help message and exit --v Print version --b B param 2 (default: test) --c {aa,bb,cc} param 3 (default: aa) It also works with type annotations and docments: ``` python def g(required:int, # Required param a:bool_arg, # param 1 b="test", # param 2 c:_en=_en.aa): # param 3 "my docs" ... p = anno_parser(g, 'progname') p.print_help() ``` usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a my docs positional arguments: required Required param a param 1 options: -h, --help show this help message and exit --b B param 2 (default: test) --c {aa,bb,cc} param 3 (default: aa) It also works with Union types: ``` python def h(n:int|str, exts:str|list=None): "Test union types" p = anno_parser(h, 'test') p.print_help() ``` usage: test [-h] [--exts EXTS] n Test union types positional arguments: n options: -h, --help show this help message and exit --exts EXTS ``` python test_eq(p.parse_args(['42', '--exts', 'py']).n, 42) test_eq(p.parse_args(['hello']).n, 'hello') ``` Sometimes it’s convenient to extract arguments from the actual name of the called program. [`args_from_prog`](https://fastcore.fast.ai/script.html#args_from_prog) will do this, assuming that names and values of the params are separated by a `#`. Optionally there can also be a prefix separated by `##` (double underscore). ------------------------------------------------------------------------ source ### args_from_prog ``` python def args_from_prog( func, prog ): ``` *Extract args from `prog`* ``` python exp = {'a': False, 'b': 'baa'} test_eq(args_from_prog(f, 'foo##a#0#b#baa'), exp) test_eq(args_from_prog(f, 'a#0#b#baa'), exp) ``` ------------------------------------------------------------------------ source ### set_ctx ``` python def set_ctx( cv, val:bool=True ): ``` ------------------------------------------------------------------------ source ### call_parse ``` python def call_parse( func:NoneType=None, nested:bool=False ): ``` *Decorator to create a simple CLI from `func` using [`anno_parser`](https://fastcore.fast.ai/script.html#anno_parser)* ``` python @call_parse def test_add( a:int=0, # param a b:int=0 # param 1 ): "Add up `a` and `b`" return a + b ``` [`call_parse`](https://fastcore.fast.ai/script.html#call_parse) decorated functions work as regular functions and also as command-line interface functions. ``` python test_eq(test_add(1,2), 3) ``` This is the main way to use `fastcore.script`; decorate your function with [`call_parse`](https://fastcore.fast.ai/script.html#call_parse), add [`Param`](https://fastcore.fast.ai/script.html#param) annotations (as shown above) or type annotations and docments, and it can then be used as a script. Use the `nested` keyword argument to create nested parsers, where earlier parsers consume only their known args from `sys.argv` before later parsers are used. This is useful to create one command line application that executes another. For example: ``` sh myrunner --keyword 1 script.py -- ``` A separating `--` after the first application’s args is recommended though not always required, otherwise args may be parsed in unexpected ways. For example: ``` sh myrunner script.py -h ``` would display `myrunner`’s help and not `script.py`’s. --- # Source: https://fastcore.fast.ai/test.html.md # Test ## Simple test functions We can check that code raises an exception when that’s expected ([`test_fail`](https://fastcore.fast.ai/test.html#test_fail)). To test for equality or inequality (with different types of things) we define a simple function [`test`](https://fastcore.fast.ai/test.html#test) that compares two objects with a given `cmp` operator. ------------------------------------------------------------------------ source ### test_fail ``` python def test_fail( f, msg:str='', contains:str='', exc:type=Exception, args:NoneType=None, kwargs:NoneType=None ): ``` *Fails with `msg` unless `f()` raises an exception of type `exc` and (optionally) has `contains` in `e.args`* ``` python def _fail(): raise Exception("foobar") test_fail(_fail, contains="foo") def _fail(): raise Exception() test_fail(_fail) def _fail(): raise ValueError() test_fail(_fail, exc=ValueError) test_fail(lambda: test_fail(_fail, exc=IndexError), exc=AssertionError) ``` We can also pass `args` and `kwargs` to function to check if it fails with special inputs. ``` python def _fail_args(a): if a == 5: raise ValueError test_fail(_fail_args, args=(5,)) test_fail(_fail_args, kwargs=dict(a=5)) ``` ------------------------------------------------------------------------ source ### test ``` python def test( a, b, cmp, cname:NoneType=None ): ``` *`assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if it fails* ``` python test([1,2],[1,2], operator.eq) test_fail(lambda: test([1,2],[1], operator.eq)) test([1,2],[1], operator.ne) test_fail(lambda: test([1,2],[1,2], operator.ne)) ``` ------------------------------------------------------------------------ ### all_equal ``` python def all_equal( a, b ): ``` *Compares whether `a` and `b` are the same length and have the same contents* ``` python test(['abc'], ['abc'], all_equal) test_fail(lambda: test(['abc'],['cab'], all_equal)) ``` ------------------------------------------------------------------------ ### equals ``` python def equals( a, b ): ``` *Compares `a` and `b` for equality; supports sublists, tensors and arrays too* ``` python test([['abc'],['a']], [['abc'],['a']], equals) test([['abc'],['a'],'b', [['x']]], [['abc'],['a'],'b', [['x']]], equals) # supports any depth and nested structure ``` ------------------------------------------------------------------------ source ### nequals ``` python def nequals( a, b ): ``` *Compares `a` and `b` for `not equals`* ``` python test(['abc'], ['ab' ], nequals) ``` ## test_eq test_ne, etc… Just use [`test_eq`](https://fastcore.fast.ai/test.html#test_eq)/[`test_ne`](https://fastcore.fast.ai/test.html#test_ne) to test for `==`/`!=`. [`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type) checks things are equal and of the same type. We define them using [`test`](https://fastcore.fast.ai/test.html#test): ------------------------------------------------------------------------ source ### test_eq ``` python def test_eq( a, b ): ``` *[`test`](https://fastcore.fast.ai/test.html#test) that `a==b`* ``` python test_eq([1,2],[1,2]) test_eq([1,2],map(int,[1,2])) test_eq(array([1,2]),array([1,2])) test_eq(array([1,2]),array([1,2])) test_eq([array([1,2]),3],[array([1,2]),3]) test_eq(dict(a=1,b=2), dict(b=2,a=1)) test_fail(lambda: test_eq([1,2], 1), contains="==") test_fail(lambda: test_eq(None, np.array([1,2])), contains="==") test_eq({'a', 'b', 'c'}, {'c', 'a', 'b'}) ``` ``` python df1 = pd.DataFrame(dict(a=[1,2],b=['a','b'])) df2 = pd.DataFrame(dict(a=[1,2],b=['a','b'])) df3 = pd.DataFrame(dict(a=[1,2],b=['a','c'])) test_eq(df1,df2) test_eq(df1.a,df2.a) test_fail(lambda: test_eq(df1,df3), contains='==') class T(pd.Series): pass test_eq(df1.iloc[0], T(df2.iloc[0])) # works with subclasses ``` ``` python test_eq(torch.zeros(10), torch.zeros(10, dtype=torch.float64)) test_eq(torch.zeros(10), torch.ones(10)-1) test_fail(lambda:test_eq(torch.zeros(10), torch.ones(1, 10)), contains='==') test_eq(torch.zeros(3), [0,0,0]) ``` ------------------------------------------------------------------------ source ### test_eq_type ``` python def test_eq_type( a, b ): ``` *[`test`](https://fastcore.fast.ai/test.html#test) that `a==b` and are same type* ``` python test_eq_type(1,1) test_fail(lambda: test_eq_type(1,1.)) test_eq_type([1,1],[1,1]) test_fail(lambda: test_eq_type([1,1],(1,1))) test_fail(lambda: test_eq_type([1,1],[1,1.])) ``` ------------------------------------------------------------------------ source ### test_ne ``` python def test_ne( a, b ): ``` *[`test`](https://fastcore.fast.ai/test.html#test) that `a!=b`* ``` python test_ne([1,2],[1]) test_ne([1,2],[1,3]) test_ne(array([1,2]),array([1,1])) test_ne(array([1,2]),array([1,1])) test_ne([array([1,2]),3],[array([1,2])]) test_ne([3,4],array([3])) test_ne([3,4],array([3,5])) test_ne(dict(a=1,b=2), ['a', 'b']) test_ne(['a', 'b'], dict(a=1,b=2)) ``` ------------------------------------------------------------------------ source ### is_close ``` python def is_close( a, b, eps:float=1e-05 ): ``` *Is `a` within `eps` of `b`* ------------------------------------------------------------------------ source ### test_close ``` python def test_close( a, b, eps:float=1e-05 ): ``` *[`test`](https://fastcore.fast.ai/test.html#test) that `a` is within `eps` of `b`* ``` python test_close(1,1.001,eps=1e-2) test_fail(lambda: test_close(1,1.001)) test_close([-0.001,1.001], [0.,1.], eps=1e-2) test_close(np.array([-0.001,1.001]), np.array([0.,1.]), eps=1e-2) test_close(array([-0.001,1.001]), array([0.,1.]), eps=1e-2) ``` ------------------------------------------------------------------------ source ### test_is ``` python def test_is( a, b ): ``` *[`test`](https://fastcore.fast.ai/test.html#test) that `a is b`* ``` python test_fail(lambda: test_is([1], [1])) a = [1] test_is(a, a) b = [2]; test_fail(lambda: test_is(a, b)) ``` ------------------------------------------------------------------------ source ### test_shuffled ``` python def test_shuffled( a, b ): ``` *[`test`](https://fastcore.fast.ai/test.html#test) that `a` and `b` are shuffled versions of the same sequence of items* ``` python a = list(range(50)) b = copy(a) random.shuffle(b) test_shuffled(a,b) test_fail(lambda:test_shuffled(a,a)) ``` ``` python a = 'abc' b = 'abcabc' test_fail(lambda:test_shuffled(a,b)) ``` ``` python a = ['a', 42, True] b = [42, True, 'a'] test_shuffled(a,b) ``` ------------------------------------------------------------------------ source ### test_stdout ``` python def test_stdout( f, exp, regex:bool=False ): ``` *Test that `f` prints `exp` to stdout, optionally checking as `regex`* ``` python test_stdout(lambda: print('hi'), 'hi') test_fail(lambda: test_stdout(lambda: print('hi'), 'ho')) test_stdout(lambda: 1+1, '') test_stdout(lambda: print('hi there!'), r'^hi.*!$', regex=True) ``` ------------------------------------------------------------------------ source ### test_warns ``` python def test_warns( f, show:bool=False ): ``` ``` python test_warns(lambda: warnings.warn("Oh no!")) test_fail(lambda: test_warns(lambda: 2+2), contains='No warnings raised') ``` ``` python test_warns(lambda: warnings.warn("Oh no!"), show=True) ``` : Oh no! ``` python im = Image.open(TEST_IMAGE).resize((128,128)); im ``` ![](00_test_files/figure-commonmark/cell-35-output-1.png) ``` python im = Image.open(TEST_IMAGE_BW).resize((128,128)); im ``` ![](00_test_files/figure-commonmark/cell-36-output-1.png) ------------------------------------------------------------------------ source ### test_fig_exists ``` python def test_fig_exists( ax ): ``` *Test there is a figure displayed in `ax`* ``` python fig,ax = plt.subplots() ax.imshow(array(im)); ``` ![](00_test_files/figure-commonmark/cell-38-output-1.png) ``` python test_fig_exists(ax) ``` ------------------------------------------------------------------------ source ### ExceptionExpected ``` python def ExceptionExpected( ex:type=Exception, regex:str='' ): ``` *Context manager that tests if an exception is raised* ``` python def _tst_1(): assert False, "This is a test" def _tst_2(): raise SyntaxError with ExceptionExpected(): _tst_1() with ExceptionExpected(ex=AssertionError, regex="This is a test"): _tst_1() with ExceptionExpected(ex=SyntaxError): _tst_2() ``` `exception` is an abbreviation for [`ExceptionExpected()`](https://fastcore.fast.ai/test.html#exceptionexpected). ``` python with exception: _tst_1() ``` --- # Source: https://fastcore.fast.ai/tour.html.md # A tour of fastcore Here’s a (somewhat) quick tour of a few higlights from fastcore. ### Documentation All fast.ai projects, including this one, are built with [nbdev](https://nbdev.fast.ai), which is a full literate programming environment built on Jupyter Notebooks. That means that every piece of documentation, including the page you’re reading now, can be accessed as interactive Jupyter notebooks. In fact, you can even grab a link directly to a notebook running interactively on Google Colab - if you want to follow along with this tour, click the link below: ``` python colab_link('000_tour') ``` [Open `000_tour` in Colab](https://colab.research.google.com/github/AnswerDotAI/fastcore/blob/main/nbs/000_tour.ipynb) The full docs are available at [fastcore.fast.ai](https://fastcore.fast.ai). The code in the examples and in all fast.ai libraries follow the [fast.ai style guide](https://docs.fast.ai/dev/style.html). In order to support interactive programming, all fast.ai libraries are designed to allow for `import *` to be used safely, particular by ensuring that [`__all__`](https://riptutorial.com/python/example/2894/the---all---special-variable) is defined in all packages. In order to see where a function is from, just type it: ``` python coll_repr ``` For more details, including a link to the full documentation and source code, use `doc`, which pops up a window with this information: ``` python doc(coll_repr) ``` The documentation also contains links to any related functions or classes, which appear like this: [`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr) (in the notebook itself you will just see a word with back-ticks around it; the links are auto-generated in the documentation site). The documentation will generally show one or more examples of use, along with any background context necessary to understand them. As you’ll see, the examples for each function and method are shown as tests, rather than example outputs, so let’s start by explaining that. ### Testing fastcore’s testing module is designed to work well with [nbdev](https://nbdev.fast.ai), which is a full literate programming environment built on Jupyter Notebooks. That means that your tests, docs, and code all live together in the same notebook. fastcore and nbdev’s approach to testing starts with the premise that all your tests should pass. If one fails, no more tests in a notebook are run. Tests look like this: ``` python test_eq(coll_repr(range(1000), 5), '(#1000) [0, 1, 2, 3, 4...]') ``` That’s an example from the docs for [`coll_repr`](https://fastcore.fast.ai/foundation.html#coll_repr). As you see, it’s not showing you the output directly. Here’s what that would look like: ``` python coll_repr(range(1000), 5) ``` '(#1000) [0, 1, 2, 3, 4...]' So, the test is actually showing you what the output looks like, because if the function call didn’t return `'(#1000) [0,1,2,3,4...]'`, then the test would have failed. So every test shown in the docs is also showing you the behavior of the library — and vice versa! Test functions always start with `test_`, and then follow with the operation being tested. So [`test_eq`](https://fastcore.fast.ai/test.html#test_eq) tests for equality (as you saw in the example above). This includes tests for equality of arrays and tensors, lists and generators, and many more: ``` python test_eq([0,1,2,3], np.arange(4)) ``` When a test fails, it prints out information about what was expected: ``` python test_eq([0,1,2,3], np.arange(3)) ``` ---- AssertionError: ==: [0, 1, 2, 3] [0 1 2] If you want to check that objects are the same type, rather than the just contain the same collection, use [`test_eq_type`](https://fastcore.fast.ai/test.html#test_eq_type). You can test with any comparison function using [`test`](https://fastcore.fast.ai/test.html#test), e.g test whether an object is less than: ``` python test(2, 3, operator.lt) ``` You can even test that exceptions are raised: ``` python def divide_zero(): return 1/0 test_fail(divide_zero) ``` …and test that things are printed to stdout: ``` python test_stdout(lambda: print('hi'), 'hi') ``` ### Foundations fast.ai is unusual in that we often use [mixins](https://en.wikipedia.org/wiki/Mixin) in our code. Mixins are widely used in many programming languages, such as Ruby, but not so much in Python. We use mixins to attach new behavior to existing libraries, or to allow modules to add new behavior to our own classes, such as in extension modules. One useful example of a mixin we define is [`Path.ls`](https://fastcore.fast.ai/xtras.html#path.ls), which lists a directory and returns an [`L`](https://fastcore.fast.ai/foundation.html#l) (an extended list class which we’ll discuss shortly): ``` python p = Path('images') p.ls() ``` [Path('images/mnist3.png'), Path('images/att_00000.png'), Path('images/att_00005.png'), Path('images/att_00007.png'), Path('images/att_00006.png'), Path('images/puppy.jpg')] You can easily add you own mixins with the [`patch`](https://fastcore.fast.ai/basics.html#patch) [decorator](https://realpython.com/primer-on-python-decorators/), which takes advantage of Python 3 [function annotations](https://www.python.org/dev/peps/pep-3107/#parameters) to say what class to patch: ``` python @patch def num_items(self:Path): return len(self.ls()) p.num_items() ``` 6 We also use `**kwargs` frequently. In python `**kwargs` in a parameter like means “*put any additional keyword arguments into a dict called `kwargs`*”. Normally, using `kwargs` makes an API quite difficult to work with, because it breaks things like tab-completion and popup lists of signatures. `utils` provides [`use_kwargs`](https://fastcore.fast.ai/meta.html#use_kwargs) and [`delegates`](https://fastcore.fast.ai/meta.html#delegates) to avoid this problem. See our [detailed article on delegation](https://www.fast.ai/2019/08/06/delegation/) on this topic. [`GetAttr`](https://fastcore.fast.ai/basics.html#getattr) solves a similar problem (and is also discussed in the article linked above): it’s allows you to use Python’s exceptionally useful [`__getattr__`](https://fastcore.fast.ai/xml.html#__getattr__) magic method, but avoids the problem that normally in Python tab-completion and docs break when using this. For instance, you can see here that Python’s `dir` function, which is used to find the attributes of a python object, finds everything inside the `self.default` attribute here: ``` python class Author: def __init__(self, name): self.name = name class ProductPage(GetAttr): _default = 'author' def __init__(self,author,price,cost): self.author,self.price,self.cost = author,price,cost p = ProductPage(Author("Jeremy"), 1.50, 0.50) [o for o in dir(p) if not o.startswith('_')] ``` ['author', 'cost', 'name', 'price'] Looking at that `ProductPage` example, it’s rather verbose and duplicates a lot of attribute names, which can lead to bugs later if you change them only in one place. `fastcore` provides [`store_attr`](https://fastcore.fast.ai/basics.html#store_attr) to simplify this common pattern. It also provides [`basic_repr`](https://fastcore.fast.ai/basics.html#basic_repr) to give simple objects a useful `repr`: ``` python class ProductPage: def __init__(self,author,price,cost): store_attr() __repr__ = basic_repr('author,price,cost') ProductPage("Jeremy", 1.50, 0.50) ``` ProductPage(author='Jeremy', price=1.5, cost=0.5) One of the most interesting `fastcore` functions is the [`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) decorator. This allows class behavior to be modified without sub-classing. This can allow folks that aren’t familiar with object-oriented programming to customize your class more easily. Here’s an example of a class that uses [`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs): ``` python @funcs_kwargs class T: _methods=['some_method'] def __init__(self, **kwargs): assert not kwargs, f'Passed unknown args: {kwargs}' p = T(some_method = print) p.some_method("hello") ``` hello The `assert not kwargs` above is used to ensure that the user doesn’t pass an unknown parameter (i.e one that’s not in `_methods`). `fastai` uses [`funcs_kwargs`](https://fastcore.fast.ai/meta.html#funcs_kwargs) in many places, for instance, you can customize any part of a `DataLoader` by passing your own methods. `fastcore` also provides many utility functions that make a Python programmer’s life easier, in `fastcore.utils`. We won’t look at many here, since you can easily look at the docs yourself. To get you started, have a look at the docs for [`chunked`](https://fastcore.fast.ai/basics.html#chunked) (remember, if you’re in a notebook, type `doc(chunked)`), which is a handy function for creating lazily generated batches from a collection. Python’s [`ProcessPoolExecutor`](https://fastcore.fast.ai/parallel.html#processpoolexecutor) is extended to allow `max_workers` to be set to `0`, to easily turn off parallel processing. This makes it easy to debug your code in serial, then run it in parallel. It also allows you to pass arguments to your parallel function, and to ensure there’s a pause between calls, in case the process you are running has race conditions. [`parallel`](https://fastcore.fast.ai/parallel.html#parallel) makes parallel processing even easier to use, and even adds an optional progress bar. ### L Like most languages, Python allows for very concise syntax for some very common types, such as `list`, which can be constructed with `[1,2,3]`. Perl’s designer Larry Wall explained the reasoning for this kind of syntax: > In metaphorical honor of Huffman’s compression code that assigns > smaller numbers of bits to more common bytes. In terms of syntax, it > simply means that commonly used things should be shorter, but you > shouldn’t waste short sequences on less common constructs. On this basis, `fastcore` has just one type that has a single letter name: [`L`](https://fastcore.fast.ai/foundation.html#l). The reason for this is that it is designed to be a replacement for `list`, so we want it to be just as easy to use as `[1,2,3]`. Here’s how to create that as an [`L`](https://fastcore.fast.ai/foundation.html#l): ``` python L(1,2,3) ``` [1, 2, 3] The first thing to notice is that an [`L`](https://fastcore.fast.ai/foundation.html#l) object includes in its representation its number of elements; that’s the `(#3)` in the output above. If there’s more than 10 elements, it will automatically truncate the list: ``` python p = L.range(20).shuffle() p ``` [14, 19, 13, 11, 8, 9, 0, 6, 4, 15, 1, 17, 16, 5, 3, 10, 2, 7, 12, 18] [`L`](https://fastcore.fast.ai/foundation.html#l) contains many of the same indexing ideas that NumPy’s `array` does, including indexing with a list of indexes, or a boolean mask list: ``` python p[2,4,6] ``` [13, 8, 0] It also contains other methods used in `array`, such as [`L.argwhere`](https://fastcore.fast.ai/foundation.html#l.argwhere): ``` python p.argwhere(ge(15)) ``` [1, 9, 11, 12, 19] As you can see from this example, `fastcore` also includes a number of features that make a functional style of programming easier, such as a full range of boolean functions (e.g `ge`, `gt`, etc) which give the same answer as the functions from Python’s `operator` module if given two parameters, but return a [curried function](https://en.wikipedia.org/wiki/Currying) if given one parameter. There’s too much functionality to show it all here, so be sure to check the docs. Many little things are added that we thought should have been in `list` in the first place, such as making this do what you’d expect (which is an error with `list`, but works fine with [`L`](https://fastcore.fast.ai/foundation.html#l)): ``` python 1 + L(2,3,4) ``` [1, 2, 3, 4] --- # Source: https://fastcore.fast.ai/xdg.html.md # XDG See the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) for more information. ## Overview [`xdg_cache_home`](https://fastcore.fast.ai/xdg.html#xdg_cache_home), [`xdg_config_home`](https://fastcore.fast.ai/xdg.html#xdg_config_home), [`xdg_data_home`](https://fastcore.fast.ai/xdg.html#xdg_data_home), and [`xdg_state_home`](https://fastcore.fast.ai/xdg.html#xdg_state_home) return `pathlib.Path` objects containing the value of the environment variable named `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, and `XDG_STATE_HOME` respectively, or the default defined in the specification if the environment variable is unset, empty, or contains a relative path rather than absolute path. [`xdg_config_dirs`](https://fastcore.fast.ai/xdg.html#xdg_config_dirs) and [`xdg_data_dirs`](https://fastcore.fast.ai/xdg.html#xdg_data_dirs) return a list of `pathlib.Path` objects containing the value, split on colons, of the environment variable named `XDG_CONFIG_DIRS` and `XDG_DATA_DIRS` respectively, or the default defined in the specification if the environment variable is unset or empty. Relative paths are ignored, as per the specification. [`xdg_runtime_dir`](https://fastcore.fast.ai/xdg.html#xdg_runtime_dir) returns a `pathlib.Path` object containing the value of the `XDG_RUNTIME_DIR` environment variable, or `None` if the environment variable is not set, or contains a relative path rather than absolute path. ## Helpers We’ll start by defining a context manager that temporarily sets an environment variable to demonstrate the behaviour of each helper function: ``` python from contextlib import contextmanager ``` ``` python @contextmanager def env(variable, value): old = os.environ.get(variable, None) try: os.environ[variable] = value yield finally: if old is None: del os.environ[variable] else: os.environ[variable] = old ``` ------------------------------------------------------------------------ source ### xdg_cache_home ``` python def xdg_cache_home( ): ``` *Path corresponding to `XDG_CACHE_HOME`* ``` python from fastcore.test import * ``` ``` python test_eq(xdg_cache_home(), Path.home()/'.cache') with env('XDG_CACHE_HOME', '/home/fastai/.cache'): test_eq(xdg_cache_home(), Path('/home/fastai/.cache')) ``` ------------------------------------------------------------------------ source ### xdg_config_dirs ``` python def xdg_config_dirs( ): ``` *Paths corresponding to `XDG_CONFIG_DIRS`* ``` python test_eq(xdg_config_dirs(), [Path('/etc/xdg')]) with env('XDG_CONFIG_DIRS', '/home/fastai/.xdg:/home/fastai/.config'): test_eq(xdg_config_dirs(), [Path('/home/fastai/.xdg'), Path('/home/fastai/.config')]) ``` ------------------------------------------------------------------------ source ### xdg_config_home ``` python def xdg_config_home( ): ``` *Path corresponding to `XDG_CONFIG_HOME`* ``` python test_eq(xdg_config_home(), Path.home()/'.config') with env('XDG_CONFIG_HOME', '/home/fastai/.config'): test_eq(xdg_config_home(), Path('/home/fastai/.config')) ``` ------------------------------------------------------------------------ source ### xdg_data_dirs ``` python def xdg_data_dirs( ): ``` *Paths corresponding to XDG_DATA_DIRS\`* ------------------------------------------------------------------------ source ### xdg_data_home ``` python def xdg_data_home( ): ``` *Path corresponding to `XDG_DATA_HOME`* ``` python test_eq(xdg_data_home(), Path.home()/'.local/share') with env('XDG_DATA_HOME', '/home/fastai/.data'): test_eq(xdg_data_home(), Path('/home/fastai/.data')) ``` ------------------------------------------------------------------------ source ### xdg_runtime_dir ``` python def xdg_runtime_dir( ): ``` *Path corresponding to `XDG_RUNTIME_DIR`* ------------------------------------------------------------------------ source ### xdg_state_home ``` python def xdg_state_home( ): ``` *Path corresponding to `XDG_STATE_HOME`* ``` python test_eq(xdg_state_home(), Path.home()/'.local/state') with env('XDG_STATE_HOME', '/home/fastai/.state'): test_eq(xdg_state_home(), Path('/home/fastai/.state')) ``` ------------------------------------------------------------------------ Copyright © 2016-2021 Scott Stevenson Modifications copyright © 2022 onwards Jeremy Howard --- # Source: https://fastcore.fast.ai/xml.html.md # XML ``` python from IPython.display import Markdown from pprint import pprint from fastcore.test import test_eq, test_ne ``` ## FT functions ------------------------------------------------------------------------ source ### attrmap ``` python def attrmap( o ): ``` ------------------------------------------------------------------------ source ### valmap ``` python def valmap( o ): ``` ------------------------------------------------------------------------ source ### FT ``` python def FT( tag:str, cs:tuple, attrs:dict=None, void_:bool=False, kwargs:VAR_KEYWORD ): ``` *A ‘Fast Tag’ structure, containing `tag`,`children`,and `attrs`* ------------------------------------------------------------------------ source ### ft ``` python def ft( tag:str, c:VAR_POSITIONAL, void_:bool=False, attrmap:callable=attrmap, valmap:callable=valmap, ft_cls:type=FT, kw:VAR_KEYWORD ): ``` *Create an [`FT`](https://fastcore.fast.ai/xml.html#ft) structure for [`to_xml()`](https://fastcore.fast.ai/xml.html#to_xml)* The main HTML tags are exported as [`ft`](https://fastcore.fast.ai/xml.html#ft) partials. Attributes are passed as keywords. Use ‘klass’ and ‘fr’ instead of ‘class’ and ‘for’, to avoid Python reserved word clashes. ------------------------------------------------------------------------ source ### Html ``` python def Html( c:VAR_POSITIONAL, doctype:bool=True, kwargs:VAR_KEYWORD )->FT: ``` *An HTML tag, optionally preceeded by `!DOCTYPE HTML`* ``` python samp = Html( Head(Title('Some page')), Body(Div('Some text\nanother line', (Input(name="jph's"), Img(src="filename", data=1)), cls=['myclass', 'another'], style={'padding':1, 'margin':2})) ) pprint(samp) ``` (!doctype((),{'html': True}), html((head((title(('Some page',),{}),),{}), body((div(('Some text\nanother line', input((),{'name': "jph's"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{})) ``` python elem = P('Some text', id="myid") print(elem.tag) print(elem.children) print(elem.attrs) ``` p ('Some text',) {'id': 'myid'} You can get and set attrs directly: ``` python elem.id = 'newid' print(elem.id, elem.get('id'), elem.get('foo', 'missing')) elem ``` newid newid missing p(('Some text',),{'id': 'newid'}) ------------------------------------------------------------------------ source ### Safe ``` python def Safe( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *str(object=’’) -\> str* str(bytes_or_buffer\[, encoding\[, errors\]\]) -\> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.\_\_str\_\_() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’. ## Conversion to XML/HTML ------------------------------------------------------------------------ source ### to_xml ``` python def to_xml( elm, lvl:int=0, indent:bool=True, do_escape:bool=True ): ``` *Convert [`ft`](https://fastcore.fast.ai/xml.html#ft) element tree into an XML string* ------------------------------------------------------------------------ source ### FT.\_\_html\_\_ ``` python def __html__( ): ``` ``` python to_xml(Div(""), do_escape=True) ``` "
<script>alert('XSS')</script>
\n" ``` python h = to_xml(samp, do_escape=False) print(h) ``` Some page
Some text another line
``` python c = I('hello') print(c) ``` hello ``` python c ``` hello ``` python class PageTitle: def __ft__(self): return H1("Hello") class HomePage: def __ft__(self): return Div(PageTitle(), Div('hello')) h = to_xml(Div(HomePage())) expected_output = """

Hello

hello
""" assert h == expected_output ``` ``` python print(h) ```

Hello

hello
``` python h = to_xml(samp, indent=False) print(h) ``` Some page
Some text another line
Interoperability both directions with Django and Jinja using the [**html**() protocol](https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.escape): ``` python def _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s)) r = Safe('Hello from Django') print(to_xml(Div(r))) print(_esc(Div(P('Hello from fastcore <3')))) ```
Hello from Django

Hello from fastcore <3

FT attributes are rendered with [`to_xml`](https://fastcore.fast.ai/xml.html#to_xml): ``` python print(to_xml(P('hi', value=Div('ho')))) ```

hi

FT components also stringify with [`to_xml`](https://fastcore.fast.ai/xml.html#to_xml): ``` python print(Div('ho')) ```
ho
------------------------------------------------------------------------ source ### FT.\_\_hash\_\_ ``` python def __hash__( ): ``` *Return hash(self).* ------------------------------------------------------------------------ source ### FT.\_\_eq\_\_ ``` python def __eq__( other ): ``` *Return self==value.* [`FT`](https://fastcore.fast.ai/xml.html#ft) object equality and hashing is based on tag, attrs, and children. ``` python test_eq(Div('hello', id='x'), Div('hello', id='x')) test_ne(Div('hello'), Div('goodbye')) test_ne(Div('hello', id='a'), Div('hello', id='b')) test_ne(P('hello'), Div('hello')) test_eq(hash(Div('hello', id='x')), hash(Div('hello', id='x'))) assert hash(Div('hello')), hash(Div('goodbye')) ``` ## Display ------------------------------------------------------------------------ source ### highlight ``` python def highlight( s, lang:str='html' ): ``` *Markdown to syntax-highlight `s` in language `lang`* ------------------------------------------------------------------------ source ### showtags ``` python def showtags( s ): ``` You can also reorder the children to come *after* the attrs, if you use this alternative syntax for [`FT`](https://fastcore.fast.ai/xml.html#ft) where the children are in a second pair of `()` (behind the scenes this is because [`FT`](https://fastcore.fast.ai/xml.html#ft) implements `__call__` to add children). ``` python hl_md( Body(klass='myclass')( Div(style='padding:3px')( 'Some text 1<2', I(spurious=True)('in italics'), Input(name='me'), Img(src="filename", data=1) ) )) ``` ``` html
Some text 1<2in italics
``` ------------------------------------------------------------------------ source ### **getattr** ``` python def __getattr__( tag ): ``` --- # Source: https://fastcore.fast.ai/xtras.html.md # Utility functions ## File Functions Utilities (other than extensions to Pathlib.Path) for dealing with IO. ------------------------------------------------------------------------ source ### walk ``` python def walk( path:Path | str, # path to start searching symlinks:bool=True, # follow symlinks? keep_file:callable=ret_true, # function that returns True for wanted files keep_folder:callable=ret_true, # function that returns True for folders to enter skip_folder:callable=ret_false, # function that returns True for folders to skip func:callable=join, # function to apply to each matched file ret_folders:bool=False, # return folders, not just files sort:bool=True, # sort files by name within each folder ): ``` *Generator version of `os.walk`, using functions to filter files and folders* ------------------------------------------------------------------------ source ### exttypes ``` python def exttypes( types ): ``` *Get exts for comma-separated or list `typ`; if not found in list, return list with just `types`.* Supported: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg ``` python print(exttypes('py,doc')) print(exttypes('zig,txt')) ``` ['ipynb', 'py', 'md', 'rst'] ['zig', 'txt'] ------------------------------------------------------------------------ source ### globtastic ``` python def globtastic( path:Path | str='.', # path to start searching recursive:bool=True, # search subfolders symlinks:bool=True, # follow symlinks? file_glob:str=None, # Only include files matching glob file_re:str=None, # Only include files matching regex folder_re:str=None, # Only enter folders matching regex skip_file_glob:str=None, # Skip files matching glob skip_file_re:str=None, # Skip files matching regex skip_folder_re:str=None, # Skip folders matching regex, func:callable=join, # function to apply to each matched file ret_folders:bool=False, # return folders, not just files sort:bool=True, # sort files by name within each folder types:str | list=None, # list or comma-separated str of ext types from: py, js, java, c, cpp, rb, r, ex, sh, web, doc, cfg exts:str | list=None, # list or comma-separated str of exts to include )->L: # Paths to matched files ``` *A more powerful `glob`, including regex matches, symlink handling, and skip parameters* ``` python globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c') ``` ['./fastcore/basics.py', './fastcore/dispatch.py', './fastcore/docments.py', './fastcore/docscrape.py', './fastcore/script.py'] ``` python globtastic(skip_folder_re='^[_.]', folder_re='core', types='py', file_re='c', skip_file_re='^_', sort=True) ``` ['./fastcore/all.py', './fastcore/ansi.py', './fastcore/basics.py', './fastcore/dispatch.py', './fastcore/docments.py', './fastcore/docscrape.py', './fastcore/foundation.py', './fastcore/imghdr.py', './fastcore/imports.py', './fastcore/meta.py', './fastcore/nb_imports.py', './fastcore/net.py', './fastcore/parallel.py', './fastcore/py2pyi.py', './fastcore/script.py', './fastcore/shutil.py', './fastcore/style.py', './fastcore/test.py', './fastcore/tools.py', './fastcore/transform.py', './fastcore/utils.py', './fastcore/xdg.py', './fastcore/xml.py', './fastcore/xtras.py'] ------------------------------------------------------------------------ source ### pglob ``` python def pglob( path:Path | str='.', # path to start searching func:callable=Path, # function to apply to each matched file recursive:bool=True, symlinks:bool=True, file_glob:str=None, file_re:str=None, folder_re:str=None, skip_file_glob:str=None, skip_file_re:str=None, skip_folder_re:str=None, ret_folders:bool=False, sort:bool=True, types:str | list=None, exts:str | list=None )->L: # Paths to matched files ``` *Shortcut for `globtastic(..., call=Path)`* ``` python pglob('..', skip_folder_re='^[_.]', types='doc', skip_file_re='^_')[:6] ``` [Path('../CHANGELOG.md'), Path('../CODE_OF_CONDUCT.md'), Path('../CONTRIBUTING.md'), Path('../README.md')] ------------------------------------------------------------------------ source ### maybe_open ``` python def maybe_open( f, mode:str='r', kwargs:VAR_KEYWORD ): ``` *Context manager: open `f` if it is a path (and close on exit)* This is useful for functions where you want to accept a path *or* file. [`maybe_open`](https://fastcore.fast.ai/xtras.html#maybe_open) will not close your file handle if you pass one in. ``` python def _f(fn): with maybe_open(fn) as f: return f.encoding fname = '00_test.ipynb' sys_encoding = 'cp1252' if sys.platform == 'win32' else 'utf-8' test_eq(_f(fname).lower(), sys_encoding) with open(fname) as fh: test_eq(_f(fh).lower(), sys_encoding) ``` For example, we can use this to reimplement [`imghdr.what`](https://docs.python.org/3/library/imghdr.html#imghdr.what) from the Python standard library, which is [written in Python 3.9](https://github.com/python/cpython/blob/3.9/Lib/imghdr.py#L11) as: ``` python from fastcore import imghdr ``` ``` python def what(file, h=None): f = None try: if h is None: if isinstance(file, (str,os.PathLike)): f = open(file, 'rb') h = f.read(32) else: location = file.tell() h = file.read(32) file.seek(location) for tf in imghdr.tests: res = tf(h, f) if res: return res finally: if f: f.close() return None ``` Here’s an example of the use of this function: ``` python fname = 'images/puppy.jpg' what(fname) ``` 'jpeg' With [`maybe_open`](https://fastcore.fast.ai/xtras.html#maybe_open), `Self`, and [`L.map_first`](https://fastcore.fast.ai/foundation.html#l.map_first), we can rewrite this in a much more concise and (in our opinion) clear way: ``` python def what(file, h=None): if h is None: with maybe_open(file, 'rb') as f: h = f.peek(32) return L(imghdr.tests).map_first(Self(h,file)) ``` …and we can check that it still works: ``` python test_eq(what(fname), 'jpeg') ``` …along with the version passing a file handle: ``` python with open(fname,'rb') as f: test_eq(what(f), 'jpeg') ``` …along with the `h` parameter version: ``` python with open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg') ``` ------------------------------------------------------------------------ source ### mkdir ``` python def mkdir( path, exist_ok:bool=False, parents:bool=False, overwrite:bool=False, kwargs:VAR_KEYWORD ): ``` *Creates and returns a directory defined by `path`, optionally removing previous existing directory if `overwrite` is `True`* ``` python with tempfile.TemporaryDirectory() as d: path = Path(os.path.join(d, 'new_dir')) new_dir = mkdir(path) assert new_dir.exists() test_eq(new_dir, path) # test overwrite with open(new_dir/'test.txt', 'w') as f: f.writelines('test') test_eq(len(list(walk(new_dir))), 1) # assert file is present new_dir = mkdir(new_dir, overwrite=True) test_eq(len(list(walk(new_dir))), 0) # assert file was deleted ``` ------------------------------------------------------------------------ source ### image_size ``` python def image_size( fn ): ``` *Tuple of (w,h) for png, gif, or jpg; `None` otherwise* ``` python test_eq(image_size(fname), (1200,803)) ``` ``` python from PIL import Image from IPython.display import Image as IPImage ``` ``` python img = Image.new('RGB', (50, 50), color='red') img ``` ![](03_xtras_files/figure-commonmark/cell-24-output-1.png) ------------------------------------------------------------------------ source ### img_bytes ``` python def img_bytes( img, fmt:str='PNG' ): ``` ``` python ib = img_bytes(img) IPImage(ib) ``` ![](03_xtras_files/figure-commonmark/cell-26-output-1.png) ------------------------------------------------------------------------ source ### detect_mime ``` python def detect_mime( data ): ``` *Get the MIME type for bytes `data`, covering common PDF, audio, video, and image types* ``` python detect_mime(ib) ``` 'image/png' ``` python test_is(detect_mime('this is a non-byte string'),None) ``` ------------------------------------------------------------------------ source ### bunzip ``` python def bunzip( fn ): ``` *bunzip `fn`, raising exception if output already exists* ``` python f = Path('files/test.txt') if f.exists(): f.unlink() bunzip('files/test.txt.bz2') t = f.open().readlines() test_eq(len(t),1) test_eq(t[0], 'test\n') f.unlink() ``` ------------------------------------------------------------------------ source ### loads ``` python def loads( s, kw:VAR_KEYWORD ): ``` *Same as `json.loads`, but handles `None`* ------------------------------------------------------------------------ source ### loads_multi ``` python def loads_multi( s:str ): ``` *Generator of \>=0 decoded json dicts, possibly with non-json ignored text at start and end* ``` python tst = """ # ignored { "a":1 } hello { "b":2 } """ test_eq(list(loads_multi(tst)), [{'a': 1}, {'b': 2}]) ``` ------------------------------------------------------------------------ source ### dumps ``` python def dumps( obj, kw:VAR_KEYWORD ): ``` *Same as `json.dumps`, but uses `ujson` if available* ------------------------------------------------------------------------ source ### untar_dir ``` python def untar_dir( fname, dest, rename:bool=False, overwrite:bool=False ): ``` *untar `file` into `dest`, creating a directory if the root contains more than one item* ``` python def test_untar(foldername, rename=False, **kwargs): with tempfile.TemporaryDirectory() as d: nm = os.path.join(d, 'a') shutil.make_archive(nm, 'gztar', **kwargs) with tempfile.TemporaryDirectory() as d2: d2 = Path(d2) untar_dir(nm+'.tar.gz', d2, rename=rename) test_eq(d2.ls(), [d2/foldername]) ``` If the contents of `fname` contain just one file or directory, it is placed directly in `dest`: ``` python # using `base_dir` in `make_archive` results in `images` directory included in file names test_untar('images', base_dir='images') ``` If `rename` then the directory created is named based on the archive, without extension: ``` python test_untar('a', base_dir='images', rename=True) ``` If the contents of `fname` contain multiple files and directories, a new folder in `dest` is created with the same name as `fname` (but without extension): ``` python # using `root_dir` in `make_archive` results in `images` directory *not* included in file names test_untar('a', root_dir='images') ``` ------------------------------------------------------------------------ source ### repo_details ``` python def repo_details( url ): ``` *Tuple of `owner,name` from ssh or https git repo `url`* ``` python test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai']) test_eq(repo_details('git@github.com:fastai/nbdev.git\n'), ['fastai', 'nbdev']) ``` ------------------------------------------------------------------------ source ### shell ``` python def shell( args:VAR_POSITIONAL, kwargs:VAR_KEYWORD ): ``` *Shortcut for `subprocess.run(shell=True)`* ------------------------------------------------------------------------ source ### ssh ``` python def ssh( host, args:str='', user:str='ubuntu', sock:NoneType=None ): ``` *Run SSH command with given arguments* ------------------------------------------------------------------------ source ### rsync_multi ``` python def rsync_multi( ip, files, user:str='ubuntu', persist:str='5m' ): ``` *Transfer multiple files with rename using persistent SSH connection* ------------------------------------------------------------------------ source ### run ``` python def run( cmd, rest:VAR_POSITIONAL, same_in_win:bool=False, ignore_ex:bool=False, as_bytes:bool=False, stderr:bool=True ): ``` *Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails* You can pass a string (which will be split based on standard shell rules), a list, or pass args directly: ``` python run('echo', same_in_win=True) run('pip', '--version', same_in_win=True) run(['pip', '--version'], same_in_win=True) ``` 'pip 25.3 from /Users/rensdimmendaal/git/.venv/lib/python3.12/site-packages/pip (python 3.12)' ``` python if sys.platform == 'win32': assert 'ipynb' in run('cmd /c dir /p') assert 'ipynb' in run(['cmd', '/c', 'dir', '/p']) assert 'ipynb' in run('cmd', '/c', 'dir', '/p') else: assert 'ipynb' in run('ls -ls') assert 'ipynb' in run(['ls', '-l']) assert 'ipynb' in run('ls', '-l') ``` Some commands fail in non-error situations, like `grep`. Use `ignore_ex` in those cases, which will return a tuple of stdout and returncode: ``` python if sys.platform == 'win32': test_eq(run('cmd /c findstr asdfds 00_test.ipynb', ignore_ex=True)[0], 1) else: test_eq(run('grep asdfds 00_test.ipynb', ignore_ex=True)[0], 1) ``` [`run`](https://fastcore.fast.ai/xtras.html#run) automatically decodes returned bytes to a `str`. Use `as_bytes` to skip that: ``` python if sys.platform == 'win32': test_eq(run('cmd /c echo hi'), 'hi') else: test_eq(run('echo hi', as_bytes=True), b'hi\n') ``` ------------------------------------------------------------------------ source ### open_file ``` python def open_file( fn, mode:str='r', kwargs:VAR_KEYWORD ): ``` *Open a file, with optional compression if gz or bz2 suffix* ------------------------------------------------------------------------ source ### save_pickle ``` python def save_pickle( fn, o ): ``` *Save a pickle file, to a file name or opened file* ------------------------------------------------------------------------ source ### load_pickle ``` python def load_pickle( fn ): ``` *Load a pickle file from a file name or opened file* ``` python for suf in '.pkl','.bz2','.gz': # delete=False is added for Windows # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file with tempfile.NamedTemporaryFile(suffix=suf, delete=False) as f: fn = Path(f.name) save_pickle(fn, 't') t = load_pickle(fn) f.close() test_eq(t,'t') ``` ------------------------------------------------------------------------ source ### parse_env ``` python def parse_env( s:str=None, fn:Union[str, Path]=None )->dict: ``` *Parse a shell-style environment string or file* ``` python testf = """# comment # another comment export FOO="bar#baz" BAR=thing # comment "ok" baz='thong' QUX=quux export ZAP = "zip" # more comments FOOBAR = 42 # trailing space and comment""" exp = dict(FOO='bar#baz', BAR='thing', baz='thong', QUX='quux', ZAP='zip', FOOBAR='42') test_eq(parse_env(testf), exp) ``` ------------------------------------------------------------------------ source ### expand_wildcards ``` python def expand_wildcards( code ): ``` *Expand all wildcard imports in the given code string.* ``` python inp = """from math import * from os import * from random import * def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)""" exp = """from math import pi, sin from os import path from random import randint def func(): return sin(pi) + path.join('a', 'b') + randint(1, 10)""" test_eq(expand_wildcards(inp), exp) inp = """from itertools import * def func(): pass""" test_eq(expand_wildcards(inp), inp) inp = """def outer(): from math import * def inner(): from os import * return sin(pi) + path.join('a', 'b')""" exp = """def outer(): from math import pi, sin def inner(): from os import path return sin(pi) + path.join('a', 'b')""" test_eq(expand_wildcards(inp), exp) ``` ## Collections ------------------------------------------------------------------------ source ### dict2obj ``` python def dict2obj( d:NoneType=None, list_func:_L_Meta=L, dict_func:type=AttrDict, kwargs:VAR_KEYWORD ): ``` *Convert (possibly nested) dicts (or lists of dicts) to [`AttrDict`](https://fastcore.fast.ai/basics.html#attrdict)* This is a convenience to give you “dotted” access to (possibly nested) dictionaries, e.g: ``` python d1 = dict(a=1, b=dict(c=2,d=3)) d2 = dict2obj(d1) test_eq(d2.b.c, 2) test_eq(d2.b['c'], 2) ``` kwargs can also be used: ``` python d3 = dict2obj(a=1, b=dict(c=2,d=3)) test_eq(d3.b.c, 2) test_eq(d3.b['c'], 2) ``` It can also be used on lists of dicts. ``` python _list_of_dicts = [d1, d1] ds = dict2obj(_list_of_dicts) test_eq(ds[0].b.c, 2) ``` ------------------------------------------------------------------------ source ### obj2dict ``` python def obj2dict( d ): ``` *Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict`* [`obj2dict`](https://fastcore.fast.ai/xtras.html#obj2dict) can be used to reverse what is done by [`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj): ``` python test_eq(obj2dict(d2), d1) test_eq(obj2dict(ds), _list_of_dicts) ``` ------------------------------------------------------------------------ source ### repr_dict ``` python def repr_dict( d ): ``` *Print nested dicts and lists, such as returned by [`dict2obj`](https://fastcore.fast.ai/xtras.html#dict2obj)* ``` python print(repr_dict(d2)) ``` - a: 1 - b: - c: 2 - d: 3 ------------------------------------------------------------------------ source ### is_listy ``` python def is_listy( x ): ``` *`isinstance(x, (tuple,list,L,slice,Generator))`* ``` python assert is_listy((1,)) assert is_listy([1]) assert is_listy(L([1])) assert is_listy(slice(2)) assert not is_listy(array([1])) ``` ------------------------------------------------------------------------ source ### mapped ``` python def mapped( f, it ): ``` *map `f` over `it`, unless it’s not listy, in which case return `f(it)`* ``` python def _f(x,a=1): return x-a test_eq(mapped(_f,1),0) test_eq(mapped(_f,[1,2]),[0,1]) test_eq(mapped(_f,(1,)),(0,)) ``` ## Extensions to Pathlib.Path The following methods are added to the standard python libary [Pathlib.Path](https://docs.python.org/3/library/pathlib.html#basic-use). ------------------------------------------------------------------------ source ### Path.readlines ``` python def readlines( hint:int=-1, encoding:str='utf8' ): ``` *Read the content of `self`* ------------------------------------------------------------------------ source ### Path.read_json ``` python def read_json( encoding:NoneType=None, errors:NoneType=None ): ``` *Same as `read_text` followed by [`loads`](https://fastcore.fast.ai/xtras.html#loads)* ------------------------------------------------------------------------ source ### Path.mk_write ``` python def mk_write( data, encoding:NoneType=None, errors:NoneType=None, mode:int=511, uid:int=-1, gid:int=-1 ): ``` *Make all parent dirs of `self`, and write `data`* ------------------------------------------------------------------------ source ### Path.write_json ``` python def write_json( data, encoding:NoneType=None, errors:NoneType=None, mode:int=511, uid:int=-1, gid:int=-1, kw:VAR_KEYWORD ): ``` *Same as [`dumps`](https://fastcore.fast.ai/xtras.html#dumps)followed by `mk_write`* ------------------------------------------------------------------------ source ### Path.relpath ``` python def relpath( start:NoneType=None ): ``` *Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks* ``` python p = Path('../fastcore/').resolve() p ``` Path('/Users/rensdimmendaal/git/repos/fastcore/fastcore') ``` python p.relpath(Path.cwd()) ``` Path('../fastcore') ------------------------------------------------------------------------ source ### Path.ls ``` python def ls( n_max:NoneType=None, file_type:NoneType=None, file_exts:NoneType=None ): ``` *Contents of path as a list* We add an `ls()` method to `pathlib.Path` which is simply defined as `list(Path.iterdir())`, mainly for convenience in REPL environments such as notebooks. ``` python path = Path() t = path.ls() assert len(t)>0 t1 = path.ls(10) test_eq(len(t1), 10) t2 = path.ls(file_exts='.ipynb') assert len(t)>len(t2) t[0] ``` Path('llms.txt') You can also pass an optional `file_type` MIME prefix and/or a list of file extensions. ``` python lib_path = (path/'../fastcore') txt_files=lib_path.ls(file_type='text') assert len(txt_files) > 0 and txt_files[0].suffix=='.py' ipy_files=path.ls(file_exts=['.ipynb']) assert len(ipy_files) > 0 and ipy_files[0].suffix=='.ipynb' txt_files[0],ipy_files[0] ``` (Path('../fastcore/shutil.py'), Path('000_tour.ipynb')) ------------------------------------------------------------------------ source ### Path.normpath ``` python def normpath( ): ``` *Normalize path, eliminating double slashes, etc.* `normpath` normalizes a path by collapsing redundant separators and up-level references (e.g., `..`). ``` python p = Path('foo//bar/../baz') p.normpath() ``` Path('foo/baz') ------------------------------------------------------------------------ source ### Path.\_\_repr\_\_ ``` python def __repr__( ): ``` *Return repr(self).* fastai also updates the `repr` of `Path` such that, if `Path.BASE_PATH` is defined, all paths are printed relative to that path (as long as they are contained in `Path.BASE_PATH`: ``` python t = ipy_files[0].absolute() try: Path.BASE_PATH = t.parent.parent test_eq(repr(t), f"Path('nbs/{t.name}')") finally: Path.BASE_PATH = None ``` ------------------------------------------------------------------------ source ### Path.delete ``` python def delete( ): ``` *Delete a file, symlink, or directory tree* ## Reindexing Collections ------------------------------------------------------------------------ source #### ReindexCollection ``` python def ReindexCollection( coll, idxs:NoneType=None, cache:NoneType=None, tfm:function=noop ): ``` *Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`* This is useful when constructing batches or organizing data in a particular manner (i.e. for deep learning). This class is primarly used in organizing data for language models in fastai. You can supply a custom index upon instantiation with the `idxs` argument, or you can call the `reindex` method to supply a new index for your collection. Here is how you can reindex a list such that the elements are reversed: ``` python rc=ReindexCollection(['a', 'b', 'c', 'd', 'e'], idxs=[4,3,2,1,0]) list(rc) ``` ['e', 'd', 'c', 'b', 'a'] Alternatively, you can use the `reindex` method: ------------------------------------------------------------------------ source ###### ReindexCollection.reindex ``` python def reindex( idxs ): ``` *Replace `self.idxs` with idxs* ``` python rc=ReindexCollection(['a', 'b', 'c', 'd', 'e']) rc.reindex([4,3,2,1,0]) list(rc) ``` ['e', 'd', 'c', 'b', 'a'] You can optionally specify a LRU cache, which uses [functools.lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache) upon instantiation: ``` python sz = 50 t = ReindexCollection(L.range(sz), cache=2) #trigger a cache hit by indexing into the same element multiple times t[0], t[0] t._get.cache_info() ``` CacheInfo(hits=1, misses=1, maxsize=2, currsize=1) You can optionally clear the LRU cache by calling the `cache_clear` method: ------------------------------------------------------------------------ source ##### ReindexCollection.cache_clear ``` python def cache_clear( ): ``` *Clear LRU cache* ``` python sz = 50 t = ReindexCollection(L.range(sz), cache=2) #trigger a cache hit by indexing into the same element multiple times t[0], t[0] t.cache_clear() t._get.cache_info() ``` CacheInfo(hits=0, misses=0, maxsize=2, currsize=0) ------------------------------------------------------------------------ source ##### ReindexCollection.shuffle ``` python def shuffle( ): ``` *Randomly shuffle indices* Note that an ordered index is automatically constructed for the data structure even if one is not supplied. ``` python rc=ReindexCollection(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']) rc.shuffle() list(rc) ``` ['a', 'g', 'h', 'c', 'd', 'b', 'f', 'e'] ``` python sz = 50 t = ReindexCollection(L.range(sz), cache=2) test_eq(list(t), range(sz)) test_eq(t[sz-1], sz-1) test_eq(t._get.cache_info().hits, 1) t.shuffle() test_eq(t._get.cache_info().hits, 1) test_ne(list(t), range(sz)) test_eq(set(t), set(range(sz))) t.cache_clear() test_eq(t._get.cache_info().hits, 0) test_eq(t.count(0), 1) ``` ## [`SaveReturn`](https://fastcore.fast.ai/xtras.html#savereturn) and [`save_iter`](https://fastcore.fast.ai/xtras.html#save_iter) Variants These utilities solve a common problem in Python: how to extract additional information from generator functions beyond just the yielded values. In Python, generator functions can `yield` values and also `return` a final value, but the return value is normally lost when you iterate over the generator: ``` python def example_generator(): total = 0 for i in range(3): total += i yield i return total # This gets lost! # The return value (3) is lost values = list(example_generator()) # [0, 1, 2] ``` ------------------------------------------------------------------------ source ### SaveReturn ``` python def SaveReturn( its ): ``` *Wrap an iterator such that the generator function’s return value is stored in `.value`*
Exported source ``` python class SaveReturn: "Wrap an iterator such that the generator function's return value is stored in `.value`" def __init__(self, its): self.its = its def __iter__(self): self.value = yield from self.its return self.value ```
[`SaveReturn`](https://fastcore.fast.ai/xtras.html#savereturn) is the simplest approach to solving this problem - it wraps any existing (non-async) generator and captures its return value. This works because `yield from` (used internally in [`SaveReturn`](https://fastcore.fast.ai/xtras.html#savereturn)) returns the value from the `return` of the generator function. ``` python def sum_range(n): total = 0 for i in range(n): total += i yield i return total # This value is returned by yield from sr = SaveReturn(sum_range(5)) values = list(sr) # This will consume the generator and get the return value print(f"Values: {values}") sr.value ``` Values: [0, 1, 2, 3, 4] 10 In order to provide an accurate signature for [`save_iter`](https://fastcore.fast.ai/xtras.html#save_iter), we need a version of `wraps` that removes leading parameters: ------------------------------------------------------------------------ source ### trim_wraps ``` python def trim_wraps( f, n:int=1 ): ``` *Like wraps, but removes the first n parameters from the signature* [`trim_wraps`](https://fastcore.fast.ai/xtras.html#trim_wraps) is a decorator factory that works like `functools.wraps`, but removes the first `n` parameters from the wrapped function’s signature. This is useful when creating wrapper functions that consume some parameters internally and shouldn’t expose them in the public API. ``` python def adder(base, x, y): return base + x + y def make_adder(base_value): @trim_wraps(adder) def _(x, y): return adder(base_value, x, y) return _ add_10 = make_adder(10) print(f"{add_10.__name__}{inspect.signature(add_10)}") ``` adder(x, y) ------------------------------------------------------------------------ source ### save_iter ``` python def save_iter( g ): ``` *Decorator that allows a generator function to store values in the returned iterator object* [`save_iter`](https://fastcore.fast.ai/xtras.html#save_iter) modifies generator functions to store state in the iterator object itself. The generator receives an object as its first parameter, which it can use to store attributes. You can store values during iteration, not just at the end, and you can store multiple attributes if needed. ``` python @save_iter def sum_range(o, n): # Note: 'o' parameter added total = 0 for i in range(n): total += i yield i o.value = total # Store directly on the iterator object ``` Because iternally [`save_iter`](https://fastcore.fast.ai/xtras.html#save_iter) uses [`trim_wraps`](https://fastcore.fast.ai/xtras.html#trim_wraps), the signature of `sum_range` correctly shows that you should *not* pass `o` to it; it’s injected by the decorating function. ``` python print(sum_range.__signature__) ``` (n) ``` python sr = sum_range(5) print(f"Values: {list(sr)}") print(f"Sum stored: {sr.value}") ``` Values: [0, 1, 2, 3, 4] Sum stored: 10 ------------------------------------------------------------------------ source ### asave_iter ``` python def asave_iter( g ): ``` *Like [`save_iter`](https://fastcore.fast.ai/xtras.html#save_iter), but for async iterators* [`asave_iter`](https://fastcore.fast.ai/xtras.html#asave_iter) provides the same functionality as [`save_iter`](https://fastcore.fast.ai/xtras.html#save_iter), but for async generator functions. `yield from` and `return` can not be used with async generator functions, so [`SaveReturn`](https://fastcore.fast.ai/xtras.html#savereturn) can’t be used here. ``` python @asave_iter async def asum_range(self, n): total = 0 for i in range(n): total += i yield i self.value = total asr = asum_range(5) print(f"Values: {[o async for o in asr]}") print(f"Sum stored: {asr.value}") ``` Values: [0, 1, 2, 3, 4] Sum stored: 10 ## Other Helpers ------------------------------------------------------------------------ source ### unqid ``` python def unqid( seeded:bool=False ): ``` *Generate a unique id suitable for use as a Python identifier* [`unqid`](https://fastcore.fast.ai/xtras.html#unqid) generates a random unique identifier that is safe to use as a Python variable name (starts with `_`, uses only alphanumeric characters and underscores). It’s based on UUID4, encoded in URL-safe base64. If `seeded=True`, uses `random.getrandbits` which respects `random.seed()`, making it reproducible. Otherwise uses `uuid4()` which is always random. ``` python unqid() ``` '_xNpj56ekTqyk9MsVDYRnnA' With seeding for reproducibility: ``` python random.seed(42) a = unqid(seeded=True) random.seed(42) b = unqid(seeded=True) test_eq(a, b) ``` Without seeding - always unique: ``` python test_ne(unqid(), unqid()) ``` ------------------------------------------------------------------------ source ### rtoken_hex ``` python def rtoken_hex( nbytes:int=16, # Number of bytes to generate )->str: # hex string of length nbytes*2 ``` *Generate a random hex string using Python’s random module.* This is the same as `secrets.token_hex`, but is reproducible/seedable. ``` python import secrets ``` ``` python secrets.token_hex(4),rtoken_hex(4) ``` ('0100dd53', '8c7d7247') ------------------------------------------------------------------------ source ### friendly_name ``` python def friendly_name( levels:int=3, suffix:int=4 ): ``` *Generate a random human-readable name with customizable word levels and suffix length* [`friendly_name`](https://fastcore.fast.ai/xtras.html#friendly_name) generates random, human-readable names by combining adjectives, nouns, verbs, and adverbs with a random alphanumeric suffix. This is useful for creating memorable identifiers for temporary files, test data, or user-friendly resource names. ``` python friendly_name() # Default: 3 word levels + 4-char suffix ``` 'objective-forest-builds-0y6d' Names are hyphen-separated and follow the pattern `adjective-noun-verb-adverb`, randomly chosen from lists of size 102, 116, 110, and 30, respectively. The `levels` param selects how many of the names to include: ``` python friendly_name(2) # 2 words + 4-char suffix ``` 'lavender-hummingbird-divu' `suffix` sets the length of the random alphanumeric ending. Each suffix item is taken from the 36 options of lowercase letters plus digits. ``` python friendly_name(4, 6) # All 4 word types + 6-char suffix ``` 'elated-koala-begins-softly-zpqk51' ------------------------------------------------------------------------ source ### n_friendly_names ``` python def n_friendly_names( levels:int=3, suffix:int=4 ): ``` *Number of possible combos for \`friendly_names* The number of combinations if all levels are included is: ``` python print(f'{n_friendly_names(4):,}') ``` 65,581,614,489,600 The default settings give: ``` python print(f'{n_friendly_names():,}') ``` 2,186,053,816,320 ------------------------------------------------------------------------ source ### exec_eval ``` python def exec_eval( code, # Code to exec/eval g:NoneType=None, # Globals namespace dict l:NoneType=None, # Locals namespace dict ): ``` *Evaluate `code` in `g` (defaults to `globals()`) and `l` (defaults to `locals()`)* This is a combination of `eval` and `exec`, which behaves like ipython and Jupyter. If the last line is an expression, it is evaluated and the result is returned: ``` python exec_eval(''' def f(x): return x+1 f(1) ''') ``` 2 By default, the code uses the caller’s globals and locals. For instance, here `f` is available since it’s been added to our symbol table: ``` python exec_eval('print(f(2))') ``` 3 Pass a dict as the `g` param in order to use an arbitrary namespace: ``` python exec_eval('print(f)', {'f': 'Hi I am f.'}) ``` Hi I am f. This function helps us identify the first declared raw function of a dispatched function: ``` python from plum import Function ``` ``` python def f1(x): return "Any" def f2(x:int): return "Int" df = Function(f1).dispatch(f1).dispatch(f2) test_eq(_unwrapped_type_dispatch_func(df), f1) ``` ------------------------------------------------------------------------ source ### get_source_link ``` python def get_source_link( func ): ``` *Return link to `func` in source code* [`get_source_link`](https://fastcore.fast.ai/xtras.html#get_source_link) allows you get a link to source code related to an object. For [nbdev](https://github.com/fastai/nbdev) related projects such as fastcore, we can get the full link to a GitHub repo. For `nbdev` projects, be sure to properly set the `git_url` in `settings.ini` (derived from `lib_name` and `branch` on top of the prefix you will need to adapt) so that those links are correct. For example, below we get the link to [`fastcore.test.test_eq`](https://fastcore.fast.ai/test.html#test_eq): ``` python from fastcore.test import test_eq ``` ``` python assert 'fastcore/test.py' in get_source_link(test_eq) assert get_source_link(test_eq).startswith('https://github.com/AnswerDotAI/fastcore') get_source_link(test_eq) ``` 'https://github.com/AnswerDotAI/fastcore/fastcore/test.py#L38' ------------------------------------------------------------------------ source ### sparkline ``` python def sparkline( data, mn:NoneType=None, mx:NoneType=None, empty_zero:bool=False ): ``` *Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column* ``` python data = [9,6,None,1,4,0,8,15,10] print(f'without "empty_zero": {sparkline(data, empty_zero=False)}') print(f' with "empty_zero": {sparkline(data, empty_zero=True )}') ``` without "empty_zero": ▅▂ ▁▂▁▃▇▅ with "empty_zero": ▅▂ ▁▂ ▃▇▅ You can set a maximum and minimum for the y-axis of the sparkline with the arguments `mn` and `mx` respectively: ``` python sparkline([1,2,3,400], mn=0, mx=3) ``` '▂▅▇▇' ------------------------------------------------------------------------ source ### modify_exception ``` python def modify_exception( e:Exception, # An exception msg:str=None, # A custom message replace:bool=False, # Whether to replace e.args with [msg] )->Exception: ``` *Modifies `e` with a custom message attached* ``` python msg = "This is my custom message!" test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), None)), contains='') test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception(), msg)), contains=msg) test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg)), contains="The first message This is my custom message!") test_fail(lambda: (_ for _ in ()).throw(modify_exception(Exception("The first message"), msg, True)), contains="This is my custom message!") ``` ------------------------------------------------------------------------ source ### round_multiple ``` python def round_multiple( x, mult, round_down:bool=False ): ``` *Round `x` to nearest multiple of `mult`* ``` python test_eq(round_multiple(63,32), 64) test_eq(round_multiple(50,32), 64) test_eq(round_multiple(40,32), 32) test_eq(round_multiple( 0,32), 0) test_eq(round_multiple(63,32, round_down=True), 32) test_eq(round_multiple((63,40),32), (64,32)) ``` ------------------------------------------------------------------------ source ### set_num_threads ``` python def set_num_threads( nt ): ``` *Get numpy (and others) to use `nt` threads* This sets the number of threads consistently for many tools, by: 1. Set the following environment variables equal to `nt`: `OPENBLAS_NUM_THREADS`,`NUMEXPR_NUM_THREADS`,`OMP_NUM_THREADS`,`MKL_NUM_THREADS` 2. Sets `nt` threads for numpy and pytorch. ------------------------------------------------------------------------ source ### join_path_file ``` python def join_path_file( file, path, ext:str='' ): ``` *Return `path/file` if file is a string or a `Path`, file otherwise* ``` python path = Path.cwd()/'_tmp'/'tst' f = join_path_file('tst.txt', path) assert path.exists() test_eq(f, path/'tst.txt') with open(f, 'w') as f_: assert join_path_file(f_, path) == f_ shutil.rmtree(Path.cwd()/'_tmp') ``` ------------------------------------------------------------------------ source ### autostart ``` python def autostart( g ): ``` *Decorator that automatically starts a generator* ------------------------------------------------------------------------ source #### EventTimer ``` python def EventTimer( store:int=5, span:int=60 ): ``` *An event timer with history of `store` items of time `span`* Add events with `add`, and get number of `events` and their frequency (`freq`). ``` python # Random wait function for testing def _randwait(): yield from (sleep(random.random()/200) for _ in range(100)) c = EventTimer(store=5, span=0.03) for o in _randwait(): c.add(1) print(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}') print('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}')) ``` Num Events: 9, Freq/sec: 346.2 Most recent: ▅▃▇▁▇ 263.7 256.6 278.3 235.1 276.9 ------------------------------------------------------------------------ source ### stringfmt_names ``` python def stringfmt_names( s:str )->list: ``` *Unique brace-delimited names in `s`* ``` python s = '/pulls/{pull_number}/reviews/{review_id}' test_eq(stringfmt_names(s), ['pull_number','review_id']) ``` ------------------------------------------------------------------------ source #### PartialFormatter ``` python def PartialFormatter( ): ``` *A `string.Formatter` that doesn’t error on missing fields, and tracks missing fields and unused args* ------------------------------------------------------------------------ source ### partial_format ``` python def partial_format( s:str, kwargs:VAR_KEYWORD ): ``` *string format `s`, ignoring missing field errors, returning missing and extra fields* The result is a tuple of `(formatted_string,missing_fields,extra_fields)`, e.g: ``` python res,missing,xtra = partial_format(s, pull_number=1, foo=2) test_eq(res, '/pulls/1/reviews/{review_id}') test_eq(missing, ['review_id']) test_eq(xtra, {'foo':2}) ``` ------------------------------------------------------------------------ source ### truncstr ``` python def truncstr( s:str, maxlen:int, suf:str='…', space:str='', sizevar:str=None )->str: ``` *Truncate `s` to length `maxlen`, adding suffix `suf` if truncated* ``` python w = 'abacadabra' test_eq(truncstr(w, 10), w) test_eq(truncstr(w, 5), 'abac…') test_eq(truncstr(w, 5, suf=''), 'abaca') test_eq(truncstr(w, 11, space='_'), w+"_") test_eq(truncstr(w, 10, space='_'), w[:-1]+'…') test_eq(truncstr(w, 5, suf='!!'), 'aba!!') ``` `sizevar` lets you include the original string length in your suffix. E.g when you set `sizevar='_n_'`, any `{_n_}` in your suffix gets replaced with the actual length of the string before truncation. For instance, here the `(11)` tells you the original string was 11 characters long: ``` python test_eq(truncstr('hello world', 8, suf='…({_n_})', sizevar='_n_'), 'hel…(11)') ``` ------------------------------------------------------------------------ source ### utc2local ``` python def utc2local( dt:datetime )->datetime: ``` *Convert `dt` from UTC to local time* ``` python dt = datetime(2000,1,1,12) print(f'{dt} UTC is {utc2local(dt)} local time') ``` 2000-01-01 12:00:00 UTC is 2000-01-01 13:00:00+01:00 local time ------------------------------------------------------------------------ source ### local2utc ``` python def local2utc( dt:datetime )->datetime: ``` *Convert `dt` from local to UTC time* ``` python print(f'{dt} local is {local2utc(dt)} UTC time') ``` 2000-01-01 12:00:00 local is 2000-01-01 11:00:00+00:00 UTC time ------------------------------------------------------------------------ source ### trace ``` python def trace( f ): ``` *Add `set_trace` to an existing function `f`* You can add a breakpoint to an existing function, e.g: ``` python Path.cwd = trace(Path.cwd) Path.cwd() ``` Now, when the function is called it will drop you into the debugger. Note, you must issue the `s` command when you begin to step into the function that is being traced. ------------------------------------------------------------------------ source ### modified_env ``` python def modified_env( delete:VAR_POSITIONAL, replace:VAR_KEYWORD ): ``` *Context manager temporarily modifying `os.environ` by deleting `delete` and replacing `replace`* ``` python # USER isn't in Cloud Linux Environments env_test = 'USERNAME' if sys.platform == "win32" else 'SHELL' oldusr = os.environ[env_test] replace_param = {env_test: 'a'} with modified_env('PATH', **replace_param): test_eq(os.environ[env_test], 'a') assert 'PATH' not in os.environ assert 'PATH' in os.environ test_eq(os.environ[env_test], oldusr) ``` ------------------------------------------------------------------------ source #### ContextManagers ``` python def ContextManagers( mgrs ): ``` *Wrapper for `contextlib.ExitStack` which enters a collection of context managers* ------------------------------------------------------------------------ source ### shufflish ``` python def shufflish( x, pct:float=0.04 ): ``` *Randomly relocate items of `x` up to `pct` of `len(x)` from their starting location* ------------------------------------------------------------------------ source ### console_help ``` python def console_help( libname:str ): ``` *Show help for all console scripts from `libname`* ------------------------------------------------------------------------ source ### hl_md ``` python def hl_md( s, lang:str='html', show:bool=True ): ``` *Syntax highlight `s` using `lang`.* When we display code in a notebook, it’s nice to highlight it, so we create a function to simplify that: ``` python hl_md('a child') ``` ``` html a child ``` ------------------------------------------------------------------------ source ### type2str ``` python def type2str( typ:type )->str: ``` *Stringify `typ`* ``` python test_eq(type2str(Optional[float]), 'Union[float, None]') ``` ------------------------------------------------------------------------ source ### dataclass_src ``` python def dataclass_src( cls ): ``` ``` python DC = make_dataclass('DC', [('x', int), ('y', Optional[float], None), ('z', float, None)]) print(dataclass_src(DC)) ``` @dataclass class DC: x: int y: Union[float, None] = None z: float = None ------------------------------------------------------------------------ source ### Unset ``` python def Unset( args:VAR_POSITIONAL, kwds:VAR_KEYWORD ): ``` *Create a collection of name/value pairs.* Example enumeration: > > > class Color(Enum): … RED = 1 … BLUE = 2 … GREEN = 3 Access them by: - attribute access: > > > Color.RED \ - value lookup: > > > Color(1) \ - name lookup: > > > Color\[‘RED’\] \ Enumerations can be iterated over, and know how many members they have: > > > len(Color) 3 > > > list(Color) \[\, \, \ > > 3\>\] Methods can be added to enumerations, and members can have their own attributes – see the documentation for details. ------------------------------------------------------------------------ source ### nullable_dc ``` python def nullable_dc( cls ): ``` *Like `dataclass`, but default of `UNSET` added to fields without defaults* ``` python @nullable_dc class Person: name: str; age: int; city: str = "Unknown" Person(name="Bob") ``` Person(name='Bob', age=UNSET, city='Unknown') ------------------------------------------------------------------------ source ### make_nullable ``` python def make_nullable( clas ): ``` ``` python @dataclass class Person: name: str; age: int; city: str = "Unknown" make_nullable(Person) Person("Bob", city='NY') ``` Person(name='Bob', age=UNSET, city='NY') ``` python Person(name="Bob") ``` Person(name='Bob', age=UNSET, city='Unknown') ``` python Person("Bob", 34) ``` Person(name='Bob', age=34, city='Unknown') ------------------------------------------------------------------------ source ### flexiclass ``` python def flexiclass( cls, # The class to convert )->dataclass: ``` *Convert `cls` into a `dataclass` like [`make_nullable`](https://fastcore.fast.ai/xtras.html#make_nullable). Converts in place and also returns the result.* This can be used as a decorator… ``` python @flexiclass class Person: name: str; age: int; city: str = "Unknown" bob = Person(name="Bob") bob ``` Person(name='Bob', age=UNSET, city='Unknown') …or can update the behavior of an existing class (or dataclass): ``` python class Person: name: str; age: int; city: str = "Unknown" flexiclass(Person) bob = Person(name="Bob") bob ``` Person(name='Bob', age=UNSET, city='Unknown') Action occurs in-place: ``` python class Person: name: str; age: int; city: str = "Unknown" flexiclass(Person) is_dataclass(Person) ``` True ------------------------------------------------------------------------ source ### asdict ``` python def asdict( o )->dict: ``` *Convert `o` to a `dict`, supporting dataclasses, namedtuples, iterables, and `__dict__` attrs.* Any `UNSET` values are not included. ``` python asdict(bob) ``` {'name': 'Bob', 'city': 'Unknown'} Set the optional `__flds__` parameter to customise the field list, and the optional `__skip__` parameter to skip some names. ``` python class CustomObj: def __init__(self): self.a,self.b,self.c,self.d = 1,2,3,4 __flds__ = ['a','b','c','d'] __skip__ = ['b'] obj = CustomObj() test_eq(asdict(obj), {'a': 1, 'c': 3, 'd': 4}) ``` To customise dict conversion behavior for a class, implement the `_asdict` method (this is used in the Python stdlib for named tuples). ------------------------------------------------------------------------ source ### vars_pub ``` python def vars_pub( x ): ``` *Get public non-skipped vars* The [`vars_pub`](https://fastcore.fast.ai/xtras.html#vars_pub) function returns a list of public (non-underscore-prefixed) variable names from an object, excluding any names listed in the object’s optional `__skip__` attribute. ``` python class TestObj: def __init__(self): self.pub_attr,self._priv_attr,self.another_pub,self.skip_me = 1,2,3,4 __skip__ = ['skip_me'] obj = TestObj() test_eq(vars_pub(obj), ['pub_attr', 'another_pub']) ``` Without `__skip__`, all pub vars are returned ``` python class SimpleObj: def __init__(self): self.a,self._b,self.c = 1,2,3 simple = SimpleObj() test_eq(vars_pub(simple), ['a', 'c']) ``` ------------------------------------------------------------------------ source ### is_typeddict ``` python def is_typeddict( cls:type )->bool: ``` *Check if `cls` is a `TypedDict`* ``` python class MyDict(TypedDict): name:str assert is_typeddict(MyDict) assert not is_typeddict({'a':1}) ``` ------------------------------------------------------------------------ source ### is_namedtuple ``` python def is_namedtuple( cls ): ``` *`True` if `cls` is a namedtuple type* ``` python assert is_namedtuple(namedtuple('tst', ['a'])) assert not is_namedtuple(tuple) ``` ------------------------------------------------------------------------ source ### CachedIter ``` python def CachedIter( o ): ``` *Cache the result returned by an iterator* ``` python def f(): yield 1 return 2 r = CachedIter(f()) for o in r: print(o) r.value ``` 1 2 ------------------------------------------------------------------------ source ### CachedAwaitable ``` python def CachedAwaitable( o ): ``` *Cache the result from an awaitable* ------------------------------------------------------------------------ source ### reawaitable ``` python def reawaitable( func:callable ): ``` *Wraps the result of an asynchronous function into an object which can be awaited more than once* `CachedCoro` and [`reawaitable`](https://fastcore.fast.ai/xtras.html#reawaitable) are partly based on [python issue tracker](https://bugs.python.org/issue46622) code from Serhiy Storchaka. They allow an awaitable to be called multiple times. ``` python @reawaitable async def fetch_data(): await asyncio.sleep(0.1) return "data" r = fetch_data() print(await r) # "data" print(await r) # "data" (no delay) ``` data data ------------------------------------------------------------------------ source ### flexicache ``` python def flexicache( funcs:VAR_POSITIONAL, maxsize:int=128 ): ``` *Like `lru_cache`, but customisable with policy `funcs`* This is a flexible lru cache function that you can pass a list of functions to. Those functions define the cache eviction policy. For instance, [`time_policy`](https://fastcore.fast.ai/xtras.html#time_policy) is provided for time-based cache eviction, and [`mtime_policy`](https://fastcore.fast.ai/xtras.html#mtime_policy) evicts based on a file’s modified-time changing. The policy functions are passed the last value that function returned was (initially `None`), and return a new value to indicate the cache has expired. When the cache expires, all functions are called with `None` to force getting new values. ------------------------------------------------------------------------ source ### time_policy ``` python def time_policy( seconds ): ``` *A [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) policy that expires cached items after `seconds` have passed* ------------------------------------------------------------------------ source ### mtime_policy ``` python def mtime_policy( filepath ): ``` *A [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) policy that expires cached items after `filepath` modified-time changes* ``` python @flexicache(time_policy(10), mtime_policy('000_tour.ipynb')) def cached_func(x, y): return x+y cached_func(1,2) ``` 3 ``` python @flexicache(time_policy(10), mtime_policy('000_tour.ipynb')) async def cached_func(x, y): return x+y print(await cached_func(1,2)) await cached_func(1,2) ``` 3 3 ------------------------------------------------------------------------ source ### timed_cache ``` python def timed_cache( seconds:int=60, maxsize:int=128 ): ``` *Like `lru_cache`, but also with time-based eviction* ``` python # demonstrate that flexicache is LRU @flexicache(maxsize=2) def cached_func(x): return time() time_1 = cached_func(1) test_eq(time_1, cached_func(1)) time_2 = cached_func(2) test_eq(time_1, cached_func(1)) test_eq(time_2, cached_func(2)) time_3 = cached_func(3) # Removes 1 test_eq(time_2, cached_func(2)) # cache remains test_eq(time_3, cached_func(3)) # cache remains test_ne(time_1, cached_func(1)) # NEQ, removes 2 test_ne(time_2, cached_func(2)) # NEQ, removes 3 test_eq(cached_func(1), cached_func(1)) ``` This function is a small convenience wrapper for using [`flexicache`](https://fastcore.fast.ai/xtras.html#flexicache) with [`time_policy`](https://fastcore.fast.ai/xtras.html#time_policy). ``` python @timed_cache(seconds=0.05, maxsize=2) def cached_func(x): return x * 2, time() # basic caching result1, time1 = cached_func(2) test_eq(result1, 4) sleep(0.001) result2, time2 = cached_func(2) test_eq(result2, 4) test_eq(time1, time2) # caching different values result3, _ = cached_func(3) test_eq(result3, 6) # maxsize _, time4 = cached_func(4) _, time2_new = cached_func(2) test_close(time2, time2_new, eps=0.1) _, time3_new = cached_func(3) test_ne(time3_new, time()) # time expiration sleep(0.05) _, time4_new = cached_func(4) test_ne(time4_new, time()) ```