Skip to content

Commit 9f13ba7

Browse files
committed
fixes #263
1 parent 37a649c commit 9f13ba7

File tree

5 files changed

+170
-26
lines changed

5 files changed

+170
-26
lines changed

fastcore/_nbdev.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"store_attr": "01_basics.ipynb",
5454
"attrdict": "01_basics.ipynb",
5555
"properties": "01_basics.ipynb",
56+
"camel2words": "01_basics.ipynb",
5657
"camel2snake": "01_basics.ipynb",
5758
"snake2camel": "01_basics.ipynb",
5859
"class2attr": "01_basics.ipynb",
@@ -164,6 +165,7 @@
164165
"sparkline": "03_xtras.ipynb",
165166
"autostart": "03_xtras.ipynb",
166167
"time_events": "03_xtras.ipynb",
168+
"EventTimer": "03_xtras.ipynb",
167169
"stringfmt_names": "03_xtras.ipynb",
168170
"PartialFormatter": "03_xtras.ipynb",
169171
"partial_format": "03_xtras.ipynb",

fastcore/basics.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
'null', 'tonull', 'get_class', 'mk_class', 'wrap_class', 'ignore_exceptions', 'exec_local', 'risinstance',
55
'Inf', 'in_', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'in_',
66
'true', 'stop', 'gen', 'chunked', 'otherwise', 'custom_dir', 'AttrDict', 'type_hints', 'annotations',
7-
'anno_ret', 'argnames', 'with_cast', 'store_attr', 'attrdict', 'properties', 'camel2snake', 'snake2camel',
8-
'class2attr', 'getattrs', 'hasattrs', 'setattrs', 'try_attrs', 'GetAttrBase', 'GetAttr', 'delegate_attr',
9-
'ShowPrint', 'Int', 'Str', 'Float', 'concat', 'detuplify', 'replicate', 'setify', 'merge', 'range_of',
10-
'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex',
11-
'not_', 'argwhere', 'filter_ex', 'range_of', 'renumerate', 'first', 'nested_attr', 'nested_idx', 'val2idx',
12-
'uniqueify', 'num_methods', 'rnum_methods', 'inum_methods', 'fastuple', 'arg0', 'arg1', 'arg2', 'arg3',
13-
'arg4', 'bind', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'Self', 'Self',
14-
'copy_func', 'patch_to', 'patch', 'patch_property', 'ImportEnum', 'StrEnum', 'str_enum', 'Stateful',
7+
'anno_ret', 'argnames', 'with_cast', 'store_attr', 'attrdict', 'properties', 'camel2words', 'camel2snake',
8+
'snake2camel', 'class2attr', 'getattrs', 'hasattrs', 'setattrs', 'try_attrs', 'GetAttrBase', 'GetAttr',
9+
'delegate_attr', 'ShowPrint', 'Int', 'Str', 'Float', 'concat', 'detuplify', 'replicate', 'setify', 'merge',
10+
'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle',
11+
'sorted_ex', 'not_', 'argwhere', 'filter_ex', 'range_of', 'renumerate', 'first', 'nested_attr', 'nested_idx',
12+
'val2idx', 'uniqueify', 'num_methods', 'rnum_methods', 'inum_methods', 'fastuple', 'arg0', 'arg1', 'arg2',
13+
'arg3', 'arg4', 'bind', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'Self',
14+
'Self', 'copy_func', 'patch_to', 'patch', 'patch_property', 'ImportEnum', 'StrEnum', 'str_enum', 'Stateful',
1515
'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'typed']
1616

1717
# Cell
@@ -311,9 +311,15 @@ def properties(cls, *ps):
311311
for p in ps: setattr(cls,p,property(getattr(cls,p)))
312312

313313
# Cell
314+
_c2w_re = re.compile(r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))')
314315
_camel_re1 = re.compile('(.)([A-Z][a-z]+)')
315316
_camel_re2 = re.compile('([a-z0-9])([A-Z])')
316317

318+
# Cell
319+
def camel2words(s, space=' '):
320+
"Convert CamelCase to 'spaced words'"
321+
return re.sub(_c2w_re, rf'{space}\1', s)
322+
317323
# Cell
318324
def camel2snake(name):
319325
"Convert CamelCase to snake_case"

fastcore/xtras.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
__all__ = ['dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'shufflish', 'mapped', 'IterLen', 'ReindexCollection',
44
'maybe_open', 'image_size', 'bunzip', 'join_path_file', 'loads', 'untar_dir', 'repo_details', 'run',
55
'open_file', 'save_pickle', 'load_pickle', 'truncstr', 'spark_chars', 'sparkline', 'autostart',
6-
'time_events', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace',
7-
'round_multiple', 'modified_env', 'ContextManagers', 'str2bool', 'sort_by_run']
6+
'time_events', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local',
7+
'local2utc', 'trace', 'round_multiple', 'modified_env', 'ContextManagers', 'str2bool', 'sort_by_run']
88

99
# Cell
1010
from .imports import *
@@ -13,7 +13,7 @@
1313
from functools import wraps
1414

1515
import mimetypes,pickle,random,json,subprocess,shlex,bz2,gzip,zipfile,tarfile
16-
import imghdr,struct,distutils.util,tempfile,time,string
16+
import imghdr,struct,distutils.util,tempfile,time,string,collections
1717
from contextlib import contextmanager,ExitStack
1818
from pdb import set_trace
1919
from datetime import datetime, timezone
@@ -222,9 +222,9 @@ def __repr__(self:Path):
222222
return f"Path({self.as_posix()!r})"
223223

224224
# Cell
225-
def truncstr(s:str, maxlen:int, suf:str='…')->str:
225+
def truncstr(s:str, maxlen:int, suf:str='…', space='')->str:
226226
"Truncate `s` to length `maxlen`, adding suffix `suf` if truncated"
227-
return s[:maxlen-len(suf)]+suf if len(s)>maxlen else s
227+
return s[:maxlen-len(suf)]+suf if len(s)+len(space)>maxlen else s+space
228228

229229
# Cell
230230
spark_chars = '▁▂▃▅▆▇'
@@ -236,10 +236,10 @@ def _sparkchar(x, mn, incr, empty_zero):
236236
return spark_chars[res]
237237

238238
# Cell
239-
def sparkline(data, empty_zero=False):
239+
def sparkline(data, mn=None, mx=None, empty_zero=False):
240240
"Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column"
241241
valid = [o for o in data if o is not None]
242-
mn,mx,n = min(valid),max(valid),len(spark_chars)
242+
mn,mx,n = ifnone(mn,min(valid)),ifnone(mx,max(valid)),len(spark_chars)
243243
res = [_sparkchar(o,mn,(mx-mn)/n,empty_zero) for o in data]
244244
return ''.join(res)
245245

@@ -260,6 +260,28 @@ def time_events():
260260
start,events = default_timer(),0
261261
while True: events += (yield events,events/(default_timer()-start)) or 0
262262

263+
# Cell
264+
class EventTimer:
265+
"An event timer with history of `store` items of time `span`"
266+
def __init__(self, store=5, span=60):
267+
self.hist,self.span,self.last = collections.deque(maxlen=store),span,default_timer()
268+
self._reset()
269+
270+
def _reset(self): self.start,self.events = self.last,0
271+
272+
def add(self, n=1):
273+
"Record `n` events"
274+
if self.duration>self.span:
275+
self.hist.append(self.freq)
276+
self._reset()
277+
self.events +=n
278+
self.last = default_timer()
279+
280+
@property
281+
def duration(self): return default_timer()-self.start
282+
@property
283+
def freq(self): return self.events/self.duration
284+
263285
# Cell
264286
_fmt = string.Formatter()
265287

nbs/01_basics.ipynb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,10 +1925,32 @@
19251925
"outputs": [],
19261926
"source": [
19271927
"#export\n",
1928+
"_c2w_re = re.compile(r'((?<=[a-z])[A-Z]|(?<!\\A)[A-Z](?=[a-z]))')\n",
19281929
"_camel_re1 = re.compile('(.)([A-Z][a-z]+)')\n",
19291930
"_camel_re2 = re.compile('([a-z0-9])([A-Z])')"
19301931
]
19311932
},
1933+
{
1934+
"cell_type": "code",
1935+
"execution_count": null,
1936+
"metadata": {},
1937+
"outputs": [],
1938+
"source": [
1939+
"#export\n",
1940+
"def camel2words(s, space=' '):\n",
1941+
" \"Convert CamelCase to 'spaced words'\"\n",
1942+
" return re.sub(_c2w_re, rf'{space}\\1', s)"
1943+
]
1944+
},
1945+
{
1946+
"cell_type": "code",
1947+
"execution_count": null,
1948+
"metadata": {},
1949+
"outputs": [],
1950+
"source": [
1951+
"test_eq(camel2words('ClassAreCamel'), 'Class Are Camel')"
1952+
]
1953+
},
19321954
{
19331955
"cell_type": "code",
19341956
"execution_count": null,

nbs/03_xtras.ipynb

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"from functools import wraps\n",
2323
"\n",
2424
"import mimetypes,pickle,random,json,subprocess,shlex,bz2,gzip,zipfile,tarfile\n",
25-
"import imghdr,struct,distutils.util,tempfile,time,string\n",
25+
"import imghdr,struct,distutils.util,tempfile,time,string,collections\n",
2626
"from contextlib import contextmanager,ExitStack\n",
2727
"from pdb import set_trace\n",
2828
"from datetime import datetime, timezone\n",
@@ -618,7 +618,7 @@
618618
{
619619
"data": {
620620
"text/plain": [
621-
"['d', 'c', 'h', 'a', 'b', 'g', 'e', 'f']"
621+
"['a', 'e', 'h', 'b', 'g', 'd', 'c', 'f']"
622622
]
623623
},
624624
"execution_count": null,
@@ -1326,9 +1326,9 @@
13261326
"outputs": [],
13271327
"source": [
13281328
"#export\n",
1329-
"def truncstr(s:str, maxlen:int, suf:str='…')->str:\n",
1329+
"def truncstr(s:str, maxlen:int, suf:str='…', space='')->str:\n",
13301330
" \"Truncate `s` to length `maxlen`, adding suffix `suf` if truncated\"\n",
1331-
" return s[:maxlen-len(suf)]+suf if len(s)>maxlen else s"
1331+
" return s[:maxlen-len(suf)]+suf if len(s)+len(space)>maxlen else s+space"
13321332
]
13331333
},
13341334
{
@@ -1338,9 +1338,11 @@
13381338
"outputs": [],
13391339
"source": [
13401340
"w = 'abacadabra'\n",
1341-
"test_eq(truncstr(w, 15), w)\n",
1341+
"test_eq(truncstr(w, 10), w)\n",
13421342
"test_eq(truncstr(w, 5), 'abac…')\n",
13431343
"test_eq(truncstr(w, 5, suf=''), 'abaca')\n",
1344+
"test_eq(truncstr(w, 11, space='_'), w+\"_\")\n",
1345+
"test_eq(truncstr(w, 10, space='_'), w[:-1]+'…')\n",
13441346
"test_eq(truncstr(w, 5, suf='!!'), 'aba!!')"
13451347
]
13461348
},
@@ -1374,10 +1376,10 @@
13741376
"outputs": [],
13751377
"source": [
13761378
"#export\n",
1377-
"def sparkline(data, empty_zero=False):\n",
1379+
"def sparkline(data, mn=None, mx=None, empty_zero=False):\n",
13781380
" \"Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column\"\n",
13791381
" valid = [o for o in data if o is not None]\n",
1380-
" mn,mx,n = min(valid),max(valid),len(spark_chars)\n",
1382+
" mn,mx,n = ifnone(mn,min(valid)),ifnone(mx,max(valid)),len(spark_chars)\n",
13811383
" res = [_sparkchar(o,mn,(mx-mn)/n,empty_zero) for o in data]\n",
13821384
" return ''.join(res)"
13831385
]
@@ -1440,6 +1442,16 @@
14401442
"This is convenient for tracking the frequency of events. Call `send(n)` any time you want to add `n` events to the counter. Pass the object to `next()` to get a tuple of the number of events and the frequency/second."
14411443
]
14421444
},
1445+
{
1446+
"cell_type": "code",
1447+
"execution_count": null,
1448+
"metadata": {},
1449+
"outputs": [],
1450+
"source": [
1451+
"# Random wait function for testing `time_events`\n",
1452+
"def _randwait(): yield from (sleep(random.random()/200) for _ in range(100))"
1453+
]
1454+
},
14431455
{
14441456
"cell_type": "code",
14451457
"execution_count": null,
@@ -1449,21 +1461,101 @@
14491461
"name": "stdout",
14501462
"output_type": "stream",
14511463
"text": [
1452-
"# Events: 10, Freq/sec: 80.30\n"
1464+
"# Events: 100, Freq/sec: 396.51\n"
14531465
]
14541466
}
14551467
],
14561468
"source": [
1457-
"# Random wait function for testing `time_events`\n",
1458-
"def _randwait(): yield from (sleep(random.random()/30) for _ in range(10))\n",
1459-
"\n",
14601469
"c = time_events() # Start timer\n",
14611470
"for o in _randwait(): c.send(1) # Send an event\n",
14621471
"events,freq = next(c) # Return counter values\n",
14631472
"c.close() # Close when done\n",
14641473
"print(f'# Events: {events}, Freq/sec: {freq:.02f}')"
14651474
]
14661475
},
1476+
{
1477+
"cell_type": "code",
1478+
"execution_count": null,
1479+
"metadata": {},
1480+
"outputs": [],
1481+
"source": [
1482+
"#export\n",
1483+
"class EventTimer:\n",
1484+
" \"An event timer with history of `store` items of time `span`\"\n",
1485+
" def __init__(self, store=5, span=60):\n",
1486+
" self.hist,self.span,self.last = collections.deque(maxlen=store),span,default_timer()\n",
1487+
" self._reset()\n",
1488+
"\n",
1489+
" def _reset(self): self.start,self.events = self.last,0\n",
1490+
"\n",
1491+
" def add(self, n=1):\n",
1492+
" \"Record `n` events\"\n",
1493+
" if self.duration>self.span:\n",
1494+
" self.hist.append(self.freq)\n",
1495+
" self._reset()\n",
1496+
" self.events +=n\n",
1497+
" self.last = default_timer()\n",
1498+
" \n",
1499+
" @property\n",
1500+
" def duration(self): return default_timer()-self.start\n",
1501+
" @property\n",
1502+
" def freq(self): return self.events/self.duration"
1503+
]
1504+
},
1505+
{
1506+
"cell_type": "code",
1507+
"execution_count": null,
1508+
"metadata": {},
1509+
"outputs": [
1510+
{
1511+
"data": {
1512+
"text/markdown": [
1513+
"<h4 id=\"EventTimer\" class=\"doc_header\"><code>class</code> <code>EventTimer</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
1514+
"\n",
1515+
"> <code>EventTimer</code>(**`store`**=*`5`*, **`span`**=*`60`*)\n",
1516+
"\n",
1517+
"An event timer with history of `store` items of time `span`"
1518+
],
1519+
"text/plain": [
1520+
"<IPython.core.display.Markdown object>"
1521+
]
1522+
},
1523+
"metadata": {},
1524+
"output_type": "display_data"
1525+
}
1526+
],
1527+
"source": [
1528+
"show_doc(EventTimer, title_level=4)"
1529+
]
1530+
},
1531+
{
1532+
"cell_type": "markdown",
1533+
"metadata": {},
1534+
"source": [
1535+
"Add events with `add`, and get number of `events` and their frequency (`freq`)."
1536+
]
1537+
},
1538+
{
1539+
"cell_type": "code",
1540+
"execution_count": null,
1541+
"metadata": {},
1542+
"outputs": [
1543+
{
1544+
"name": "stdout",
1545+
"output_type": "stream",
1546+
"text": [
1547+
"Num Events: 5, Freq/sec: 646.7\n",
1548+
"Most recent: ▁▇▃▇▆ 259.5 450.4 362.1 441.0 427.9\n"
1549+
]
1550+
}
1551+
],
1552+
"source": [
1553+
"c = EventTimer(store=5, span=0.03)\n",
1554+
"for o in _randwait(): c.add(1)\n",
1555+
"print(f'Num Events: {c.events}, Freq/sec: {c.freq:.01f}')\n",
1556+
"print('Most recent: ', sparkline(c.hist), *L(c.hist).map('{:.01f}'))"
1557+
]
1558+
},
14671559
{
14681560
"cell_type": "code",
14691561
"execution_count": null,

0 commit comments

Comments
 (0)