Skip to content

Commit 4560a8b

Browse files
committed
fixes #652
1 parent 960cc2b commit 4560a8b

File tree

6 files changed

+150
-21
lines changed

6 files changed

+150
-21
lines changed

fastcore/_modidx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@
346346
'fastcore.foundation.L.cycle': ('foundation.html#l.cycle', 'fastcore/foundation.py'),
347347
'fastcore.foundation.L.enumerate': ('foundation.html#l.enumerate', 'fastcore/foundation.py'),
348348
'fastcore.foundation.L.filter': ('foundation.html#l.filter', 'fastcore/foundation.py'),
349+
'fastcore.foundation.L.groupby': ('foundation.html#l.groupby', 'fastcore/foundation.py'),
349350
'fastcore.foundation.L.itemgot': ('foundation.html#l.itemgot', 'fastcore/foundation.py'),
350351
'fastcore.foundation.L.map': ('foundation.html#l.map', 'fastcore/foundation.py'),
351352
'fastcore.foundation.L.map_dict': ('foundation.html#l.map_dict', 'fastcore/foundation.py'),
@@ -360,6 +361,7 @@
360361
'fastcore.foundation.L.shuffle': ('foundation.html#l.shuffle', 'fastcore/foundation.py'),
361362
'fastcore.foundation.L.sorted': ('foundation.html#l.sorted', 'fastcore/foundation.py'),
362363
'fastcore.foundation.L.split': ('foundation.html#l.split', 'fastcore/foundation.py'),
364+
'fastcore.foundation.L.splitlines': ('foundation.html#l.splitlines', 'fastcore/foundation.py'),
363365
'fastcore.foundation.L.starmap': ('foundation.html#l.starmap', 'fastcore/foundation.py'),
364366
'fastcore.foundation.L.sum': ('foundation.html#l.sum', 'fastcore/foundation.py'),
365367
'fastcore.foundation.L.unique': ('foundation.html#l.unique', 'fastcore/foundation.py'),

fastcore/basics.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
# %% ../nbs/01_basics.ipynb
2626
from .imports import *
2727
import ast,builtins,pprint,types,typing
28+
from functools import cmp_to_key
2829
from copy import copy
2930
from datetime import date
3031
try: from types import UnionType
@@ -660,9 +661,13 @@ def zip_cycle(x, *args):
660661
return zip(x, *map(cycle,args))
661662

662663
# %% ../nbs/01_basics.ipynb
663-
def sorted_ex(iterable, key=None, reverse=False):
664-
"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`"
665-
if isinstance(key,str): k=lambda o:getattr(o,key,0)
664+
def sorted_ex(iterable, key=None, reverse=False, cmp=None, **kwargs):
665+
"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`; use `cmp` comparator function or `key` with `kwargs`"
666+
if callable(key) and kwargs: k=partial(key, **kwargs)
667+
elif callable(cmp):
668+
if kwargs: cmp=partial(cmp, **kwargs)
669+
k = cmp_to_key(cmp)
670+
elif isinstance(key,str): k=lambda o:getattr(o,key,0)
666671
elif isinstance(key,int): k=itemgetter(key)
667672
else: k=key
668673
return sorted(iterable, key=k, reverse=reverse)

fastcore/foundation.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def __eq__(self,b):
136136
if isinstance(b, (str,dict)) or callable(b): return False
137137
return all_equal(b,self)
138138

139-
def sorted(self, key=None, reverse=False): return self._new(sorted_ex(self, key=key, reverse=reverse))
139+
def sorted(self, key=None, reverse=False, cmp=None, **kwargs): return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs))
140140
def __iter__(self): return iter(self.items.itertuples() if hasattr(self.items,'iloc') else self.items)
141141
def __contains__(self,b): return b in self.items
142142
def __reversed__(self): return self._new(reversed(self.items))
@@ -154,6 +154,8 @@ def __addi__(a,b):
154154
@classmethod
155155
def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit))
156156
@classmethod
157+
def splitlines(cls, s, keepends=False): return cls(s.splitslines(keepends))
158+
@classmethod
157159
def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step))
158160

159161
def map(self, f, *args, **kwargs): return self._new(map_ex(self, f, *args, gen=False, **kwargs))
@@ -169,6 +171,7 @@ def renumerate(self): return L(renumerate(self))
169171
def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start))
170172
def val2idx(self): return val2idx(self)
171173
def cycle(self): return cycle(self)
174+
def groupby(self, key, val=noop): return L(groupby(self, key, val=val))
172175
def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self}
173176
def map_first(self, f=noop, g=noop, *args, **kwargs):
174177
return first(self.map(f, *args, **kwargs), g)
@@ -201,8 +204,9 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self]
201204
__getitem__="Retrieve `idx` (can be list of indices, or mask, or int) items",
202205
range="Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`",
203206
split="Class Method: Same as `str.split`, but returns an `L`",
207+
splitlines="Class Method: Same as `str.splitlines`, but returns an `L`",
204208
copy="Same as `list.copy`, but returns an `L`",
205-
sorted="New `L` sorted by `key`. If key is str use `attrgetter`; if int use `itemgetter`",
209+
sorted="New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`",
206210
unique="Unique items, in stable order",
207211
val2idx="Dict from value to index",
208212
filter="Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`",
@@ -217,6 +221,7 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self]
217221
cycle="Same as `itertools.cycle`",
218222
enumerate="Same as `enumerate`",
219223
renumerate="Same as `renumerate`",
224+
groupby="Same as `groupby`",
220225
zip="Create new `L` with `zip(*items)`",
221226
zipwith="Create new `L` with `self` zip with each of `*rest`",
222227
map_zip="Combine `zip` and `starmap`",
@@ -226,8 +231,7 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self]
226231
reduce="Wrapper for `functools.reduce`",
227232
sum="Sum of the items",
228233
product="Product of the items",
229-
setattrs="Call `setattr` on all items"
230-
)
234+
setattrs="Call `setattr` on all items")
231235

232236
# %% ../nbs/02_foundation.ipynb
233237
# Here we are fixing the signature of L. What happens is that the __call__ method on the MetaClass of L shadows the __init__

nbs/01_basics.ipynb

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"#|export\n",
1919
"from fastcore.imports import *\n",
2020
"import ast,builtins,pprint,types,typing\n",
21+
"from functools import cmp_to_key\n",
2122
"from copy import copy\n",
2223
"from datetime import date\n",
2324
"try: from types import UnionType\n",
@@ -671,12 +672,20 @@
671672
"execution_count": null,
672673
"metadata": {},
673674
"outputs": [
675+
{
676+
"name": "stderr",
677+
"output_type": "stream",
678+
"text": [
679+
"/Users/jhoward/miniforge3/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
680+
" from .autonotebook import tqdm as notebook_tqdm\n"
681+
]
682+
},
674683
{
675684
"data": {
676685
"text/markdown": [
677686
"---\n",
678687
"\n",
679-
"[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
688+
"[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
680689
"\n",
681690
"### get_class\n",
682691
"\n",
@@ -688,7 +697,7 @@
688697
"text/plain": [
689698
"---\n",
690699
"\n",
691-
"[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
700+
"[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
692701
"\n",
693702
"### get_class\n",
694703
"\n",
@@ -866,7 +875,7 @@
866875
"text/markdown": [
867876
"---\n",
868877
"\n",
869-
"[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
878+
"[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
870879
"\n",
871880
"#### ignore_exceptions\n",
872881
"\n",
@@ -877,7 +886,7 @@
877886
"text/plain": [
878887
"---\n",
879888
"\n",
880-
"[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
889+
"[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
881890
"\n",
882891
"#### ignore_exceptions\n",
883892
"\n",
@@ -2901,7 +2910,7 @@
29012910
"text/markdown": [
29022911
"---\n",
29032912
"\n",
2904-
"[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
2913+
"[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
29052914
"\n",
29062915
"#### GetAttr\n",
29072916
"\n",
@@ -2912,7 +2921,7 @@
29122921
"text/plain": [
29132922
"---\n",
29142923
"\n",
2915-
"[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
2924+
"[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
29162925
"\n",
29172926
"#### GetAttr\n",
29182927
"\n",
@@ -3833,14 +3842,119 @@
38333842
"outputs": [],
38343843
"source": [
38353844
"#|export\n",
3836-
"def sorted_ex(iterable, key=None, reverse=False):\n",
3837-
" \"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`\"\n",
3838-
" if isinstance(key,str): k=lambda o:getattr(o,key,0)\n",
3845+
"def sorted_ex(iterable, key=None, reverse=False, cmp=None, **kwargs):\n",
3846+
" \"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`; use `cmp` comparator function or `key` with `kwargs`\"\n",
3847+
" if callable(key) and kwargs: k=partial(key, **kwargs)\n",
3848+
" elif callable(cmp):\n",
3849+
" if kwargs: cmp=partial(cmp, **kwargs)\n",
3850+
" k = cmp_to_key(cmp)\n",
3851+
" elif isinstance(key,str): k=lambda o:getattr(o,key,0)\n",
38393852
" elif isinstance(key,int): k=itemgetter(key)\n",
38403853
" else: k=key\n",
38413854
" return sorted(iterable, key=k, reverse=reverse)"
38423855
]
38433856
},
3857+
{
3858+
"cell_type": "markdown",
3859+
"metadata": {},
3860+
"source": [
3861+
"Attributes can be used for sorting by passing their name as a string:"
3862+
]
3863+
},
3864+
{
3865+
"cell_type": "code",
3866+
"execution_count": null,
3867+
"metadata": {},
3868+
"outputs": [],
3869+
"source": [
3870+
"class TestObj:\n",
3871+
" def __init__(self, x): self.x = x\n",
3872+
"objs = [TestObj(i) for i in [3,1,2]]\n",
3873+
"test_eq([o.x for o in sorted_ex(objs, 'x')], [1,2,3])"
3874+
]
3875+
},
3876+
{
3877+
"cell_type": "markdown",
3878+
"metadata": {},
3879+
"source": [
3880+
"Tuple/list items can be sorted by index position:"
3881+
]
3882+
},
3883+
{
3884+
"cell_type": "code",
3885+
"execution_count": null,
3886+
"metadata": {},
3887+
"outputs": [],
3888+
"source": [
3889+
"items = [(1,'c'), (2,'b'), (3,'a')]\n",
3890+
"test_eq(sorted_ex(items, 1), [(3,'a'), (2,'b'), (1,'c')])"
3891+
]
3892+
},
3893+
{
3894+
"cell_type": "markdown",
3895+
"metadata": {},
3896+
"source": [
3897+
"A custom key function transforms values:"
3898+
]
3899+
},
3900+
{
3901+
"cell_type": "code",
3902+
"execution_count": null,
3903+
"metadata": {},
3904+
"outputs": [],
3905+
"source": [
3906+
"test_eq(sorted_ex([3,1,2], lambda x: -x), [3,2,1])"
3907+
]
3908+
},
3909+
{
3910+
"cell_type": "markdown",
3911+
"metadata": {},
3912+
"source": [
3913+
"You can use a comparison function (returning -1/1/0):"
3914+
]
3915+
},
3916+
{
3917+
"cell_type": "code",
3918+
"execution_count": null,
3919+
"metadata": {},
3920+
"outputs": [],
3921+
"source": [
3922+
"test_eq(sorted_ex([3,1,2], cmp=lambda a,b: 1 if a>b else -1 if a<b else 0), [1,2,3])"
3923+
]
3924+
},
3925+
{
3926+
"cell_type": "markdown",
3927+
"metadata": {},
3928+
"source": [
3929+
"Additional parameters can be passed to key/cmp functions:"
3930+
]
3931+
},
3932+
{
3933+
"cell_type": "code",
3934+
"execution_count": null,
3935+
"metadata": {},
3936+
"outputs": [],
3937+
"source": [
3938+
"def key_with_kwargs(x, offset=0): return x + offset\n",
3939+
"test_eq(sorted_ex([3,1,2], key=key_with_kwargs, offset=10), [1,2,3])"
3940+
]
3941+
},
3942+
{
3943+
"cell_type": "markdown",
3944+
"metadata": {},
3945+
"source": [
3946+
"Reverse sort capability:"
3947+
]
3948+
},
3949+
{
3950+
"cell_type": "code",
3951+
"execution_count": null,
3952+
"metadata": {},
3953+
"outputs": [],
3954+
"source": [
3955+
"test_eq(sorted_ex([1,2,3], reverse=True), [3,2,1])"
3956+
]
3957+
},
38443958
{
38453959
"cell_type": "code",
38463960
"execution_count": null,

nbs/02_foundation.ipynb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@
575575
" if isinstance(b, (str,dict)) or callable(b): return False\n",
576576
" return all_equal(b,self)\n",
577577
"\n",
578-
" def sorted(self, key=None, reverse=False): return self._new(sorted_ex(self, key=key, reverse=reverse))\n",
578+
" def sorted(self, key=None, reverse=False, cmp=None, **kwargs): return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs))\n",
579579
" def __iter__(self): return iter(self.items.itertuples() if hasattr(self.items,'iloc') else self.items)\n",
580580
" def __contains__(self,b): return b in self.items\n",
581581
" def __reversed__(self): return self._new(reversed(self.items))\n",
@@ -593,6 +593,8 @@
593593
" @classmethod\n",
594594
" def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit))\n",
595595
" @classmethod\n",
596+
" def splitlines(cls, s, keepends=False): return cls(s.splitslines(keepends))\n",
597+
" @classmethod\n",
596598
" def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step))\n",
597599
"\n",
598600
" def map(self, f, *args, **kwargs): return self._new(map_ex(self, f, *args, gen=False, **kwargs))\n",
@@ -608,6 +610,7 @@
608610
" def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start))\n",
609611
" def val2idx(self): return val2idx(self)\n",
610612
" def cycle(self): return cycle(self)\n",
613+
" def groupby(self, key, val=noop): return L(groupby(self, key, val=val))\n",
611614
" def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self}\n",
612615
" def map_first(self, f=noop, g=noop, *args, **kwargs):\n",
613616
" return first(self.map(f, *args, **kwargs), g)\n",
@@ -647,8 +650,9 @@
647650
" __getitem__=\"Retrieve `idx` (can be list of indices, or mask, or int) items\",\n",
648651
" range=\"Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`\",\n",
649652
" split=\"Class Method: Same as `str.split`, but returns an `L`\",\n",
653+
" splitlines=\"Class Method: Same as `str.splitlines`, but returns an `L`\",\n",
650654
" copy=\"Same as `list.copy`, but returns an `L`\",\n",
651-
" sorted=\"New `L` sorted by `key`. If key is str use `attrgetter`; if int use `itemgetter`\",\n",
655+
" sorted=\"New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`\",\n",
652656
" unique=\"Unique items, in stable order\",\n",
653657
" val2idx=\"Dict from value to index\",\n",
654658
" filter=\"Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`\",\n",
@@ -663,6 +667,7 @@
663667
" cycle=\"Same as `itertools.cycle`\",\n",
664668
" enumerate=\"Same as `enumerate`\",\n",
665669
" renumerate=\"Same as `renumerate`\",\n",
670+
" groupby=\"Same as `groupby`\",\n",
666671
" zip=\"Create new `L` with `zip(*items)`\",\n",
667672
" zipwith=\"Create new `L` with `self` zip with each of `*rest`\",\n",
668673
" map_zip=\"Combine `zip` and `starmap`\",\n",
@@ -672,8 +677,7 @@
672677
" reduce=\"Wrapper for `functools.reduce`\",\n",
673678
" sum=\"Sum of the items\",\n",
674679
" product=\"Product of the items\",\n",
675-
" setattrs=\"Call `setattr` on all items\"\n",
676-
" )"
680+
" setattrs=\"Call `setattr` on all items\")"
677681
]
678682
},
679683
{

nbs/nbdev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ website:
66
site-url: "https://fastcore.fast.ai/"
77
description: "Python supercharged for fastai development"
88
repo-branch: master
9-
repo-url: "https://github.com/fastai/fastcore/"
9+
repo-url: "https://github.com/AnswerDotAI/fastcore/"

0 commit comments

Comments
 (0)