Skip to content

Commit

Permalink
Merge pull request #10 from pete88b/GetAttr-cleanup2
Browse files Browse the repository at this point in the history
GetAttr cleanup and doc
  • Loading branch information
jph00 committed Feb 18, 2020
2 parents c1613f9 + 89979ae commit 96de75c
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 33 deletions.
16 changes: 8 additions & 8 deletions fastcore/foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,17 @@ def __call__(self, *args, **kwargs):
class GetAttr:
"Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`"
_default='default'
@property
def _xtra(self): return self._dir()
def _dir(self): return [o for o in dir(getattr(self,self._default)) if not o.startswith('_')]
def _component_attr_filter(self,k):
if k.startswith('__') or k in ('_xtra',self._default): return False
xtra = getattr(self,'_xtra',None)
return xtra is None or k in xtra
def _dir(self): return [k for k in dir(getattr(self,self._default)) if self._component_attr_filter(k)]
def __getattr__(self,k):
if k.startswith('__') or k in ('_xtra',self._default): raise AttributeError(k)
xtra = getattr(self, '_xtra', None)
if xtra is None or k in xtra:
if self._component_attr_filter(k):
attr = getattr(self,self._default,None)
if attr is not None: return getattr(attr, k)
if attr is not None: return getattr(attr,k)
raise AttributeError(k)
def __dir__(self): return custom_dir(self, self._dir() if self._xtra is None else self._dir())
def __dir__(self): return custom_dir(self,self._dir())
# def __getstate__(self): return self.__dict__
def __setstate__(self,data): self.__dict__.update(data)

Expand Down
174 changes: 149 additions & 25 deletions nbs/01_foundation.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -822,21 +822,162 @@
"class GetAttr:\n",
" \"Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`\"\n",
" _default='default'\n",
" @property\n",
" def _xtra(self): return self._dir()\n",
" def _dir(self): return [o for o in dir(getattr(self,self._default)) if not o.startswith('_')]\n",
" def _component_attr_filter(self,k):\n",
" if k.startswith('__') or k in ('_xtra',self._default): return False\n",
" xtra = getattr(self,'_xtra',None)\n",
" return xtra is None or k in xtra\n",
" def _dir(self): return [k for k in dir(getattr(self,self._default)) if self._component_attr_filter(k)]\n",
" def __getattr__(self,k):\n",
" if k.startswith('__') or k in ('_xtra',self._default): raise AttributeError(k)\n",
" xtra = getattr(self, '_xtra', None)\n",
" if xtra is None or k in xtra:\n",
" if self._component_attr_filter(k):\n",
" attr = getattr(self,self._default,None)\n",
" if attr is not None: return getattr(attr, k)\n",
" if attr is not None: return getattr(attr,k)\n",
" raise AttributeError(k)\n",
" def __dir__(self): return custom_dir(self, self._dir() if self._xtra is None else self._dir())\n",
" def __dir__(self): return custom_dir(self,self._dir())\n",
"# def __getstate__(self): return self.__dict__\n",
" def __setstate__(self,data): self.__dict__.update(data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Inherit from `GetAttr` to have attr access passed down to an instance attribute. \n",
"This makes it easy to create composites that don't require callers to know about their components.\n",
"\n",
"You can customise the behaviour of `GetAttr` in subclasses via;\n",
"- `_default`\n",
" - By default, this is set to `'default'`, so attr access is passed down to `self.default`\n",
" - `_default` can be set to the name of any instance attribute that does not start with dunder `__`\n",
"- `_xtra`\n",
" - By default, this is `None`, so all attr access is passed down\n",
" - You can limit which attrs get passed down by setting `_xtra` to a list of attribute names"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class _C(GetAttr):\n",
" # allow all attributes to get passed to `self.default` (by leaving _xtra=None)\n",
" def __init__(self,a): self.default = a\n",
" def foo(self): noop\n",
"\n",
"t = _C('Hi')\n",
"test_eq(t.lower(), 'hi')\n",
"test_eq(t.upper(), 'HI')\n",
"assert 'lower' in dir(t)\n",
"assert 'upper' in dir(t)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class _C(GetAttr):\n",
" _xtra = ['lower'] # specify which attributes get passed to `self.default`\n",
" def __init__(self,a): self.default = a\n",
" def foo(self): noop\n",
"\n",
"t = _C('Hi')\n",
"test_eq(t.default, 'Hi')\n",
"test_eq(t.lower(), 'hi')\n",
"test_fail(lambda: t.upper())\n",
"assert 'lower' in dir(t)\n",
"assert 'upper' not in dir(t)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class _C(GetAttr):\n",
" _default = '_data' # use different component name; `self._data` rather than `self.default`\n",
" def __init__(self,a): self._data = a\n",
" def foo(self): noop\n",
"\n",
"t = _C('Hi')\n",
"test_eq(t._data, 'Hi')\n",
"test_eq(t.lower(), 'hi')\n",
"test_eq(t.upper(), 'HI')\n",
"assert 'lower' in dir(t)\n",
"assert 'upper' in dir(t)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class _C(GetAttr):\n",
" _default = 'data' # use a bad component name; i.e. self.data does not exist\n",
" def __init__(self,a): self.default = a\n",
" def foo(self): noop\n",
"# TODO: should we raise an error when we create a new instance ...\n",
"t = _C('Hi')\n",
"test_eq(t.default, 'Hi')\n",
"# ... or is it enough for all GetAttr features to raise errors\n",
"test_fail(lambda: t.data)\n",
"test_fail(lambda: t.lower())\n",
"test_fail(lambda: t.upper())\n",
"test_fail(lambda: dir(t))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"# I don't think this test is essential to the docs but it probably makes sense to\n",
"# check that everything works when we set both _xtra and _default to non-default values\n",
"class _C(GetAttr):\n",
" _xtra = ['lower', 'upper']\n",
" _default = 'data'\n",
" def __init__(self,a): self.data = a\n",
" def foo(self): noop\n",
"\n",
"t = _C('Hi')\n",
"test_eq(t.data, 'Hi')\n",
"test_eq(t.lower(), 'hi')\n",
"test_eq(t.upper(), 'HI')\n",
"assert 'lower' in dir(t)\n",
"assert 'upper' in dir(t)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"# when consolidating the filter logic, I choose the previous logic from \n",
"# __getattr__ k.startswith('__') rather than\n",
"# _dir k.startswith('_'). \n",
"class _C(GetAttr):\n",
" def __init__(self): self.default = type('_D', (), {'_under': 1, '__dunder': 2})() \n",
" \n",
"t = _C()\n",
"test_eq(t.default._under, 1)\n",
"test_eq(t._under, 1) # _ prefix attr access is allowed on component\n",
"assert '_under' in dir(t)\n",
"\n",
"test_eq(t.default.__dunder, 2)\n",
"test_fail(lambda: t.__dunder) # __ prefix attr access is not allowed on component\n",
"assert '__dunder' not in dir(t)\n",
"\n",
"assert t.__dir__ is not None # __ prefix attr access is allowed on composite\n",
"assert '__dir__' in dir(t)"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -884,23 +1025,6 @@
"#c = pickle.loads(tst)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class _C(GetAttr):\n",
" _xtra = ['lower']\n",
" def __init__(self,a): self.default = a\n",
" def foo(self): noop\n",
"\n",
"t = _C('Hi')\n",
"test_eq(t.lower(), 'hi')\n",
"test_fail(lambda: t.upper())\n",
"assert 'lower' in dir(t)"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down

0 comments on commit 96de75c

Please sign in to comment.