Basic functions used in the fastai library
from fastcore.test import *
from nbdev.showdoc import *
from fastcore.nb_imports import *
import inspect

Foundational Functions

copy_func[source]

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:

import copy as cp
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, you can retrieve a copy of a function without a reference to the original object:

c = copy_func(foo) # c is an indpendent object
assert c is not foo
def g(x, *, y=3):
    return x+y
test_eq(copy_func(g)(4), 7)

patch_to[source]

patch_to(as_prop=False, cls_method=False)

Decorator: add f to cls

The @patch_to decorator allows you to monkey patch a function into a class as a method:

class _T3(int): pass  

@patch_to(_T3)
def func1(self, a): return self+a

t = _T3(1) # we initilized `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:

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 = 'helllo '
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:

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 = False

@patch_to(_T5, as_prop=True)
def add_ten(self): return self + 10

t = _T5(4)
test_eq(t.add_ten, 14)

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:

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)

patch[source]

patch(f=None, as_prop=False, cls_method=False)

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:

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, you can supply a tuple of classes instead of a single class in your type annotations to patch multiple classes:

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 decorator you can use as_propas_prop and cls_method parameters with patch decorator:

@patch(as_prop=True)
def add_ten(self:_T5): return self + 10

t = _T5(4)
test_eq(t.add_ten, 14)
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)

patch_property[source]

patch_property(f)

Deprecated; use patch(as_prop=True) instead

working_directory[source]

working_directory(path)

Change working directory to path and return to previous on exit.

add_docs[source]

add_docs(cls_doc=None, **docs)

Copy values from docs to cls docstrings, and confirm all public methods are documented

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.

Suppose you have the following undocumented class:

class T:
    def foo(self): pass
    def bar(self): pass

You can add documentation to this class like so:

add_docs(T, cls_doc="A docstring for the class.",
            foo="The foo method.",
            bar="The bar method.")

Now, docstrings will appear as expected:

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 also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:

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")

docs[source]

docs()

Decorator version of add_docs, using _docs dict

Instead of using add_docs, you can use the decorator docs as shown below. Note that the docstring for the class can be set with the argument cls_doc:

@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 decorator or the 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:

@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[source]

is_iter(o)

Test whether o can be used in a for loop

assert is_iter([1])
assert not is_iter(array(1))
assert is_iter(array([1,2]))
assert (o for o in range(3))

coll_repr[source]

coll_repr(c, max_n=10)

String repr of up to max_n items of (possibly lazy) collection c

coll_repr is used to provide a more informative __repr__ about list-like objects. coll_repr and is used by L to build a __repr__ that displays the length of a list in addition to a preview of a list.

Below is an example of the __repr__ string created for a list of 1000 elements:

test_eq(coll_repr(range(1000)), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')

We can set the option max_n to optionally preview a specified number of items instead of the default:

test_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]')

is_bool[source]

is_bool(x)

Check whether x is a bool or None

mask2idxs[source]

mask2idxs(mask)

Convert bool mask or index list to index L

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])

cycle[source]

cycle(o)

Like itertools.cycle except creates list of Nones if o is empty

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])

zip_cycle[source]

zip_cycle(x, *args)

Like itertools.zip_longest but cycles through elements of all but first argument

test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])

is_indexer[source]

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:

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.

assert not is_indexer([1, 2])
assert not is_indexer(np.array([[1, 2], [3, 4]]))

Attribute Delegation

class GetAttr[source]

GetAttr()

Inherit from this to have all attr accesses in self._xtra passed down to self.default

Inherit from 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.

You can customise the behaviour of 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, suppose we have the following two classes, _WebPage which is a superclass of _ProductPage, which we wish to compose like so:

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, of course! First, we subclass 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:

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:

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:

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():

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:

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:

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))

delegate_attr[source]

delegate_attr(k, to)

Use in __getattr__ to delegate to attr to without inheriting from GetAttr

delegate_attr is a functional way to delegate attributes, and is an alternative to GetAttr. We recommend reading the documentation of GetAttr for more details around delegation.

You can use achieve delegation when you define __getattr__ by using delegate_attr:

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)

L helpers

class CollBase[source]

CollBase(items)

Base class for composing a list of items

ColBase is a base class that emulates the functionality of a python list:

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__

class L[source]

L(items=None, *rest, use_list=False, match=None) :: GetAttr

Behaves like a list of items but can also index with list of indices or masks

L is a drop in replacement for a python list. Inspired by NumPy, L, supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code. For example, the code below takes a list of pairs, selects the second item of each pair, takes its absolute value, filters items greater than 4, and adds them up:

from fastcore.utils import gt

d = dict(a=1,b=-5,d=6,e=9).items()
test_eq(L(d).itemgot(1).map(abs).filter(gt(4)).sum(), 20) # abs(-5) + abs(6) + abs(9) = 20; 1 was filtered out.

Read this overview section for a quick tutorial of L, as well as background on the name.

You can create an 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.

t = L(range(12))
test_eq(t, list(range(12)))
test_ne(t, list(range(11)))
t.reverse()
test_eq(t[0], 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
(#12) [11,10,9,'j',7,'k',5,4,3,2...]

Any L is a Sequence so you can use it with methods like random.sample:

assert isinstance(t, Sequence)
import random
random.sample(t, 3)
[3, 9, 'j']

There are optimized indexers for arrays, tensors, and DataFrames.

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 with append, +, and *.

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
t = t.map(operator.neg)
test_eq(t,[-1]*5)
test_eq(~L([True,False,False]), L([False,True,True]))
t = L(range(4))
test_eq(zip(t, L(1).cycle()), zip(range(4),(1,1,1,1)))
t = L.range(100)
test_shuffled(t,t.shuffle())
def _f(x,a=0): return x+a
t = L(1)*5
test_eq(t.map(_f), t)
test_eq(t.map(_f,1), [2]*5)
test_eq(t.map(_f,a=2), [3]*5)

An 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.

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.
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 from an existing L then you'll get back the original object (since L uses the NewChkMeta metaclass).

test_is(L(t), t)

An 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.

test_eq(L(['a', 'b']), ['a', 'b'])
test_ne(L(['a', 'b']), 'ab')
test_ne(L(['a', 'b']), {'a':1, 'b':2})

L Methods

L.__getitem__[source]

L.__getitem__(idx)

Retrieve idx (can be list of indices, or mask, or int) items

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)

L.__setitem__[source]

L.__setitem__(idx, o)

Set idx (can be list of indices, or mask, or int) items to o (which is broadcast if not iterable)

t[4,6] = 0
test_eq(t[4,6], [0,0])
t[4,6] = [1,2]
test_eq(t[4,6], [1,2])

L.unique[source]

L.unique()

Unique items, in stable order

test_eq(L(1,2,3,4,4).unique(), [1,2,3,4])

L.val2idx[source]

L.val2idx()

Dict from value to index

test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1})

L.filter[source]

L.filter(f=noop, negate=False, gen=False, **kwargs)

Create new L filtered by predicate f, passing args and kwargs to f

list(t)
[0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11]
test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2])
test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])

L.argwhere[source]

L.argwhere(f, negate=False, **kwargs)

Like filter, but return indices for matching items

test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])

L.map[source]

L.map(f, *args, gen=False, **kwargs)

Create new L with f applied to all items, passing args and kwargs to f

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:

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:

test_eq(L.range(4).map(list('abcd')), list('abcd'))

You can also pass the same arg params that bind accepts:

def f(a=None,b=None): return b
test_eq(L.range(4).map(f, b=arg0), range(4))

L.map_dict[source]

L.map_dict(f=noop, *args, gen=False, **kwargs)

Like map, but creates a dict from items to function results

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})

L.zip[source]

L.zip(cycled=False)

Create new L with zip(*items)

t = L([[1,2,3],'abc'])
test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')])
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')])

L.map_zip[source]

L.map_zip(f, *args, cycled=False, **kwargs)

Combine zip and starmap

t = L([1,2,3],[2,3,4])
test_eq(t.map_zip(operator.mul), [2,6,12])

L.zipwith[source]

L.zipwith(*rest, cycled=False)

Create new L with self zip with each of *rest

b = [[0],[1],[2,2]]
t = L([1,2,3]).zipwith(b)
test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])])

L.map_zipwith[source]

L.map_zipwith(f, *rest, cycled=False, **kwargs)

Combine zipwith and starmap

test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])

L.itemgot[source]

L.itemgot(*idxs)

Create new L with item idx of all items

test_eq(t.itemgot(1), b)

L.attrgot[source]

L.attrgot(k, default=None)

Create new L with attr k (or value k for dicts) of all items.

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])

L.sorted[source]

L.sorted(key=None, reverse=False)

New L sorted by key. If key is str use attrgetter; if int use itemgetter

test_eq(L(a).sorted('a').attrgot('b'), [2,4])

L.split[source]

L.split(s, sep=None, maxsplit=-1)

Class Method: Same as str.split, but returns an L

test_eq(L.split('a b c'), list('abc'))

L.range[source]

L.range(a, b=None, step=None)

Class Method: Same as range, but returns L. Can pass collection for a, to use len(a)

test_eq_type(L.range([1,1,1]), L(range(3)))
test_eq_type(L.range(5,2,2), L(range(5,2,2)))

L.concat[source]

L.concat()

Concatenate all elements of list

test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))

L.copy[source]

L.copy()

Same as list.copy, but returns an L

t = L([0,1,2,3],4,L(5,6)).copy()
test_eq(t.concat(), range(7))

L.map_filter[source]

L.map_filter(f=noop, g=noop, *args, gen=False, **kwargs)

Same as map with f followed by filter with g

t = L(0,1,2,3)
test_eq(t.map_filter(lambda o:o*2, lambda o:o<5), L(0,2,4))

L.map_first[source]

L.map_first(f=noop, g=noop, *args, **kwargs)

First element of map_filter

t = L(0,1,2,3)
test_eq(t.map_first(lambda o:o*2 if o>2 else None), 6)

L.setattrs[source]

L.setattrs(attr, val)

Call setattr on all items

t = L(SimpleNamespace(),SimpleNamespace())
t.setattrs('foo', 'bar')
test_eq(t.attrgot('foo'), ['bar','bar'])

save_config_file[source]

save_config_file(file, d, **kwargs)

Write settings dict to a new config file, or overwrite the existing one.

read_config_file[source]

read_config_file(file, **kwargs)

Config files are saved and read using Python's configparser.ConfigParser, inside the DEFAULT section.

_d = dict(user='fastai', lib_name='fastcore', some_path='test')
try:
    save_config_file('tmp.ini', _d)
    res = read_config_file('tmp.ini')
finally: os.unlink('tmp.ini')
test_eq(res['DEFAULT'], _d)

Config[source]

Config(cfg_name='settings.ini')

Reading and writing settings.ini

Config searches parent directories for a config file, and provides direct access to the 'DEFAULT' section. Keys ending in _path are converted to paths in the config file's directory.

try:
    save_config_file('../tmp.ini', _d)
    cfg = Config('tmp.ini')
finally: os.unlink('../tmp.ini')

test_eq(cfg.user,'fastai')
test_eq(cfg.doc_baseurl,'/fastcore/')
test_eq(cfg.get('some_path'), 'test')
test_eq(cfg.path('some_path'), Path('../test').resolve())
test_eq(cfg.get('foo','bar'),'bar')