Skip to content

Commit 3be0a0c

Browse files
committed
fixes #774
1 parent 61b01d2 commit 3be0a0c

3 files changed

Lines changed: 176 additions & 12 deletions

File tree

fastcore/_modidx.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
'fastcore/basics.py'),
1919
'fastcore.basics.AttrDictDefault.__init__': ('basics.html#attrdictdefault.__init__', 'fastcore/basics.py'),
2020
'fastcore.basics.BasicRepr': ('basics.html#basicrepr', 'fastcore/basics.py'),
21+
'fastcore.basics.DepProp': ('basics.html#depprop', 'fastcore/basics.py'),
22+
'fastcore.basics.DepProp.__delete__': ('basics.html#depprop.__delete__', 'fastcore/basics.py'),
23+
'fastcore.basics.DepProp.__get__': ('basics.html#depprop.__get__', 'fastcore/basics.py'),
24+
'fastcore.basics.DepProp.__init__': ('basics.html#depprop.__init__', 'fastcore/basics.py'),
25+
'fastcore.basics.DepProp.__set__': ('basics.html#depprop.__set__', 'fastcore/basics.py'),
26+
'fastcore.basics.DepProp.__set_name__': ('basics.html#depprop.__set_name__', 'fastcore/basics.py'),
27+
'fastcore.basics.DepProp.norm': ('basics.html#depprop.norm', 'fastcore/basics.py'),
2128
'fastcore.basics.Float': ('basics.html#float', 'fastcore/basics.py'),
2229
'fastcore.basics.GetAttr': ('basics.html#getattr', 'fastcore/basics.py'),
2330
'fastcore.basics.GetAttr.__dir__': ('basics.html#getattr.__dir__', 'fastcore/basics.py'),

fastcore/basics.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
'custom_dir', 'AttrDict', 'AttrDictDefault', 'NS', 'get_annotations_ex', 'eval_type', 'type_hints',
1111
'annotations', 'anno_ret', 'signature_ex', 'union2tuple', 'argnames', 'with_cast', 'store_attr', 'attrdict',
1212
'properties', 'camel2words', 'camel2snake', 'snake2camel', 'class2attr', 'getcallable', 'getattrs',
13-
'hasattrs', 'setattrs', 'try_attrs', 'GetAttrBase', 'GetAttr', 'delegate_attr', 'ShowPrint', 'Int', 'Str',
14-
'Float', 'partition', 'partition_dict', 'flatten', 'concat', 'strcat', 'detuplify', 'replicate', 'setify',
15-
'merge', 'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle',
16-
'zip_cycle', 'sorted_ex', 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'last', 'only',
17-
'nested_attr', 'nested_setdefault', 'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx',
18-
'uniqueify', 'loop_first_last', 'loop_first', 'loop_last', 'first_match', 'last_match', 'joins', 'fastuple',
19-
'bind', 'mapt', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'negate', 'spread',
20-
'dspread', 'copy_func', 'patch_to', 'patch', 'compile_re', 'ImportEnum', 'StrEnum', 'str_enum', 'ValEnum',
21-
'Stateful', 'NotStr', 'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'str2bool', 'str2int',
22-
'str2float', 'str2list', 'str2date', 'to_bool', 'to_int', 'to_float', 'to_list', 'to_date', 'typed',
23-
'exec_new', 'exec_import', 'sig_with_params', 'fdelegates', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub',
24-
'mul', 'truediv', 'is_', 'is_not', 'mod']
13+
'hasattrs', 'setattrs', 'try_attrs', 'DepProp', 'GetAttrBase', 'GetAttr', 'delegate_attr', 'ShowPrint',
14+
'Int', 'Str', 'Float', 'partition', 'partition_dict', 'flatten', 'concat', 'strcat', 'detuplify',
15+
'replicate', 'setify', 'merge', 'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys',
16+
'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first',
17+
'last', 'only', 'nested_attr', 'nested_setdefault', 'nested_callable', 'nested_idx', 'set_nested_idx',
18+
'val2idx', 'uniqueify', 'loop_first_last', 'loop_first', 'loop_last', 'first_match', 'last_match', 'joins',
19+
'fastuple', 'bind', 'mapt', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'negate',
20+
'spread', 'dspread', 'copy_func', 'patch_to', 'patch', 'compile_re', 'ImportEnum', 'StrEnum', 'str_enum',
21+
'ValEnum', 'Stateful', 'NotStr', 'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'str2bool',
22+
'str2int', 'str2float', 'str2list', 'str2date', 'to_bool', 'to_int', 'to_float', 'to_list', 'to_date',
23+
'typed', 'exec_new', 'exec_import', 'sig_with_params', 'fdelegates', 'lt', 'gt', 'le', 'ge', 'eq', 'ne',
24+
'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'mod']
2525

2626
# %% ../nbs/01_basics.ipynb #0e91ed82
2727
from .imports import *
@@ -520,6 +520,31 @@ def try_attrs(obj, *attrs):
520520
except: pass
521521
raise AttributeError(attrs)
522522

523+
# %% ../nbs/01_basics.ipynb #df28aacb
524+
class DepProp:
525+
"Property decorator with dependency update triggering"
526+
def __init__(self, fchange, fnorm=None): self.fchange, self.fnorm = fchange, fnorm
527+
def __set_name__(self, owner, name): self.attr = f'_{name}'
528+
529+
def norm(self, fn):
530+
self.fnorm = fn
531+
return self
532+
533+
def __get__(self, o, objtype=None):
534+
if o is None: return self
535+
return getattr(o, self.attr, None)
536+
537+
def __set__(self, o, v):
538+
if self.fnorm: v = self.fnorm(o, v)
539+
change = not hasattr(o, self.attr) or v!=self.__get__(o)
540+
setattr(o, self.attr, v)
541+
if change: self.fchange(o)
542+
543+
def __delete__(self, o):
544+
if hasattr(o, self.attr):
545+
delattr(o, self.attr)
546+
self.fchange(o)
547+
523548
# %% ../nbs/01_basics.ipynb #8c571e08
524549
class GetAttrBase:
525550
"Basic delegation of `__getattr__` and `__dir__`"

nbs/01_basics.ipynb

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3095,6 +3095,138 @@
30953095
"test_eq(try_attrs(1, 'foobar', 'real'), 1)"
30963096
]
30973097
},
3098+
{
3099+
"cell_type": "code",
3100+
"execution_count": null,
3101+
"id": "df28aacb",
3102+
"metadata": {},
3103+
"outputs": [],
3104+
"source": [
3105+
"#| export\n",
3106+
"class DepProp:\n",
3107+
" \"Property decorator with dependency update triggering\"\n",
3108+
" def __init__(self, fchange, fnorm=None): self.fchange, self.fnorm = fchange, fnorm\n",
3109+
" def __set_name__(self, owner, name): self.attr = f'_{name}'\n",
3110+
"\n",
3111+
" def norm(self, fn):\n",
3112+
" self.fnorm = fn\n",
3113+
" return self\n",
3114+
"\n",
3115+
" def __get__(self, o, objtype=None):\n",
3116+
" if o is None: return self\n",
3117+
" return getattr(o, self.attr, None)\n",
3118+
"\n",
3119+
" def __set__(self, o, v):\n",
3120+
" if self.fnorm: v = self.fnorm(o, v)\n",
3121+
" change = not hasattr(o, self.attr) or v!=self.__get__(o)\n",
3122+
" setattr(o, self.attr, v)\n",
3123+
" if change: self.fchange(o)\n",
3124+
"\n",
3125+
" def __delete__(self, o):\n",
3126+
" if hasattr(o, self.attr):\n",
3127+
" delattr(o, self.attr)\n",
3128+
" self.fchange(o)"
3129+
]
3130+
},
3131+
{
3132+
"cell_type": "markdown",
3133+
"id": "f77929c0",
3134+
"metadata": {},
3135+
"source": [
3136+
"`DepProp` is a descriptor that stores a value and calls a \"change\" function whenever that value changes or is deleted. This is useful for invalidating caches or triggering side effects when a dependency is updated. An optional normalizer can preprocess values before storage."
3137+
]
3138+
},
3139+
{
3140+
"cell_type": "code",
3141+
"execution_count": null,
3142+
"id": "0ce0f8d2",
3143+
"metadata": {},
3144+
"outputs": [],
3145+
"source": [
3146+
"class Square:\n",
3147+
" @DepProp\n",
3148+
" def width(self): self._area_cache = None\n",
3149+
"\n",
3150+
" @property\n",
3151+
" def area(self):\n",
3152+
" if self._area_cache is None: self._area_cache = self.width**2\n",
3153+
" return self._area_cache\n",
3154+
"\n",
3155+
"r = Square()\n",
3156+
"r.width = 3\n",
3157+
"test_eq(r.area, 9)\n",
3158+
"r.width = 5 # cache cleared automatically\n",
3159+
"test_eq(r.area, 25)"
3160+
]
3161+
},
3162+
{
3163+
"cell_type": "markdown",
3164+
"id": "c86ca394",
3165+
"metadata": {},
3166+
"source": [
3167+
"Use the `.norm` decorator to add a normalizer that preprocesses values before storage:"
3168+
]
3169+
},
3170+
{
3171+
"cell_type": "code",
3172+
"execution_count": null,
3173+
"id": "cde333c9",
3174+
"metadata": {},
3175+
"outputs": [],
3176+
"source": [
3177+
"class T:\n",
3178+
" @DepProp\n",
3179+
" def name(self): self.log = f'changed to {self.name}'\n",
3180+
"\n",
3181+
" @name.norm\n",
3182+
" def name(self, v): return v.strip().lower()\n",
3183+
"\n",
3184+
"t = T()\n",
3185+
"t.name = ' Hello '\n",
3186+
"test_eq(t.name, 'hello')\n",
3187+
"test_eq(t.log, 'changed to hello')"
3188+
]
3189+
},
3190+
{
3191+
"cell_type": "markdown",
3192+
"id": "27e77787",
3193+
"metadata": {},
3194+
"source": [
3195+
"Setting the same value again does *not* trigger the change function:"
3196+
]
3197+
},
3198+
{
3199+
"cell_type": "code",
3200+
"execution_count": null,
3201+
"id": "a898265d",
3202+
"metadata": {},
3203+
"outputs": [],
3204+
"source": [
3205+
"t.log = 'not called'\n",
3206+
"t.name = 'hello' # same value -- no change\n",
3207+
"test_eq(t.log, 'not called')"
3208+
]
3209+
},
3210+
{
3211+
"cell_type": "markdown",
3212+
"id": "ef04f745",
3213+
"metadata": {},
3214+
"source": [
3215+
"Deleting the property removes the backing attribute and calls `fchange`:"
3216+
]
3217+
},
3218+
{
3219+
"cell_type": "code",
3220+
"execution_count": null,
3221+
"id": "9871488b",
3222+
"metadata": {},
3223+
"outputs": [],
3224+
"source": [
3225+
"del t.name\n",
3226+
"test_eq(t.log, 'changed to None')\n",
3227+
"test_eq(t.name, None)"
3228+
]
3229+
},
30983230
{
30993231
"cell_type": "markdown",
31003232
"id": "cf4b62e1",

0 commit comments

Comments
 (0)