From 1856d9c5c6f0c27fda15d8a86fa75bbfcbe106c2 Mon Sep 17 00:00:00 2001 From: jernejfrank Date: Tue, 19 Nov 2024 21:17:11 +0800 Subject: [PATCH] Reimplement within the registry framework --- dag_example_module.png | Bin 27367 -> 27325 bytes docs/reference/decorators/with_columns.rst | 18 +- examples/pandas/with_columns/notebook.ipynb | 633 ++++---- examples/polars/with_columns/notebook.ipynb | 1294 ++++++++--------- hamilton/function_modifiers/__init__.py | 1 + hamilton/function_modifiers/recursive.py | 176 ++- hamilton/plugins/dask_extensions.py | 7 + hamilton/plugins/geopandas_extensions.py | 7 + hamilton/plugins/h_pandas.py | 55 +- hamilton/plugins/h_polars.py | 30 +- hamilton/plugins/ibis_extensions.py | 9 +- hamilton/plugins/pandas_extensions.py | 5 + hamilton/plugins/polars_extensions.py | 5 + .../plugins/polars_lazyframe_extensions.py | 5 + .../plugins/polars_pre_1_0_0_extension.py | 5 + hamilton/plugins/pyspark_pandas_extensions.py | 7 + hamilton/plugins/vaex_extensions.py | 9 + hamilton/registry.py | 17 +- plugin_tests/h_pandas/test_with_columns.py | 23 +- plugin_tests/h_polars/test_with_columns.py | 21 +- tests/function_modifiers/test_recursive.py | 149 +- 21 files changed, 1291 insertions(+), 1185 deletions(-) diff --git a/dag_example_module.png b/dag_example_module.png index 5351fb719b68ca8d7ce09d441a72e4593589718a..6bacf4647d72389dd1078cfc22bc333c839453f9 100644 GIT binary patch delta 25297 zcma&O1yoh*+b_Bh6BQ9eLP9`7K)|5O4M<6MBi$_^IZ`Gml5S8bY3Xk1l5RN9 z+TZ^>-@Rj;d&b>k?6DoLx#pVlecva3@$AI`jM@Q=&-{p!3jV-@?^~M&cq~HtcZDu1 zXizAJ3cVh;Ef;M{s$Pf;Rg3J^-7rh8)USMG!ek;In>7=8Qb$SWPe*TggTAe9Vv4*` z^75`$g*C=h_s(CQXLbu6E{1>W92Yv`Q)TEQ#658YuJ17p^x0n)xK1H{UyKC)^McY( z@H&O3@FSbBzqle=S6@{YXJ*pzogdDnjHl5(a(8!sVcIV`q;s9(?HADk4qZLHkqR3s z=|rB#Z4+2e+J60dGuIqAUTG&Xlnj@Ag-eP$61bzP>{nNGhf+Ln@$jCqvJTi23S1`* zXv2V;2^ZJvWibgSRO59U{KCS^ba;B-fm7NL`M?_t~_|Af4#1*4qn1X ztJZmDX-Txybb#=MG0xW3)<|0-k5lNzM74aWsiIoBWoOE(Pq36;b}XJaly|s@{(R%` z_xDe6+F9Uv_t(Sy>{!RrQc6Y!CxP3sd1@-@sa`Y6dip!YXt`BzR@US7(NaNeZE|fy zTbo`vUqVHN&~CA-skc{d{kSWETjcHA7jf)X%_Ac*iM%ehr-wF|E?x2s3^eFVmqe|N zl?Mlra_gF#2f%ANJ3C86v%KG)Z(m)DzCocS+eUd07dPc{I0Xg8t1@YAZEfl=@Iccy z+xIpm)NjxQlX9o4MaIYHnvWEv_5MPBi4x)ArAkOkD-Y%vW;_~wH)L&}eZRwuh3B8JoiyQ8-9n61D7kYMs!asr9HV5n7;a>`~q5P%^WFmn^ zGBz{v0TIzt{a-k8a&p&i-TJdOQXJ2ZBk8(tmbZHQHMl7#U2<$Ut^>m*piSC67)v z>oGW74_V;2bSZ7X0uT>nFc}#cq4qbYx?MJFqbxM_^}mkp!7*nOCBG(9>ujH!mv;jT zi;|UkM|)|BHI&= zQ(Hf-k+azT5xs|XC5%>@hK;T4Wy>o_HA>;#*2~*A;{?dr*YtEqNNKh|K3oyk)6*NU z#l*y%|2xy57~BPaZ(wSgZM)K+B@U}i{iUR&L?MWnRs3jgNSgs;9?qho!9Z4l&0KSv zTB+&0FLzE|Zi{(XXlS^g_pR3ZjyXzs;q|B6pLl{8Jn6(=-xD|JsdXkHCic(CVSxB( z+BZSIWJo93?ysrN&dyrf*){z5LD5|lOvWq6d$!vnLcX<_gxH=P@3{_T1gV!>+RXgC zVmg$cw{k)!?&s$h9UHsdLXP}dTD&VzgBw}*tUSBlz|gQ+-KsWXwW_gENUhA=&&TK5 z&O)a=b>QLQyAK~e*t%)#zuKGk`S?-reONFKB5=?15%z63GBU{mSMD=uh?<)-CUDu$ z_N0njzI@sGWZxo^Sv!9f#|tOrx7h6O-=n2wF;DfbRy(ZE!n37A6u&SZ{+y>?Zo1b7 zN!*`7jD+|ZJG;^T+DJztZ`|;(N=HY>@4m2%STCFcY%!86Z!9h0cAhvXzgm?ID8ioL zkwG4&MZWe2D5fdUbJ{K3x`s{i@X;gBYdt-W8e3Mmaw!eWbw3z*VfAOp`=m?8^_G~( z@9yo{lSbmCgg@T$n}cJ{s8R7btv%9aao?g%T}`f3Nwecg9-W>^&gx%(1zquH1XQVq zWzr>EaaN_4+_oClEcs?i|00>+Ylk|fK^%Lt=pRd_*N>`N~3(VD5Sa;~>&rt7nu z)E2VUq)vf?$XV-(y_w2s!yVU+#g>dDcYj#^d_g>caBJ$KU@A(=58i>qPn~xOYbIBJ zY-@@Tvh3d!Bkum@nC;*FSC;Vxmr(azy=Yb=>{7F#g+@QTDs0K?vOgm#8f_pYPrggN zfrt@(GhVg4>b_4+W1!tvAW%eyGH-iVNE4lA?rhVd3k;Vu0S{o^{oHg{`%NE z7niw}bDD;_cuKKUkD6<@Ga4?Ad#a_$mv?XkjB|w^1)ZN=d$X&?+1#xjkXRG-1z)f( zS}4JLV|RThslQ)A(}&8F?jL`*SxBw=o0Gf&(Hw$YKT1Wlr#xi)>gE?ZA26yQ-(C*Q zKr8TBH-RHGER2ZTfl(`=Hmh&xwx*B#ipq%MZ0$b{F+5IK7pFVH`3-jk!f~3oY)<#b ztb``&B6jgs-pXxf4>H;(miQv z!_N(;6;69tE&G>4B4fpwr=;JK2DKIP{_{2c>+c#jBlMHv5Hpne&Dlu2(5te4Nd4H4 ze!y&r(ME&I#eb&-yDwja8A1c|+qusXkJG6Nt`AO%5ItPq8zcYq5jE0t5!SuT_x5Yo z>~&uI4+A^(GlBQ>6#Y`=litI}%ZxM>o^-x)aU8RWJXgOsYYtJ4B*|r6mNENvptR|L z*k}?F(d~Xc5xXwS6VcYVReW(vYGVxZd-mqSH>*`czC_mG$~lp+kdPl4Q;Z!-=jCZr zcD5H}hWVo&b#)jwXax zy}2m%TJlGhp@s8NP}-Kd7UQWi$s#uS5EJV%TR%Z}8L5K73%}RbDX?B=ovNw-XpTg-Bb87pV@f}p>z<*M z1(~f`cLjpB9W6B$XP5Elo75y4h{qUCQ@VPlb1Q#k`#D@F+Y0mr996rdo_<-67lTO` zCNKoISs$I8jg>F4>0dtMKO*rGc>j#*<5RfN`7p)99f#gtBD|+Bp6L!UKPb$Zz5M-z z{MKSake0c6b*OZ`s;IE@Rh>ntKDS?@*@O9Udi>&Pq0~s<^VK862}Al}g^paKzt?H7 zH#!UWJ2ISnn6i%swC_j3DJ+w7YD&H>+}ymoO`5Y(EVHyMo1vpOo5V+bJgG^^M&K&(kYYm~87xjbzXQkE zcARnn%jQn-4Ne=z;3vg7N-g-9f`VP7oS*g3C#9!vC_|_CK5+}JCyv*Agg6N+qHXn3 z&6IxXC~)SdU+U$vvY;%gt2(AiQgc-hwSA>iZz!3yXHWdxJ$)`0JMIWC1d#gG)$y&n z>|gnw9?FYCEK~#!y>>WPvfjvtNV#3bwR!5<6jnbid}xr8FY?VWncwGQ+04L5ocIM_ zGlM5i(+`FL&hyDHI-UvKopt=|NbzQOr;1WZn!(V)`;I6S>wx2~kw>T`|Jkpgy&R0p zHx>;UQ%os_wH+DR#P8}}*^HM z(EMchNC&>IWRB+(7xw_4USYZ;*$rK3xLy7{#Pa&E&>6RiaNVO2x*-FXz^-amz16uG zoU(_4RgSD~zH?@4XloPy--QI_jZNA%@QH#`s|at@?!fR-%l?M`@VDeD>{r)i1@5gj zZY{)F^(G67L`q($akbuT-Hr*%luf%9Bho(|fF6ct)*kIlNcmh}fiM`mZlnm%&K zN<3dh@Eo$aaPsOC3kzMDwBbz({}|mQ4uCR;Z`V}Z&mFk#5(vH}mCCy7c7T1ej^y-4!29{UXXIqT;?rZNBe^Rn&^!w&MQV1n7th&Hf%5 zGU|*SP*GK3aZ=$IBfElg@#jV#B7BRIX#s=V!MtU>X33isswy@$r41^aSv#@*eDu9v z-3MtmE<7^a+||$G@h;_-+aniMwigTb(9&AJecNv@3h$NK_y`*%A|et*#w(tBymwTy zd18LNe63eb-kwycFr(ww8^>wq=Hs_N+r|UJXr*N8-Q6^Os6FYD#TAfQ6rFND$ynK0 z>!Q2+>G2tlYjw%O;bcN(*Bb4^-F_Rs^^piK?cfaU%}EjtYm|$N%fDa!A^zui^zrer z>)GDWEkYKL!X)>${<|MD6`K4E*SY(??>vb&8+hDH(fTQ@@sPvPQ+wgGuz}>psBmMN z3@qO8yZzjNSJa-r5r5nmjv|FDIj<=;^Xb{9PmJni0iEh)-*j39P`7RevaqoZ*m%BT z;Cvhs{zo50KRVi5`kG+y@~Kc5jpSRB7@U+Bk9+*6UPJ31&2B{o9X}O4z5nOWI4P;A zVxpqI?Tufx4G$CEym@nFFz-`waj_Cj7Sxu-2>-^dT!74Y@|Cs*nm!bsIB80Lt0V*h z*OS#l@|8lmnGoGCt0=6M=^r0808x7xT*W5w4GED=Tt&{0jG$F}y}lp8pq#GP66BN* zUz4N@7f&n7bh!V&QBHrJx-x^kx0hE>y5s|ZIophQL_~g-l}?GP2?+^KKrUpB^HET| zhcGC1IoRMS9}?2lrA$vxKiz5}f4jjmuF7DKMIj!ZMt$y|s+6ZTUS*&2(q5z9y|&-- zVm?aS7GCz!0JA4fH~p5MV9oIEa-Tiv2fzj)T{dBicw|B$xJevv^Og#mxwp`93Di6j zKb@Lo>*A3L)%Jj`M+f1kv<)2yOj`@$BOv2yH?R>n|$@6oTH< ztE=sR;Fkt+2!euw6mpaT|M__JE;)Gt{UX4D0HWs{wtuek@$spstIL;LY8rMYN(u{m zXTxHi*^)j7E<*K%)nq)tb99OU%H;a@FaLZbWHnWwC{wwPP;86fw$wv(wVj`xDAp%& zI|f;dm2EXYu(DnqeB!*n=J)X~z=2W(QV&!M3|e1N5#YWwU=JS-4l+oOdOpx%OqF~k1>{kcT zPjH9-^s{gvaaL))H2+!n?(SQ71OKEX^1aoeA?G;gKfb_*x1_y}f-YYox!*!p_vcr! zFd4dgsHNrQndN0MfW)%0ve}63e0!7;@P64-d%L@bv!9rOe_I=`ESAxgR|dumn7lrd zTLQklX^pf&KlSzWJ{A_*lo{N=e;?J@)RYxjSy7Q0`Z^K_$xAWHR8^05fHN$S#BI5W zCTuFDvJ|q<69$;Ka*GVwFZ~ha>2^8%tHYOyCvbh@b7S%qvrb(f>l0KC3?_wH}Q9e(TOUh4Dnlg$*10EkRafg^KfD~g-9 zZ#NALL_qo)kQcdvfB(KPWQ5xY@r&Q$Zm~s0ML#Dd5=gln{sKiSmB5AmY5SG_*3#3D z*X~rfov;D9)xPy;uy|v<@?%U4k)fgC6@ROx-^m))4uKQZjs%&BjQ_)){I0Y!%IkmZ zNhcc_dB_kn`-0rZ%j=58Xo-^!k(rqpkX+ho%4y;eCAdgfM+=a=Wspu9Kef67XKfMI zwRNeexcJ}l41i52B!nU8QkC`0hkQXC0bfdL<^MOajpm3&{iI1CW9f}o*i>Ny;jKkP zU1xXNoAw%>PI5@+o_bGD&)2jx9W%3>RT)UfLVoqLILtI~STNzVgiR1VloRzsNDjiJ z`J86H&A_eZ;LFV8=dT2dqjuL*Q&V>|pshTDq}0;BFjoO7_2$&hyAch1YPXY>ft-5l zT1aBqjom0Z|Huv$T_b~4)^5n}Cu}KU1gMW6K3uYYZ z+)4!1_)rwCygFW}fLFtnp8U5+5QWX)3Lr1Y{;=@y#6zI5bnmg6_P^PhZ7QlY*QOi0 zN%59hWcE5Xwqa*1D-c55L$}+)9}5m-EAHg}o}4`Q;tpRoIQd|%-P_pQYzy?3|)I$-zh>|EldH@c?cuL~k~*T;MgCS3|1is@%bEzCj96qk_r z6K7SYGq^UgwEAFrB%jLN?b_VzG1s6&+N!vCI8Z|-<8|(VuZ^Vbbggh|oe!GpLV3^6 zr+RB$4qOKw!~PooNJ5Zvzk&khJG#1miTM13YOlR9*Ueu2=J|`_c!PA1aoebSv%V+S z$u5#fRRETOHA?{f<6jIrqW^&^l&Mzmlz)Tc`^iTU(O#UU$OuhpVd;Ywoqc5U7xY}wqm%e`J69w<$petf(}#BNFQk?#a6SZQ;Ki-e+N_T!eee0M|G z!Q_tnGPejPt_Z9N5x=`@@)cSkvB<>4E)jA!p;W^s?;NLP&~4-3((>wahc1CoIk{*c zYd*E#mz59*$9f&fI5XSy+V$@dB80BngAq@ZgZT8C3$14a;Gbxa*AXAq&gr zrKKf}cF|d!CV%MN%gV~Cu&I>M(n#lop!c1-X-eXWNGuQd< zfq}lx<>EZ{mVY#7AK`AvDTi+)s%eAdFVJ%?@F8(BdWGXD{V**QY~f6G&q6B z8?xtkMPE?w0;-j76c)E*EP1-?4G%%qC}nz^wTU9X&tWa!%8HSBP~*PZX+Wy_{j2s5yyH z)$~#oRW84G8e|o6HLiH{Os-cD*oT#K*S!uLS}op-LQyhleSt^R(ySpmJ!&*3-Mv<3 zq0_|P=UeAU-~l{P%|P-^@pnT9g8JivaYEn0*MQA}hG#2+@MyF<^iZ{`M``b$O z2yhy$&Be<7L9Zw5o*5jX1iItDXQz-(Sxuylo=@-dtMIvCJw14y#A7;D%zT??N*9PN zjObb}oDAa97hLD6E!n1dRUI08ai-e95d}>lUGc`qvX8|HK=jltn*kazk|P`vl8!0l zLK3>VZ{NPrS-Y`Yj=zDF>*?czM@}AGT>Ko`wlw%x&3B(4&MO5cXAH2D#-&BZOtDDR4)+%#$SL>gzLdf~( zH}C3lOBUW~WmH+NMpscxn8FG0Lt7j)vHb3;KPJ@KqyL^^0k`FcYFT_FFX0Y2J`jrNi?METma@{1qwg;d4%WSfV31YIPf(x+jx zDE+S)n8t5Ui-Qz(U0usm!ooro_UNBKd%>4K3f1*b1N5bY&kZFKyE)8ef#FX9sDVax zm#%)5U3?oyiCGhAy zw+ehauhtF4QE4R|bmzq9&Utc#;~gb(zINrQ3s1?kHk6RYtr?pROki46S*_T_({O<_ z7M4U#yf@1?lHo^z_ryQsxF;M($>jaU*jTihtoA5rv&FO}G|4WMnu<3;dja_ABBT7vVP=^qngP8&N0E&TOb4jMNFgBY-5f>W0~y}h65HeLr%Aok|X!+{*- zYY%B?yg}t6d`<1gQG_gm z!ET5-<&!6Y_2uR%!1HEv?d{zrJRf**D=^@%@g8qYFR$ib!!&>$$u zvA1aD17jMJ``A!HAu_gbiPYz(G2Zune6uXA1N%VT)^5<dLD%!0cZ8q8U6{vKgaV zr$)-2@1^+%nBGdugQ7X@$zHWoHhrfN8+)rBdhNYBUmnFYK}h;E)YM)#@MtnauX?1V zQMX4iXNX78n~#+tPsY+L{@LA}N}6eyxA~c3_{P{*dFTMc*9EtI^pzbEB!Rll2X_zq zaGyO3CyaO;X7L~+wp5Y)GOB>x#9ts8HNBB1I&h7^HMBN_~ZvJ^Zo!RRRJ6@7oWW0g5RafRq{y z(w;~V$q#6+Ag=>e&<;wl9m(I8VDjg17J$AhEG=ys8{=-d9t;xakD;L`ZpRJecaAbM z2`OooQXcWgtG6l7D_uU(N+*7WYNe2;7BQHoUToY$nOA$uVO41X=|@YYDj70#e-93f z`ZHxmES0nR>SR-e(PAwQK7JEm;=-~rr6=SZ)^Ey+R{Ar#IB}WTmWm4qdCtv%PLX}` zIv|+Jx+(aU$(-0)L$o-7@4xM%r?LlTbznvBjeeY7Kl$zqZo$RLpv=$N)`er_YJY&+R_zo=H4J0)S?_0H`bt6_5j}g7ne@2n5m#(R0IFsKu@X zDlV>AFg?tztw{ic<^G_OQ8&E9%^muo<;XUTHXkHgK zIIwB3h-)J|+D5$)3v}}7x6s=;Qe?oWTI30F^S7J-!Z_^=^BVS7I0Er}Zp6&Y%n0L? zC(FZyRL>1NraNNdN$p}wHo*`xJThVf=yqVMRoX2!p2~u*3(CyU!0$K04wg;7y7I1{ zE{iZSjP@3-o&>~f6wvQ1{Wvop8?SNpnu=tigcYx#Ei_jxl_>364@|G9sLOX&LM9HT zLPEnYfA@2%UY;l+tBo7fYi~p5iJO)+a`IPdKg_u}*fLO(c(gBe+a6Ez1&*EZXqG%? zKH{WRCDAm^;wp2rRO{ZvczqqZJ}1X^-qQ$)oR^5^sO8&`?;o-0Ca3@Y!q_#r`SKkOXk#JU?KpywwegEzbMd;RNB$r zA?MQv%n!6o-`7^;Krb*LfDnvLj~_q2eD!K}WL$JKi%IXJf!ddFe0g|z{Q?82fvW@p z(dPWrNjQM;eOOqSDK}&$pbK&$_8$-v7g>z4m06560|{AJRHS>dzwUZ89|d->>y8H( z%?-#D*(Brr9&G4Jt|I=xzzoE+u~PGv6N%Wk%3BKes^|^P+?~X?N+e{^W-7 zXqjG_ML=7)&iuIQX@MWZ#f~-ApFcLJl^4z*g*(+{DoyR7LB^aOPJHO>T=6V3?V7yz zorn86w*X*fXN2@C4tCs1x+8*#uHbhNZs(qzz^Z_~<$${TT6BG{;K{reXz9yyx2M5gPOZXi?ff^Cnfwpjv{`Man+N7t6l>C0QUyW2l^GF?fAv@Xj z_mu{B$xobux)z7xUNj}E-FC>;4&0t{xv(lR?rbTpmycL}?o49P*`azdcPSKi_GnS+ z9;$SORez2B=FXndwhN|DNN9XVtk*fHtx!18&H5a#F-LLpy|o`$nx&7~;S6`1PS!m) z=xp0+Kjj>+!1WHS#RS1o7g6mAqK4%7XLWtO%k8S|iSy-u>hq{ssl9;7RI4At^f4W{ zap)kNKbC2yQ7W12nxDZACO=lQK^B+&HG#3^9bgVsDs8F3vb6{WPhVeOCRO-0IG=*^ z@}4LuDZO)cE;4w%-~Jt{8u(YbphB4p<&y%_nhr)gC&c|*x!F(HGqxeaHpGpApee*dyqI%4Tpbb zW1|b4C*SNdLpPl_L?-^6uhWI%;T4wUc-GpLnoEz~GFfe%h+{WCb$CtlMD-P@jiBeC zFJFHgZ$wQ?ZM$R2VIxg8wtb?)&KFvJjp}>;_S7!)qXcVwPBtd;S^&?qgkwUI zR~~8eUOaF=({W#@zk?)o9#EBmqB{=o@}^lF5BK6-kIrOM_gyB$ekqZ(ibiG zB7?3ZuR=J$WG5;r`n|Q)@3DM3i$U8xFg3OSYp+^jeD@{+lRhM5lt!&HYs)_Pxn3?* z(<$Vj zi9;FE_Am2%r$Ob+=yz5Y86phciv8sp)&@LT@2 zu`y?Ek4AFfxDY76E1FGUg}HoCu73A~#oH`OfT(vc>|nKedaU&J9rDwU;61`@b`O~w zbT&!pA*b!5DYpYsa@W5Y zR3bq_$ucZX+xp<}E$uyrd!eH$j?Lol&Q3!^112{&H-szNO9Z?Ty2w-^_GVxjEg9vr zUxlnFe?sE-fx$@jedFO}t>^7$dGoU=s?}1aQBT^~OmLO|35b6Y7Q07`<9d2MIYa92 z^du?0QvL_Q(v@A3r`~FN#P^+VRh_E$n9Q;~f362V0ew9=bHRo8T^2SrLpYlweBu8O zzPomJC>3X=w==#|MtVbEoY;=BpAC5`-*oQ0M38;ZM}dWJWWj(7l7K_4#T! z(6M<8ffjO0%RLKci6u_;w4a@5XGa{rA^snjg3W@uwr)RQo1UAK|0-`l^XL(p1%N&h z3g*v&0sIGX7fjT7xw+Pkjz-W=@i_1LLekK&vXTbL&^B*#xi2H0*Cn!|f@6Gq+}fH> zWpNTreHGR-mmt?D?YS9;fuR)6C7MHc@Zb{&J^A_hPIbna>-$-V{8W2aNZGxiBaV^* zP9`C-WZA_ECU7Ut&dj{`5t&S|QHAqs+?U>YvpqT{W@}L0%7lRj_8seV=9Ci4>k`y@ zkeU1G3*ct*Ni>U@53%g{f}qerE#g9-q_wrR60Lfi0<45!MQR~gPEfJ*ngfz;=i5q* zdp;oe3`&2YrHitn^Zb=A)X}12WaJZ}cusDCH>(N-n}qAFFQ9u}2r0NBx_!5?xsqfj z;H&&kP@^o#;vD)l(LT-m2AnieNHAasgg&UyZcz%Yb^_Le4q6Z5mXl2OzQC!7rJ|p* z7F-6m{`&RnHv^>oe%0IM4{#k=HT6ISdUgE87l-;iaIS0ME4=3=ki183Z~4kVGIkH_ zw(dvs1fwM;AB&6af-oL=y^FyAl#%WEP6>WM`}mKBJIH6jUlE@b_q$+4?d|Q&lu5qi zcDy@-T%3@D)Z7uzDZl#khAghwe^^OLB#tc&N;NZcZZgE?d}SA~P#|-`QTn^Tk5(T` z%!aT98#;*kK`lw!d*?})g=UlIkM4fH83uj>;1Pgo2T!>qbjko2;)FlG!rGz_ZKTk8 z47&qu8Z;KreE@$R1(w1n+j;+kVHtsYw4As;Oh9ELDRhK{gy<**#%ZZ1_1fUzmAE`f zU*!m?MIgHAyu7{TvgD{iUKaAB4W9E?_I`Amq9k>A*9NQ@UeM$_?=B^;%J`25K43@h z4uGY`mKHPs8Y{Cf87s?EI@qBn*Q4;rEdWJ5Q>C`ek&~e zCs_uEkkGfjp5LnOhz2>^iyGvz)E>}d^!%N#8^+=4<|ZZ~(Qi-qivGom@VivPGR20H zmoPE2v}(Cf;Oh=}z#atWh{gE#LpICt%!soHPdyC{cz%J?jA69J@nn_Dld{}t#}MjQ zG>gs$dJ~k2np%<5-;xjZ#iP(40= z`g9Xoxj6{6>}RRMfx6S*-;b1m(;W@vz-nFPu-*m8k(yf|2IiDJ4d_T z9m*!H2x1P&1?~LLe(L}K1>zOu2eZf0qbluJ@1sCaX)BaUmYLq%><0Dt-S!_p(<3l5 zqwVVu)x)Esv+L`f05yv(xJ~=%_ztHrQ&LhONq=1_>>Cd%63pFgi~c&xB`GE%y%+(pj&b7{3$} zx(pS0?x)vH5>DI89soqMLovPs3NxOn;|1v-ePH28!k%SmR6YY6JWO&BfCc;iJez?0 z&u{`DGOn>}NaWwY){r{Bs+AjclSw7MhQKN}iU>s%{GEu3S# zZ$GCk0kYxYl>5mONcimz20eM|?9hRwAM*k-0q?-h^bgGAy;YyeO3r`&`RDD%I1%(5 z%>hK1`ZVJ%2gpWGp`Hk!YsC|%bHW_;B(_m{t=o^q#5(pKG$hxbU;a{)#(Kj>T2xHR zuy31u>8wY^IfyVd!fm3`CgDiiT|8KaTz0e5P%TS7lU8K*_r5DhYinz+R&6ZI72x9F zd{NVa6C@l=)(PboHcYt8;@PujAj{u_VSt%_$iEz1T$fnDzDvw*8LvkUZBqa_e;QeKz+KMV`LzSkGJ657Eue(O8vz9th@tPD!I)1bpP4h$&HvCSL8nKYlQ zi2;sadVc;D_^6BrbBPiY6JbZB_TDF<1$@BF@La-RJVAjT^K5+#DK|Pj+(M@zF;$trZ;)HfO-c82hO(tz)Y|RpkPJ?I(Xa%4_+%Mgg2vGB&|IQ@aluZ z<5HSjD;%nwf%I|0|0~XPbcA@g_8nZis?`opHa0f0?>M2TsHnVe5eXpT;^INc^%+tL zu|~Zz<$M;Uis9}AQY7<@Q`JkojyPcB{%1Zr=F9<$mv>yfB&A1P3pDt$a-WeptSM=Lo6#>3wzp!znqw||1e{I`pH^GiWNl3pC_;=`Sl zMJMn#bQX3rHzQZ|^ng2o8jIRyCC!8$i2ep|{S@Fo*V4z#;4nj1(PY7EsEw)m?*Ajo zOID!w`}Ctu0~wuV{=MtgGKNzs?fDb7nwtYn58}il z3X4kRpL}v7Y(T0u7wn`E`-8e+6xn}|? zbRD`I*%>cnDvp(!?v^U+1}@Lb`6@r_^6GcG6X+N%%(JFSh`?#?-(#0VMd|pem-8HL z@!4BZ1ft6W2ABBH52>JINwJ)$g7GV6ks~N?2u6%F0Wb6FgTKX4^R~wi+6}E-qT9>K zxw)%J+S?+Z-25Ug6I-_I1SktA2wU(SnzzHb{{?&kr}Z=n9UHA-w7}#_4zSCnOAtYS zU3><~y|>zN(_E|+P#p7kJ_v}JzK?8vTy z(fWCf;7zHGsn>$t?Q@JzBMCk>OMY+4BLgg}2l5ow-eEY6mwQu4Xeh{=mjD@} z(Ir4AYXnU7X!Z*x!qAuz$lUI-z_2hpl+PXVbeQWJ8RT`T7Q$0?Bk!(;ausBtT<{+& z$*&r$HtD!Tqc?9}UYws5mY37QAsTuH-G&adJf{FLOUy@bf7KAH!MsnIE%Ze^ZpR9# zGv8sdC0ogD)HsI*+C}7BY3YEPTguYgHC_+iIApG|ZX$lTJkEa~yMookf9wI-yRvnU zLK9WR5n!2~n(eaCE9Z6XdT1+7?T$HYdRavcu3>9*uS&+^+ON|?qPzj@RI;{2b58pS zNQG3VKb_>{j7H zhT@C62KKjxKw#Ila849R{sOWGnE7LXw_Q*(gr!0O`Q-cGFb$EBDPu!HUxo+%I;nV$ zt==<3Sx5sVig^D~gVpln@7)FkPi1=u!}-Wv0LTqIw@s#M%%SJqn|jCf6VZG8OfnIg zTyB&%Y-|9bJRe)9fa_rxV0JPu4y93PjqTyU>)e-hfNG14!La7!*tx0S>3o|tdvp5; zd5!`buVnLcN9OydWQ~pF%BE-0;Yx5;z`X?Y+HLuC33RXg!uYpGj&ebEU77>%pe=0s z`};5lLky)1+&&(`!FQnXa=kb^I0{M?3!{N>2*kj=$^v6?`C7GYfKr~av(s{MajB0$ zb2;gDpb0iM@Kv#1M*xe#_!JCfeMHwvXmJC_!sGsb0Jnc}TQsK~4Xi61U_Nx!g6AwW zXuprXHyZQp?kb$T%T(9a(1=$;SJgp>{oU;|Pp!J7&*3=Izgp+krpk~QH^g!wYI9+F zwBiQ#fCfh?a;G3B&geCbYB&*5jiDBq2g8p=Fm&yn8=IFsQ)Y@6OwJJ5tFq*A+Lc*9 zo;2@Egc}%wVzmZ(A=sO}Ts{qgVt%b5YJ3v^;_>bg2bh3WiVbg{ogB2Zx4$kOA0Cc| zHg56g$8)nmMg&|c-Ow1fK=-D2Fag3)H)ty_$m&hjh6_n}ox>rC7r_MS+WPwPSb2e| zcDLz3Ha@KW+tooydzT!bcYvR+cnL=hOa?$n>A+G-B=T^MaRJ$b-kbXR`fOzW;~l<- zV3b5la5@cFAMQjBK>~~6b3>eiu+O4oGyW|+T3aws26F)P+{*3yo?#Gm#Pt)Q%~s`& zeKh8wqM{kz+f}zU7D~+@fv_DIoH704r%;*|I7){NWhml95S)GY=aZ8v*0BT%GHYMmxXT zaX5GZaR~^}R%`f(cvHUbxMW~36uimuKo%530Wv4WI>#0;!F4#lm2~_z8T>Dbnlq;VkG@1_hp41jvPf!d&BQVAZ)|Az2pxS^sIaRm5A>Wc2FgEW z1HR21-krFA4QhYRY(r=)obP%GAnyTLz-Xyy1pD*nb9v>H^uPI7^Q6@+@Y99@|FS>~ z(CrpECh!IM8IDycDr9M4ma|X(5q`IT`MB_oUQvY0@!?(a+5>F-b<*fsXS|`G0%EUU z%iDVx-Ke+5PLmUZJcCluMrkWeebqUuaM`)aaF8Sy0aA`cDVHUrCL~cjNJ@92r88j#Tib=vCLE=%I7w$ydOgEK%$V|2 z6wH_)4>@j=YyP*HVC=kztHZ2fU?{T=i>=Hvk*`(z!QfxRRfk87yz_^Xn&=oGby)Q) zO>he#Dd_#8LWAuA0s2g@`If`_XcGuL5}@~(K<@<;mjf&1>x&~JBRU2KipG04Wk+H3 zmrwg!_$J?lr=H6Pjxt)&(k>)Zf@sowB9SkZzX|k)&z$Z9Ff5@zxk$b<@Z|#IOM%t* zv4p9#pzMv>X-caBAq}hH$NQ>$* z1;(oQK1Wr5<1TBlMmsIMv+2?mxzI41eANf-{#RTOk%Z$=Ts+?zSHEkHn=+HVSRzre z;l-IUU(ON%``}QKZK0h-UAbh`ZPu8?Su^iY&B`MXS`$t2QwR_$;=MCL@4}oGJ?so z&Nq_4-*m4T?33YdBj^=>cI1-42u%+RCfxH=S1yo&$x@A>c~O$l#9%?+-j{$1?tGwy6qO^+=@PZ2hxjcgI5koZx7W=qzBKYIm+Ujc*M#?NDn z;nIC5J)efWl>Of(nj~ z$vMueDn}Y6uKy#TPe<`gsPmyxE^$*qe|8Vw=s?=5CQ(^+*_?3Vw7hM_YG1{;0OF@q zH~jAEw^dcv^D{-MmmIVOn$;u)i;xBXnY8O#Jgba4sav$;TZ|8*6^g_d3#$|I#h3^a zc_oW+H!S8AHHLC=J^UGCI_$0p8s{=v#*I<{C%b4R%NaS+KT z`B%!#(u9)Y;#lziHF&=A^pVYVS&x2GR$iCA+3}+ti8CiEDW(Vs($IH^V98o9>DW)N znEhoOVCq4Cke0r(16$ z^uf9Eh>MH3U}a$WY{Yo9ZmOk!#+N-@EzfZF3*UG(nY4nw=TjPNrqJqaPr8Q zWp3DGosZpE$)2b6qVE?kNJKli z%Wvf^T&Unfxi)G$s8^G(}Qph?R zgSmM?iaf=q>tp3d>*Z64M>YLX#}`T$ifD$5mqa{5_wZ}XKjLA}O+D%a3F~5(E#R071DRu>_=l%Ql-L6_Iug{R23wrf>civ5hjgA$; zkb6vW{pdT{TesY=J3FKb4GpDJ+pWB?AWr1?L&HdMJ^^H+YNZ3|j@UcA(I_7hSP zUzES_g8-Zg!dv%Pz_=o{XJJ!wvs_h8aaIG)?2Y{(gKs58v(TC!nTmk7Q#dj{^n-;C zx4aKUqdY#nc?%eKw+Fl@pNB<=r65t+o?f z@t+@fBK+6ub9K6wx+O>x>E6Huh*)D|qxHdtro7Kq@9SE}C>OEvz`@ChvGQM{uRE0= zRC02jR*4qBeN}ruPqprVgJD16d!Cv-YTs%?$XQ&xPVWgFy&}_%!l&bvPJ+lLSF2-a z32zR9roAy0bQf1$$0P%?S(aptgCiBnaHcdiv z^p3FJIiGAJANiVF({NwY1nVsxm!e!b%_t`A$tKi}6uY}LxK7x;*8PjDPuwA+kJQg2 zj4{s=zdrCom8>1}v97Yc*+(*^QPD-Vyi6O3&P@bh^rX8Nt*-Jrx9`HFg>{9^s?)2U z3PV(~t|cI(;ppU0CtTdl{T8=(S_HmwSAvdyu#|`4{NQ`NdW`9)S9VACt*)r_f(fBY z|9olQ{f)r-Lfr*@u))z4^2;>6EdT)bMuS4wff+b)1Y=EX-t^`W)Gd@KDt#6*|7y} z?((9gxWaL%iXh)fes64qRvbgMTe?1RK6`b@oRT^p;p z>p^TaG}rP?@6>taQeO4m)#AO?p=AUIN6F9SGHx+)Qj-Ece)w3zcu>^wi}{DRPrnO2&r?1t0A-`kW=Gvzs8j$85ns zo@J*y{!X!DS91_{BmeFCTa(Yx`#EjU0)v))qMFMmx|dQ?*fvp2)A9ur_`Fjo6}6a- zR#TK*+xNbRk-);8-;T4^Kfq>63`)^A5s?dH-3fak%}dT;Qy9AMy1h=qn3spKw6!Ig z?s|F`B`b0##ED;!TPy6dv@(Z-1_f4hrPmcj(_>thr?H%=e5tAB56CYM^k{86X}pOO z`6QG$OfH-#-qd=nHuyW+x-8v2=tN_~$YnH;Fow=2jK=->R49|jt+|QZAG@pVr_YY- za{pIH=N$<3|HpAs=}RFcn}jP{l%$YK_Dn>G6j|Bx@M)QaBr7v<2%YRL^Rgq^d(Z4~ z#`(QIzy2%kxX*Zh-mmBL@qBh@%H`oN{GZ&G$>yh}EzNx8O%gMaTR!DnuGxuc~^*y%&g94ZNN>u@& zg`tVw5~s8*@X*Mc+gc`1EYOxZ2;X+<3qPVIj2NltMbG(Y)qmON=`Y{Oi9BwlRu zs5gYAU_twX6RCChTU+J6kQJ~vIZ-=uoXmNHqfGN^a(_4RhJ4#!kQ5e2 znj@LHmG||Y>-6}SxSYt>n96TT4+C^#`+OWe`_IKttMGe0p?)3S(dO!PiB)*46%lSV z`>8?Z3pAWv|1AIQ>R#p7aivVk%6o?tF2#u6}O?3axv~5S^6u(D9X;R%? z(O+#Nz{+YHq>{3hw)0XG@PuuQRTh96u4Gu*X&FCJ5o9i|!Lj3xF!J#9;HG!ERYkU2%N!ryU}e}X7^^Hh3U0vP?y|}J`iBHp(UW?7 za>bVOCVCP*iVn})#`nIPap@0`ay;Q;%t4mg96^}-A-7LUQ2lNEt%;UE=$3pZZTh!_A6n+&L1ChoRqX0RsFm|-XOVgM&_Fvw`N78*JdSgVBR~HTu8|pLdX6nl%Z-GY z6+{J{Q)LQPj@av3({NgK=6Tb9#Z)sE16MdXugZ%>2By4Ywt_?xMHuZf~;l^}fFT5tT^S z6wh1i7iieeulpP~N0fO_aSYsSf~Wp)VsPi+a7+||_C98|%Ucp(OGisNtidT;v)p`+ zzR*${j&J{fjlhEp<%7uVf+_EXYvZus!WBWqz?Xc0`=?cb+gi z6#;?B_UM++9^zmJb!-ny{*5@RV-UaS1r zcX4rctwS`@=SCRpOo-_ZxTzTVlns>ER0@jT{r`ZtcdvYz2?#q|3lm{zUm7>DgGA=}RMzv+Bj?|eGdy(oqT9EU2btgxpzu6pH%%4Qa=YDmj6 zXRy~9OS?(I{$}n$dsDw3zUncK-K3I^p&D8B#uhs*VCt9Hna)Wq1y;|%lJ|TBOJ=U@ z1aHj1R2NNDDR@>&yBC+hU0K&(NY@t=)1NY?U+d4jK>EGKak2w;w(w=u8@RB@kMH5I z9}i7@V*luvyo4it%`bx@%UW+I>wBsRn}fFL>uUiaRB8G5J%R>qFbPSx7$+cG6qZwq z8T5n#x4oG=S8Y^()i6v>PukD5j7Ub}lf>Og#aS(Tb;jkl44S0lhHfS*4!3i23q`=b zWJ0q$uv2|%eQ5ZOH0zGobS5GK4p#5F#1BQ`2mh_oYe&b75qqz#%|t|aB$8Se!ehhl zGH%|f%c%{^AT;>DzPglHfn2>zt1 zFoMEqGOvjkW2wZ&Kf5j}{|PZ72S3|SkY)!LQ|bB#Vf3tRtV)=nV`grIK01^zfS$0P zvSA1%=>K?fj8WL|ytH(D5Q9KxmcBToW6isEz{6M6)jbC@l7v-R1J9mAbO_I>u*Z%! z{y9m4j1nMJ;_d>1@2m8_K}pMj0%&LqTFPbq!;`vTI~2nN;tzKD zvF5AH4-82+EK^&gT5(%VB^K!oPFEEOi%3%-*|}!-%v_ zoJnNOuDI3;j#>tkF@U`W0SiZ%z3%(-)TzkR6A`rEf}N9X6!Kr^%~UQUnVt6ONA4CEFU z5C36C7>BP!L@)qlgtilO5w^ycU_u-5T0aGV1S1qpRKw8s?Lj7FZ*6!vTC4lsu7>YrmU2#So8tFwL*YlSm1;`BJj4OG02C-c6ajE% zV6;a*aeG{CT{I>U+73}lqtHelv_q86F0x~_6S3zq=EaPJnt@LZ$FDOWP4Kv(tvVhzl9>HMjhni`ZlgI^R<(nt05^Z)^< z&A0q>Qc8i7vvny1W=o8q;9_8cJb1wbK-n)=+MigJ5}*6Xt*@^SOnC>O zs1h%{^0j9GYG9JEVS>OgDBEU$azVBOWzZqbc>GFi0-T7`=gu|2Rj{(Ti2_1d1_eQI z;h&*P(s#q$xDgAV%Mtp=pdj&*R`O89xgy?-dk`tpK&kS5+(8X_br@6HM3 z2TpN$w{rs;2#2V%+`|dt38_nHhhO-oPbn1=cnz#CLdDRzP$3!#nHn^7KUU)#+>QIO zI8ya^Sw$Np|MK#i5XMG1d+_!q%1WFX^K%+|K62kad)7?w`jbwT2efXDRMgZHp#1d{ z;67G0Fkt!qJ+!+_L^q2GK6ekv7RvDesP+Zf1uj^$W)oJBiRESdf&I}OBtJg|h?=Js z7HIZ$X#$)IxV4@$8j6dHe+9c46ctF%wL>;GFd)DLuD|{i^3mz(28hwy{q`_;_%H|9 zAPKu+)saeXa8fM*aCy&il?uc?2zUHPO-&)XbCV2Q?3J!&6Tf_mUs%(Vuv6^_R5-CZ zFFJQ>2m!Yeg4U6M0!~a!+yQO$c~|;rxIbYnVw*A(a4jt@lri!%rKCU9nC*Nb;6h5gDI8%7rwTY7((1^45AfF1l1 zf`iiQM1MSBZAp-Vb6)C?03Jj;{ewB7uw;F@;)A5GC_^}HNGQwIAb_96%v+eCHV5q( z0HP2F>xxtqHQ-R|cZd8BwX(Cbm6Vl_{A_AM;M`HiCw}Br=>+;Fp4Kat;3}|pvB01e z7ICtK-B-l+x9fI}j*bsOL4wow$DnxC zR`H7_#1dfU+S%O&U)vcjc397-(Jma)qqE~`CY>A#uC5Y*@*%~=#VDiN5Xz|m9?X|7 zU(otkC_e~+;vzC(*XZn%iTRRjUvB zto96UZ50ryToSPEhThS!v43FM230k`A{v<=uDD^{oizqdDJW09=`&xJ7oPmWu3`|> zUcGjWk&}}XcsQsmig$og3F!Na{-lIu0}=t}zB-oeMq`IT==mFxm>vM<1R#fml~Ep zSjYOsP+S)doEM2Wll7>G=C`30l%9&ffzvqo{V)oMif{;XLzj(ysdI)z2vj>VIa@?T zMKOZJ{{#Tp(P>6&GBPT4Gyw|qo%+mmR#sL|VBg_MQP9&E?9H)@-X^Q1doya#6Kv$! zK5qJ(~9D=U6jY#}Om3X(WYQSBpz?;rzlS6*H#RheO_m$0sx z`4{Ev!H)V67pDr98E8xmG)k} z(<|Y!q<}X!3V-kY!9UF5eIG%ou{w}J{oW{tE%vsrzPQ~v*nS2(B7zXyXg~d!z880Z zFrK^jZ0!J=Y4HB(U;QOZEiT%QEZ-qiQt;_1?r*vg-pW@E$_-(r0#F)}n=tt{K*XrCS-fQ#o#4`6iZY z@jM`oHqM<K@#i%jwF&A&VYoqYX0ey^Z*NIidl~g~j#083g zxbwKqoBrk&MT>(@-?z5A@v%J|M z=?QM_DXZ?=kFHvQR zv&y+?J~$qsL!15v7WeIj5&PBjnlz(rOxi(%#iLHhwE*?S+3eMYQ`f zZW@!jAS>S<#Fnw5PK0<#f=IIdP z1!l{PIBHqBLwrL=x~&2gpi{oV)#}sC+iMk*I>bASV$s7VthJgOk{FU?XVa#8YUp-blVTr6eTHBtj;)MJy{f%;Xp)i_O4v&t)Y zA+!s@E3nM#m7U+l*S}b4;_O1&GQFj3cHg%Wm=>S3S-aLCHeH1jLS^$WG(N;f$gAs~ zD|B^zYVqv!8E-$oY?m3!lr)^1nzr| z2lEU#MCQ|M!2P(;prm`Y2iv6{;b6yT(d|JmkTIv8NZg$R6MP(DNTsxO`l>*>W*V^i^6lo z)ILc)2E+&l?`CSyt)9gjtvT)5ovk>dpsD#mmF(>8o7G=E)nGc^fpdJ@e`8{Fs}*$W zsXCIV56MS@v#@Kh;>y#SBC;;)4yx47xFw`2i)$Z$Fj&sfJ|QO|K?&!!wYIVNl zbe-yFg{NBlgfyF|+xD77dIfzdTL}#H1Krsdi0bs}4^D5b@$BYK9ms^)@T?ZLtn8r$J+cZ%d_nMllUKx>Enaf{} zCQcFF|651ME=5dVv00~CKE?71Y!R}!P{}~hm>HzM&9W_nDg*w3WEqyJ%Xo(TjwE(X z(y5B{y5uoJ4oWGnE!OK+V!Hz4AoT_d;O?rowx;KFO_`dj6+G|%vzV2Batrtr^I zmn`!zdDQzaa1Crnq`JuGJfns}7@U3iQ2tMXOX>Hg|5!Q^H~0uF(#!rcR_*me;mbvf zT$o+KBKq(5vr?qnUT~b{GrsV6hJ=A1^frtyLOK5W5j!e4AyfRN+TW8iji+p3D3?wO z=?H=CaJNJu7(C;12h$-v(-}IKBAUDkxXzIIismNEBSH4E(yg#guIkKLW}Fq!6?|+9 z6(u#lU|+8IKKL^9jocj5t+@&wi@k$Gqu!0<7eip%dLR1REJ)z{-3BY}FbjdA7DQ;9 zKT|&s&uBR6J9z;ArK%Ug7+#gTVYaw0We)VwMje?{~$9G>_E@Iw8)7Q zJ3YDhZvcOp!j3x78~ZezYgPMtDfS{cdBJ*xSix-}R+|&1&bIx9yreB7jPPC`yfyiA z^TBPa+2&7;;ml%B6@5M%@R*@K=9fEubPhY`5zZS=<~84vjcz}q{vleF1Yjreg$h`B z$Ix9S?uZ2PZ0#mjT>aU)M*>;T+ zsrxe&FEX;6Vzl}o;B}$PlCrti!vJU`n*MX=6K4!hO< z??$ib@JFh05cWTa5ItL%V4StuF#B~A_8N2%%JqPIql07t%dyKQUGCtLnat-Pb=H&1 zX8!v7tq;eAA5Z%8s-Q!=9NWEV@UW7h8Xu#P&;oe`#Y&F$pWVUw5?4R5uKvK$sFjbm z#ocU3F{NN4`dqxpUrNWotgop=@qxpi4igq0UJDZun&N_r&f*7~$p+a{aN~f~D;XK7 zi=)vhultvl=Cr+~h6u+W#0I?CQ!GCRwfMA9QU{DzaT0BnBAhMEr`XtLmOGh{M_#BPF;>b;%%~31TEik4vE8>7AXO^7K0@<8|n)A~?6LB=qIbJ6)v3N-F!=XTz U>X1B#4E`v|tKa>8+vN5C0Q_;cRsaA1 delta 25259 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

=TNEDa5SCXY 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 r=(Ny<4a+9yu1tr{M=O6z5YSX`tkn&MfRxg diff --git a/docs/reference/decorators/with_columns.rst b/docs/reference/decorators/with_columns.rst index 522938221..f4ac6d89a 100644 --- a/docs/reference/decorators/with_columns.rst +++ b/docs/reference/decorators/with_columns.rst @@ -2,27 +2,17 @@ with_columns ======================= -Pandas --------------- +Pandas and Polars +----------------------- -We have a ``with_columns`` option to run operations on columns of a Pandas dataframe and append the results as new columns. +We have a ``with_columns`` option to run operations on columns of a Pandas / Polars dataframe and append the results as new columns. **Reference Documentation** -.. autoclass:: hamilton.plugins.h_pandas.with_columns +.. autoclass:: hamilton.function_modifiers.with_columns :special-members: __init__ -Polars --------------- - -We have a ``with_columns`` decorator to run operations on columns of a Polars dataframe or lazyframe and append the results as new columns. - -**Reference Documentation** - -.. autoclass:: hamilton.plugins.h_polars.with_columns - :special-members: __init__ - PySpark -------------- diff --git a/examples/pandas/with_columns/notebook.ipynb b/examples/pandas/with_columns/notebook.ipynb index 8b9ba9de9..768673517 100644 --- a/examples/pandas/with_columns/notebook.ipynb +++ b/examples/pandas/with_columns/notebook.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -59,228 +59,228 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", "\n", "\n", "case\n", - "\n", - "\n", - "\n", - "case\n", - "thousands\n", + "\n", + "\n", + "\n", + "case\n", + "thousands\n", "\n", "\n", "\n", "final_df.spend_zero_mean_unit_variance\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "Series\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.__append\n", - "\n", - "final_df.__append\n", - "DataFrame\n", + "\n", + "final_df.__append\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "final_df.spend_zero_mean_unit_variance->final_df.__append\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_mean\n", - "\n", - "final_df.spend_mean\n", - "float\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean\n", - "\n", - "final_df.spend_zero_mean\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.spend_mean->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.signups\n", - "\n", - "final_df.signups\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.spend_per_signup\n", - "\n", - "final_df.spend_per_signup\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.signups->final_df.spend_per_signup\n", - "\n", - "\n", + "final_df.spend\n", + "\n", + "final_df.spend\n", + "Series\n", "\n", "\n", "\n", "final_df.avg_3wk_spend\n", - "\n", - "final_df.avg_3wk_spend: case\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.avg_3wk_spend->final_df.__append\n", - "\n", - "\n", - "\n", + "\n", + "final_df.avg_3wk_spend: case\n", + "Series\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.avg_3wk_spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.__append\n", - "\n", + "\n", + "\n", + "final_df.spend_std_dev\n", + "\n", + "final_df.spend_std_dev\n", + "float\n", "\n", - "\n", - "\n", - "final_df\n", - "\n", - "final_df\n", - "DataFrame\n", + "\n", + "\n", + "final_df.spend->final_df.spend_std_dev\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend\n", - "\n", - "final_df.spend\n", - "Series\n", + "\n", + "\n", + "final_df.spend_mean\n", + "\n", + "final_df.spend_mean\n", + "float\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_mean\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.avg_3wk_spend\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean\n", + "\n", + "final_df.spend_zero_mean\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_per_signup\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_std_dev\n", - "\n", - "final_df.spend_std_dev\n", - "float\n", + "final_df.spend_per_signup\n", + "\n", + "final_df.spend_per_signup\n", + "Series\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_std_dev\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.spend_per_signup\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_per_signup->final_df.__append\n", - "\n", + "\n", + "\n", + "final_df\n", + "\n", + "final_df\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "final_df.__append->final_df\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.avg_3wk_spend->final_df.__append\n", + "\n", + "\n", "\n", "\n", "\n", "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.signups\n", + "\n", + "final_df.signups\n", + "Series\n", + "\n", + "\n", + "\n", + "final_df.signups->final_df.spend_per_signup\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "initial_df\n", - "\n", - "initial_df\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "initial_df->final_df.signups\n", - "\n", - "\n", + "\n", + "initial_df\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "initial_df->final_df.spend\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "initial_df->final_df.__append\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "initial_df->final_df.signups\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_mean->final_df.spend_zero_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_per_signup->final_df.__append\n", + "\n", "\n", "\n", "\n", "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", + "\n", + "config\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -290,7 +290,7 @@ "source": [ "%%cell_to_module with_columns_example --builder my_builder --display --execute output_node\n", "import pandas as pd\n", - "from hamilton.plugins.h_pandas import with_columns\n", + "from hamilton.function_modifiers import with_columns\n", "import my_functions\n", "\n", "output_columns = [\n", @@ -355,228 +355,228 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", "\n", "\n", "case\n", - "\n", - "\n", - "\n", - "case\n", - "millions\n", + "\n", + "\n", + "\n", + "case\n", + "millions\n", "\n", "\n", "\n", "final_df.spend_zero_mean_unit_variance\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "Series\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.__append\n", - "\n", - "final_df.__append\n", - "DataFrame\n", + "\n", + "final_df.__append\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "final_df.spend_zero_mean_unit_variance->final_df.__append\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_mean\n", - "\n", - "final_df.spend_mean\n", - "float\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean\n", - "\n", - "final_df.spend_zero_mean\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.spend_mean->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.signups\n", - "\n", - "final_df.signups\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.spend_per_signup\n", - "\n", - "final_df.spend_per_signup\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.signups->final_df.spend_per_signup\n", - "\n", - "\n", + "final_df.spend\n", + "\n", + "final_df.spend\n", + "Series\n", "\n", "\n", "\n", "final_df.avg_3wk_spend\n", - "\n", - "final_df.avg_3wk_spend: case\n", - "Series\n", + "\n", + "final_df.avg_3wk_spend: case\n", + "Series\n", "\n", - "\n", - "\n", - "final_df.avg_3wk_spend->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.avg_3wk_spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.__append\n", - "\n", + "\n", + "\n", + "final_df.spend_std_dev\n", + "\n", + "final_df.spend_std_dev\n", + "float\n", "\n", - "\n", - "\n", - "final_df\n", - "\n", - "final_df\n", - "DataFrame\n", + "\n", + "\n", + "final_df.spend->final_df.spend_std_dev\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend\n", - "\n", - "final_df.spend\n", - "Series\n", + "\n", + "\n", + "final_df.spend_mean\n", + "\n", + "final_df.spend_mean\n", + "float\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_mean\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.avg_3wk_spend\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean\n", + "\n", + "final_df.spend_zero_mean\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_zero_mean\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_per_signup\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "final_df.spend_std_dev\n", - "\n", - "final_df.spend_std_dev\n", - "float\n", + "final_df.spend_per_signup\n", + "\n", + "final_df.spend_per_signup\n", + "Series\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_std_dev\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.spend_per_signup\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_per_signup->final_df.__append\n", - "\n", + "\n", + "\n", + "final_df\n", + "\n", + "final_df\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "final_df.__append->final_df\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.avg_3wk_spend->final_df.__append\n", + "\n", + "\n", "\n", "\n", "\n", "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.signups\n", + "\n", + "final_df.signups\n", + "Series\n", + "\n", + "\n", + "\n", + "final_df.signups->final_df.spend_per_signup\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "initial_df\n", - "\n", - "initial_df\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "initial_df->final_df.signups\n", - "\n", - "\n", + "\n", + "initial_df\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "initial_df->final_df.spend\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "initial_df->final_df.__append\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "initial_df->final_df.signups\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_mean->final_df.spend_zero_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_per_signup->final_df.__append\n", + "\n", "\n", "\n", "\n", "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", + "\n", + "config\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -600,27 +600,16 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/jernejfrank/miniconda3/envs/hamilton/lib/python3.10/site-packages/pyspark/pandas/__init__.py:50: UserWarning: 'PYARROW_IGNORE_TIMEZONE' environment variable was not set. It is required to set this environment variable to '1' in both driver and executor sides if you use pyarrow>=2.0.0. pandas-on-Spark will set it for you but it does not work if there is a Spark context already launched.\n", - " warnings.warn(\n", - "/Users/jernejfrank/miniconda3/envs/hamilton/lib/python3.10/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", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], + "outputs": [], "source": [ "%reload_ext hamilton.plugins.jupyter_magic" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -628,7 +617,7 @@ "\n", "import asyncio\n", "import pandas as pd\n", - "from hamilton.plugins.h_pandas import with_columns\n", + "from hamilton.function_modifiers import with_columns\n", "\n", "async def data_input() -> pd.DataFrame:\n", " await asyncio.sleep(0.0001)\n", @@ -667,7 +656,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 7, "metadata": {}, "outputs": [ { diff --git a/examples/polars/with_columns/notebook.ipynb b/examples/polars/with_columns/notebook.ipynb index 99b19178d..39c1f5614 100644 --- a/examples/polars/with_columns/notebook.ipynb +++ b/examples/polars/with_columns/notebook.ipynb @@ -59,228 +59,227 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", "\n", "\n", "case\n", - "\n", - "\n", - "\n", - "case\n", - "thousands\n", + "\n", + "\n", + "\n", + "case\n", + "thousands\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_zero_mean\n", - "\n", - "final_df.spend_zero_mean\n", - "Series\n", + "final_df.avg_3wk_spend\n", + "\n", + "final_df.avg_3wk_spend: case\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.__append\n", - "\n", - "final_df.__append\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "Series\n", + "\n", + "final_df.__append\n", + "DataFrame\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "final_df.avg_3wk_spend->final_df.__append\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_mean\n", - "\n", - "final_df.spend_mean\n", - "float\n", - "\n", - "\n", - "\n", - "final_df.spend_mean->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df\n", - "\n", - "final_df\n", - "DataFrame\n", + "initial_df\n", + "\n", + "initial_df\n", + "DataFrame\n", "\n", - "\n", - "\n", - "final_df.__append->final_df\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend\n", + "\n", + "final_df.spend\n", + "Series\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev\n", - "\n", - "final_df.spend_std_dev\n", - "float\n", + "\n", + "\n", + "initial_df->final_df.spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend\n", - "\n", - "final_df.spend\n", - "Series\n", + "final_df.signups\n", + "\n", + "final_df.signups\n", + "Series\n", "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.signups\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.spend->final_df.spend_zero_mean\n", - "\n", - "\n", + "final_df.spend->final_df.avg_3wk_spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_mean\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean\n", + "\n", + "final_df.spend_zero_mean\n", + "Series\n", "\n", - "\n", + "\n", "\n", - "final_df.spend->final_df.spend_std_dev\n", - "\n", - "\n", + "final_df.spend->final_df.spend_zero_mean\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.spend_per_signup\n", - "\n", - "final_df.spend_per_signup\n", - "Series\n", + "\n", + "final_df.spend_per_signup\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_per_signup\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.avg_3wk_spend\n", - "\n", - "final_df.avg_3wk_spend: case\n", - "Series\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.avg_3wk_spend\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "initial_df\n", - "\n", - "initial_df\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "initial_df->final_df.__append\n", - "\n", + "final_df.spend_std_dev\n", + "\n", + "final_df.spend_std_dev\n", + "float\n", "\n", - "\n", - "\n", - "initial_df->final_df.spend\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.spend_std_dev\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.signups\n", - "\n", - "final_df.signups\n", - "Series\n", + "final_df.spend_mean\n", + "\n", + "final_df.spend_mean\n", + "float\n", "\n", - "\n", + "\n", "\n", - "initial_df->final_df.signups\n", - "\n", - "\n", + "final_df.spend->final_df.spend_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df\n", + "\n", + "final_df\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "final_df.__append->final_df\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.signups->final_df.spend_per_signup\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_per_signup->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_zero_mean->final_df.__append\n", + "\n", "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "Series\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.avg_3wk_spend->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_per_signup->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_mean->final_df.spend_zero_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance->final_df.__append\n", + "\n", + "\n", "\n", "\n", "\n", "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", + "\n", + "config\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -290,7 +289,7 @@ "source": [ "%%cell_to_module with_columns_example --builder my_builder --display --execute output_node\n", "import polars as pl\n", - "from hamilton.plugins.h_polars import with_columns\n", + "from hamilton.function_modifiers import with_columns\n", "import my_functions\n", "\n", "output_columns = [\n", @@ -354,228 +353,227 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", "\n", "\n", "case\n", - "\n", - "\n", - "\n", - "case\n", - "millions\n", + "\n", + "\n", + "\n", + "case\n", + "millions\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_zero_mean\n", - "\n", - "final_df.spend_zero_mean\n", - "Series\n", + "final_df.avg_3wk_spend\n", + "\n", + "final_df.avg_3wk_spend: case\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.__append\n", - "\n", - "final_df.__append\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "Series\n", + "\n", + "final_df.__append\n", + "DataFrame\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "final_df.avg_3wk_spend->final_df.__append\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_mean\n", - "\n", - "final_df.spend_mean\n", - "float\n", - "\n", - "\n", - "\n", - "final_df.spend_mean->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df\n", - "\n", - "final_df\n", - "DataFrame\n", + "initial_df\n", + "\n", + "initial_df\n", + "DataFrame\n", "\n", - "\n", - "\n", - "final_df.__append->final_df\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend\n", + "\n", + "final_df.spend\n", + "Series\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev\n", - "\n", - "final_df.spend_std_dev\n", - "float\n", + "\n", + "\n", + "initial_df->final_df.spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend\n", - "\n", - "final_df.spend\n", - "Series\n", + "final_df.signups\n", + "\n", + "final_df.signups\n", + "Series\n", "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.signups\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.spend->final_df.spend_zero_mean\n", - "\n", - "\n", + "final_df.spend->final_df.avg_3wk_spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_mean\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean\n", + "\n", + "final_df.spend_zero_mean\n", + "Series\n", "\n", - "\n", + "\n", "\n", - "final_df.spend->final_df.spend_std_dev\n", - "\n", - "\n", + "final_df.spend->final_df.spend_zero_mean\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.spend_per_signup\n", - "\n", - "final_df.spend_per_signup\n", - "Series\n", + "\n", + "final_df.spend_per_signup\n", + "Series\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_per_signup\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.avg_3wk_spend\n", - "\n", - "final_df.avg_3wk_spend: case\n", - "Series\n", - "\n", - "\n", - "\n", - "final_df.spend->final_df.avg_3wk_spend\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", - "initial_df\n", - "\n", - "initial_df\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "initial_df->final_df.__append\n", - "\n", + "final_df.spend_std_dev\n", + "\n", + "final_df.spend_std_dev\n", + "float\n", "\n", - "\n", - "\n", - "initial_df->final_df.spend\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.spend_std_dev\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.signups\n", - "\n", - "final_df.signups\n", - "Series\n", + "final_df.spend_mean\n", + "\n", + "final_df.spend_mean\n", + "float\n", "\n", - "\n", + "\n", "\n", - "initial_df->final_df.signups\n", - "\n", - "\n", + "final_df.spend->final_df.spend_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df\n", + "\n", + "final_df\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "final_df.__append->final_df\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.signups->final_df.spend_per_signup\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_per_signup->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_zero_mean->final_df.__append\n", + "\n", "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "Series\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.avg_3wk_spend->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_per_signup->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_mean->final_df.spend_zero_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance->final_df.__append\n", + "\n", + "\n", "\n", "\n", "\n", "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", + "\n", + "config\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -616,7 +614,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -628,228 +626,227 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", "\n", "\n", "case\n", - "\n", - "\n", - "\n", - "case\n", - "thousands\n", + "\n", + "\n", + "\n", + "case\n", + "thousands\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_zero_mean\n", - "\n", - "final_df.spend_zero_mean\n", - "Expr\n", + "final_df.avg_3wk_spend\n", + "\n", + "final_df.avg_3wk_spend: case\n", + "Expr\n", "\n", "\n", - "\n", + "\n", "final_df.__append\n", - "\n", - "final_df.__append\n", - "LazyFrame\n", + "\n", + "final_df.__append\n", + "LazyFrame\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "Expr\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "final_df.avg_3wk_spend->final_df.__append\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_mean\n", - "\n", - "final_df.spend_mean\n", - "float\n", - "\n", - "\n", - "\n", - "final_df.spend_mean->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df\n", - "\n", - "final_df\n", - "LazyFrame\n", + "initial_df\n", + "\n", + "initial_df\n", + "LazyFrame\n", "\n", - "\n", - "\n", - "final_df.__append->final_df\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend\n", + "\n", + "final_df.spend\n", + "Expr\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev\n", - "\n", - "final_df.spend_std_dev\n", - "float\n", + "\n", + "\n", + "initial_df->final_df.spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend\n", - "\n", - "final_df.spend\n", - "Expr\n", + "final_df.signups\n", + "\n", + "final_df.signups\n", + "Expr\n", "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.signups\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.spend->final_df.spend_zero_mean\n", - "\n", - "\n", + "final_df.spend->final_df.avg_3wk_spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_mean\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean\n", + "\n", + "final_df.spend_zero_mean\n", + "Expr\n", "\n", - "\n", + "\n", "\n", - "final_df.spend->final_df.spend_std_dev\n", - "\n", - "\n", + "final_df.spend->final_df.spend_zero_mean\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.spend_per_signup\n", - "\n", - "final_df.spend_per_signup\n", - "Expr\n", + "\n", + "final_df.spend_per_signup\n", + "Expr\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_per_signup\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.avg_3wk_spend\n", - "\n", - "final_df.avg_3wk_spend: case\n", - "Expr\n", - "\n", - "\n", - "\n", - "final_df.spend->final_df.avg_3wk_spend\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance->final_df.__append\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "initial_df\n", - "\n", - "initial_df\n", - "LazyFrame\n", - "\n", - "\n", - "\n", - "initial_df->final_df.__append\n", - "\n", + "final_df.spend_std_dev\n", + "\n", + "final_df.spend_std_dev\n", + "float\n", "\n", - "\n", - "\n", - "initial_df->final_df.spend\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.spend_std_dev\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.signups\n", - "\n", - "final_df.signups\n", - "Expr\n", + "final_df.spend_mean\n", + "\n", + "final_df.spend_mean\n", + "float\n", "\n", - "\n", + "\n", "\n", - "initial_df->final_df.signups\n", - "\n", - "\n", + "final_df.spend->final_df.spend_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df\n", + "\n", + "final_df\n", + "LazyFrame\n", + "\n", + "\n", + "\n", + "final_df.__append->final_df\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.signups->final_df.spend_per_signup\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_per_signup->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_zero_mean->final_df.__append\n", + "\n", "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "Expr\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.avg_3wk_spend->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_per_signup->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_mean->final_df.spend_zero_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance->final_df.__append\n", + "\n", + "\n", "\n", "\n", "\n", "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", + "\n", + "config\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -859,7 +856,7 @@ "source": [ "%%cell_to_module with_columns_lazy_example --builder my_builder_lazy --display --execute output_node\n", "import polars as pl\n", - "from hamilton.plugins.h_polars import with_columns\n", + "from hamilton.function_modifiers import with_columns\n", "import my_functions_lazy\n", "\n", "output_columns = [\n", @@ -891,7 +888,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -923,231 +920,230 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", "\n", "\n", "case\n", - "\n", - "\n", - "\n", - "case\n", - "millions\n", + "\n", + "\n", + "\n", + "case\n", + "millions\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_zero_mean\n", - "\n", - "final_df.spend_zero_mean\n", - "Expr\n", + "final_df.avg_3wk_spend\n", + "\n", + "final_df.avg_3wk_spend: case\n", + "Expr\n", "\n", "\n", - "\n", + "\n", "final_df.__append\n", - "\n", - "final_df.__append\n", - "LazyFrame\n", + "\n", + "final_df.__append\n", + "LazyFrame\n", "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.__append\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "\n", - "final_df.spend_zero_mean_unit_variance\n", - "Expr\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "final_df.avg_3wk_spend->final_df.__append\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_mean\n", - "\n", - "final_df.spend_mean\n", - "float\n", - "\n", - "\n", - "\n", - "final_df.spend_mean->final_df.spend_zero_mean\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df\n", - "\n", - "final_df\n", - "LazyFrame\n", + "initial_df\n", + "\n", + "initial_df\n", + "LazyFrame\n", "\n", - "\n", - "\n", - "final_df.__append->final_df\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend\n", + "\n", + "final_df.spend\n", + "Expr\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev\n", - "\n", - "final_df.spend_std_dev\n", - "float\n", + "\n", + "\n", + "initial_df->final_df.spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", - "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend\n", - "\n", - "final_df.spend\n", - "Expr\n", + "final_df.signups\n", + "\n", + "final_df.signups\n", + "Expr\n", "\n", - "\n", + "\n", + "\n", + "initial_df->final_df.signups\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.spend->final_df.spend_zero_mean\n", - "\n", - "\n", + "final_df.spend->final_df.avg_3wk_spend\n", + "\n", + "\n", "\n", - "\n", - "\n", - "final_df.spend->final_df.spend_mean\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean\n", + "\n", + "final_df.spend_zero_mean\n", + "Expr\n", "\n", - "\n", + "\n", "\n", - "final_df.spend->final_df.spend_std_dev\n", - "\n", - "\n", + "final_df.spend->final_df.spend_zero_mean\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.spend_per_signup\n", - "\n", - "final_df.spend_per_signup\n", - "Expr\n", + "\n", + "final_df.spend_per_signup\n", + "Expr\n", "\n", "\n", - "\n", + "\n", "final_df.spend->final_df.spend_per_signup\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.avg_3wk_spend\n", - "\n", - "final_df.avg_3wk_spend: case\n", - "Expr\n", - "\n", - "\n", - "\n", - "final_df.spend->final_df.avg_3wk_spend\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "final_df.spend_zero_mean_unit_variance->final_df.__append\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "initial_df\n", - "\n", - "initial_df\n", - "LazyFrame\n", - "\n", - "\n", - "\n", - "initial_df->final_df.__append\n", - "\n", + "final_df.spend_std_dev\n", + "\n", + "final_df.spend_std_dev\n", + "float\n", "\n", - "\n", - "\n", - "initial_df->final_df.spend\n", - "\n", - "\n", + "\n", + "\n", + "final_df.spend->final_df.spend_std_dev\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.signups\n", - "\n", - "final_df.signups\n", - "Expr\n", + "final_df.spend_mean\n", + "\n", + "final_df.spend_mean\n", + "float\n", "\n", - "\n", + "\n", "\n", - "initial_df->final_df.signups\n", - "\n", - "\n", + "final_df.spend->final_df.spend_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df\n", + "\n", + "final_df\n", + "LazyFrame\n", + "\n", + "\n", + "\n", + "final_df.__append->final_df\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "final_df.signups->final_df.spend_per_signup\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "final_df.spend_per_signup->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_zero_mean->final_df.__append\n", + "\n", "\n", - "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "\n", + "final_df.spend_zero_mean_unit_variance\n", + "Expr\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", "\n", - "final_df.avg_3wk_spend->final_df.__append\n", - "\n", - "\n", - "\n", + "final_df.spend_per_signup->final_df.__append\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_std_dev->final_df.spend_zero_mean_unit_variance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_mean->final_df.spend_zero_mean\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "final_df.spend_zero_mean_unit_variance->final_df.__append\n", + "\n", + "\n", "\n", "\n", "\n", "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", + "\n", + "config\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 8, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } diff --git a/hamilton/function_modifiers/__init__.py b/hamilton/function_modifiers/__init__.py index 958d07540..cd0161991 100644 --- a/hamilton/function_modifiers/__init__.py +++ b/hamilton/function_modifiers/__init__.py @@ -88,6 +88,7 @@ subdag = recursive.subdag parameterized_subdag = recursive.parameterized_subdag +with_columns = recursive.with_columns # resolve/meta stuff -- power user features diff --git a/hamilton/function_modifiers/recursive.py b/hamilton/function_modifiers/recursive.py index 35576b891..bf1d6ef35 100644 --- a/hamilton/function_modifiers/recursive.py +++ b/hamilton/function_modifiers/recursive.py @@ -14,10 +14,6 @@ else: from typing import NotRequired -from pandas import DataFrame as PandasDataFrame -from polars import DataFrame as PolarsDataFrame -from polars import LazyFrame as PolarsLazyFrame - # Copied this over from function_graph # TODO -- determine the best place to put this code from hamilton import graph_utils, node, registry @@ -635,24 +631,96 @@ def prune_nodes(nodes: List[node.Node], select: Optional[List[str]] = None) -> L return output -SUPPORTED_DATAFAME_TYPES = [PandasDataFrame, PolarsDataFrame, PolarsLazyFrame] - - -class with_columns_factory(base.NodeInjector, abc.ABC): - """Performs with_columns operation on a dataframe. This is a special case of NodeInjector - that applies only to dataframes. For now can be used with: +class with_columns(base.NodeInjector, abc.ABC): + """Performs with_columns operation on a dataframe. This is used when you want to extract some + columns out of the dataframe, perform operations on them and then append to the original dataframe. + For now can be used with: - Pandas - Polars - This is used when you want to extract some columns out of the dataframe, perform operations - on them and then append to the original dataframe. - def processed_data(data: pd.DataFrame) -> pd.DataFrame: + + Here's an example of calling it on a pandas dataframe -- if you've seen ``@subdag``, you should be familiar with + the concepts: + + .. code-block:: python + + # my_module.py + def a(a_from_df: pd.Series) -> pd.Series: + return _process(a) + + def b(b_from_df: pd.Series) -> pd.Series: + return _process(b) + + def a_b_average(a_from_df: pd.Series, b_from_df: pd.Series) -> pd.Series: + return (a_from_df + b_from_df) / 2 + + + .. code-block:: python + + # with_columns_module.py + def a_plus_b(a: pd.Series, b: pd.Series) -> pd.Series: + return a + b + + + # the with_columns call + @with_columns( + *[my_module], # Load from any module + *[a_plus_b], # or list operations directly + columns_to_pass=["a_from_df", "b_from_df"], # The columns to pass from the dataframe to + # the subdag + select=["a", "b", "a_plus_b", "a_b_average"], # The columns to select from the dataframe + ) + def final_df(initial_df: pd.DataFrame) -> pd.DataFrame: + # process, or just return unprocessed ... - In this case we would build a subdag out of the node ``data`` and append selected nodes back to - the original dataframe before feeding it into ``processed_data``. + In this instance the ``initial_df`` would get two columns added: ``a_plus_b`` and ``a_b_average``. + + The operations are applied in topological order. This allows you to + express the operations individually, making it easy to unit-test and reuse. + + Note that the operation is "append", meaning that the columns that are selected are appended + onto the dataframe. + + If the function takes multiple dataframes, the dataframe input to process will always be + the first argument. This will be passed to the subdag, transformed, and passed back to the function. + This follows the hamilton rule of reference by parameter name. To demonstarte this, in the code + above, the dataframe that is passed to the subdag is `initial_df`. That is transformed + by the subdag, and then returned as the final dataframe. + + You can read it as: + + "final_df is a function that transforms the upstream dataframe initial_df, running the transformations + from my_module. It starts with the columns a_from_df and b_from_df, and then adds the columns + a, b, and a_plus_b to the dataframe. It then returns the dataframe, and does some processing on it." + + In case you need more flexibility you can alternatively use ``pass_dataframe_as``, for example, + + .. code-block:: python + + # with_columns_module.py + def a_from_df(initial_df: pd.Series) -> pd.Series: + return initial_df["a_from_df"] / 100 + + def b_from_df(initial_df: pd.Series) -> pd.Series: + return initial_df["b_from_df"] / 100 + + + # the with_columns call + @with_columns( + *[my_module], + *[a_from_df], + columns_to_pass=["a_from_df", "b_from_df"], + select=["a_from_df", "b_from_df", "a", "b", "a_plus_b", "a_b_average"], + ) + def final_df(initial_df: pd.DataFrame) -> pd.DataFrame: + # process, or just return unprocessed + ... + + the above would output a dataframe where the two columns ``a_from_df`` and ``b_from_df`` get + overwritten. """ # TODO: if we rename the column nodes into something smarter this can be avoided and @@ -674,14 +742,6 @@ def _check_for_duplicates(nodes_: List[node.Node]) -> bool: return True return False - def validate_dataframe_type(self): - if not set(self.allowed_dataframe_types).issubset(list(SUPPORTED_DATAFAME_TYPES)): - raise InvalidDecoratorException( - f"The provided dataframe types: {self.allowed_dataframe_types} are currently not supported " - "to be used in `with_columns`. Please reach out if you need it. " - f"We currently only support: {SUPPORTED_DATAFAME_TYPES}." - ) - def __init__( self, *load_from: Union[Callable, ModuleType], @@ -690,7 +750,6 @@ def __init__( select: List[str] = None, namespace: str = None, config_required: List[str] = None, - dataframe_types: Collection[Type] = None, ): """Instantiates a ``@with_column`` decorator. @@ -711,14 +770,6 @@ def __init__( if you want the functions/modules to have access to all possible config. """ - if dataframe_types is None: - raise ValueError("You need to specify which dataframe types it will be applied to.") - else: - if isinstance(dataframe_types, Type): - dataframe_types = [dataframe_types] - self.allowed_dataframe_types = dataframe_types - self.validate_dataframe_type() - self.subdag_functions = subdag.collect_functions(load_from) self.select = select @@ -796,28 +847,49 @@ def _get_inital_nodes( f"It might not be compatible with some other decorators." ) - if input_types[inject_parameter] not in self.allowed_dataframe_types: - raise ValueError(f"Dataframe has to be a {self.allowed_dataframe_types} DataFrame.") - else: - self.dataframe_type = input_types[inject_parameter] - + dataframe_type = input_types[inject_parameter] initial_nodes = ( [] if self.dataframe_subdag_param is not None else self._create_column_nodes(inject_parameter=inject_parameter, params=params) ) - return inject_parameter, initial_nodes + return inject_parameter, initial_nodes, dataframe_type + + def create_merge_node( + self, upstream_node: str, node_name: str, dataframe_type: Type + ) -> node.Node: + "Node that adds to / overrides columns for the original dataframe based on selected output." + if self.is_async: - @abc.abstractmethod - def create_merge_node(self, upstream_node: str, node_name: str) -> node.Node: - """Should create a node that merges the results back into the original dataframe. + async def new_callable(**kwargs) -> Any: + df = kwargs[upstream_node] + columns_to_append = {} + for column in self.select: + columns_to_append[column] = kwargs[column] + new_df = registry.with_columns(df, columns_to_append) + return new_df + else: - Node that adds to / overrides columns for the original dataframe based on selected output. + def new_callable(**kwargs) -> Any: + df = kwargs[upstream_node] + columns_to_append = {} + for column in self.select: + columns_to_append[column] = kwargs[column] - This will be platform specific, see Pandas and Polars plugins for implementation. - """ - pass + new_df = registry.with_columns(df, columns_to_append) + return new_df + + column_type = registry.get_column_type_from_df_type(dataframe_type) + input_map = {column: column_type for column in self.select} + input_map[upstream_node] = dataframe_type + + return node.Node( + name=node_name, + typ=dataframe_type, + callabl=new_callable, + input_types=input_map, + ) def inject_nodes( self, params: Dict[str, Type[Type]], config: Dict[str, Any], fn: Callable @@ -825,7 +897,9 @@ def inject_nodes( self.is_async = inspect.iscoroutinefunction(fn) namespace = fn.__name__ if self.namespace is None else self.namespace - inject_parameter, initial_nodes = self._get_inital_nodes(fn=fn, params=params) + inject_parameter, initial_nodes, dataframe_type = self._get_inital_nodes( + fn=fn, params=params + ) subdag_nodes = subdag.collect_nodes(config, self.subdag_functions) @@ -833,7 +907,7 @@ def inject_nodes( # pass the dataframe and extract them himself. If we add namespace to initial nodes and rewire the # initial node names with the ongoing ones that have a column argument, we can also allow in place # changes when using columns_to_pass - if with_columns_factory._check_for_duplicates(initial_nodes + subdag_nodes): + if with_columns._check_for_duplicates(initial_nodes + subdag_nodes): raise ValueError( "You can only specify columns once. You used `columns_to_pass` and we " "extract the columns for you. In this case they cannot be overwritten -- only new columns get " @@ -853,14 +927,16 @@ def inject_nodes( self.select = [ sink_node.name for sink_node in pruned_nodes - if sink_node.type == registry.get_column_type_from_df_type(self.dataframe_type) + if sink_node.type == registry.get_column_type_from_df_type(dataframe_type) ] - merge_node = self.create_merge_node(inject_parameter, node_name="__append") + merge_node = self.create_merge_node( + inject_parameter, node_name="__append", dataframe_type=dataframe_type + ) output_nodes = initial_nodes + pruned_nodes + [merge_node] output_nodes = subdag.add_namespace(output_nodes, namespace) return output_nodes, {inject_parameter: assign_namespace(merge_node.name, namespace)} def validate(self, fn: Callable): - self.validate_dataframe_type() + pass diff --git a/hamilton/plugins/dask_extensions.py b/hamilton/plugins/dask_extensions.py index 6bf9e664c..03661bd93 100644 --- a/hamilton/plugins/dask_extensions.py +++ b/hamilton/plugins/dask_extensions.py @@ -22,6 +22,13 @@ def fill_with_scalar_dask(df: dd.DataFrame, column_name: str, value: Any) -> dd. return df +@registry.with_columns.register(dd.DataFrame) +def with_columns_dask(df: dd.DataFrame, columns: dd.Series) -> dd.DataFrame: + raise NotImplementedError( + "As of Hamilton version 1.83.1, with_columns for Dask isn't supported." + ) + + def register_types(): """Function to register the types for this extension.""" registry.register_types("dask", DATAFRAME_TYPE, COLUMN_TYPE) diff --git a/hamilton/plugins/geopandas_extensions.py b/hamilton/plugins/geopandas_extensions.py index 70e7e0135..6bbcc6e4f 100644 --- a/hamilton/plugins/geopandas_extensions.py +++ b/hamilton/plugins/geopandas_extensions.py @@ -24,6 +24,13 @@ def fill_with_scalar_geopandas( return df +@registry.with_columns.register(gpd.GeoDataFrame) +def with_columns_geopandas(df: gpd.GeoDataFrame, columns: gpd.GeoSeries) -> gpd.GeoDataFrame: + raise NotImplementedError( + "As of Hamilton version 1.83.1, with_columns for geopandas isn't supported." + ) + + def register_types(): """Function to register the types for this extension.""" registry.register_types("geopandas", DATAFRAME_TYPE, COLUMN_TYPE) diff --git a/hamilton/plugins/h_pandas.py b/hamilton/plugins/h_pandas.py index df6a147b6..fef658685 100644 --- a/hamilton/plugins/h_pandas.py +++ b/hamilton/plugins/h_pandas.py @@ -1,6 +1,8 @@ import sys from types import ModuleType -from typing import Any, Callable, List, Union +from typing import Callable, List, Union + +from hamilton.dev_utils.deprecation import deprecated _sys_version_info = sys.version_info _version_tuple = (_sys_version_info.major, _sys_version_info.minor, _sys_version_info.micro) @@ -10,16 +12,18 @@ else: pass -import pandas as pd - -# Copied this over from function_graph -# TODO -- determine the best place to put this code -from hamilton import node, registry -from hamilton.function_modifiers.recursive import ( - with_columns_factory, -) +from hamilton.function_modifiers.recursive import with_columns as with_columns_factory +@deprecated( + warn_starting=(1, 82, 0), + fail_starting=(2, 0, 0), + use_this=with_columns_factory, + explanation="with_columns has been centralised and can be imported from function modifiers the same " + "extract_columns.", + current_version=(1, 83, 1), + migration_guide="https://hamilton.dagworks.io/en/latest/reference/decorators/", +) class with_columns(with_columns_factory): """Initializes a with_columns decorator for pandas. This allows you to efficiently run groups of map operations on a dataframe. @@ -140,37 +144,4 @@ def __init__( select=select, namespace=namespace, config_required=config_required, - dataframe_types=pd.DataFrame, - ) - - def create_merge_node(self, upstream_node: str, node_name: str) -> node.Node: - "Node that adds to / overrides columns for the original dataframe based on selected output." - if self.is_async: - - async def new_callable(**kwargs) -> Any: - df = kwargs[upstream_node] - columns_to_append = {} - for column in self.select: - columns_to_append[column] = kwargs[column] - - return df.assign(**columns_to_append) - else: - - def new_callable(**kwargs) -> Any: - df = kwargs[upstream_node] - columns_to_append = {} - for column in self.select: - columns_to_append[column] = kwargs[column] - - return df.assign(**columns_to_append) - - column_type = registry.get_column_type_from_df_type(self.dataframe_type) - input_map = {column: column_type for column in self.select} - input_map[upstream_node] = self.dataframe_type - - return node.Node( - name=node_name, - typ=self.dataframe_type, - callabl=new_callable, - input_types=input_map, ) diff --git a/hamilton/plugins/h_polars.py b/hamilton/plugins/h_polars.py index ec0ad2686..5cec86328 100644 --- a/hamilton/plugins/h_polars.py +++ b/hamilton/plugins/h_polars.py @@ -14,10 +14,8 @@ # Copied this over from function_graph # TODO -- determine the best place to put this code -from hamilton import base, node, registry -from hamilton.function_modifiers.recursive import ( - with_columns_factory, -) +from hamilton import base +from hamilton.function_modifiers.recursive import with_columns as with_columns_factory class PolarsDataFrameResult(base.ResultMixin): @@ -71,6 +69,7 @@ def output_type(self) -> Type: return pl.DataFrame +# Do we need this here? class with_columns(with_columns_factory): """Initializes a with_columns decorator for polars. @@ -216,27 +215,4 @@ def __init__( select=select, namespace=namespace, config_required=config_required, - dataframe_types=[pl.DataFrame, pl.LazyFrame], - ) - - def create_merge_node(self, upstream_node: str, node_name: str) -> node.Node: - "Node that adds to / overrides columns for the original dataframe based on selected output." - - def new_callable(**kwargs) -> Any: - df = kwargs[upstream_node] - columns_to_append = {} - for column in self.select: - columns_to_append[column] = kwargs[column] - - return df.with_columns(**columns_to_append) - - column_type = registry.get_column_type_from_df_type(self.dataframe_type) - input_map = {column: column_type for column in self.select} - input_map[upstream_node] = self.dataframe_type - - return node.Node( - name=node_name, - typ=self.dataframe_type, - callabl=new_callable, - input_types=input_map, ) diff --git a/hamilton/plugins/ibis_extensions.py b/hamilton/plugins/ibis_extensions.py index 861312600..7f828234e 100644 --- a/hamilton/plugins/ibis_extensions.py +++ b/hamilton/plugins/ibis_extensions.py @@ -1,4 +1,4 @@ -from typing import Any, Type +from typing import Any, List, Type from hamilton import registry @@ -31,6 +31,13 @@ def fill_with_scalar_ibis(df: ir.Table, column_name: str, scalar_value: Any) -> ) +@registry.with_column.register(ir.Table) +def with_columns_ibis(df: ir.Table, columns: List[ir.Columns]) -> ir.Table: + raise NotImplementedError( + "As of Hamilton version 1.83.1, with_columns for Ibis isn't supported." + ) + + register_types() diff --git a/hamilton/plugins/pandas_extensions.py b/hamilton/plugins/pandas_extensions.py index e212e5df8..5c3e42baf 100644 --- a/hamilton/plugins/pandas_extensions.py +++ b/hamilton/plugins/pandas_extensions.py @@ -55,6 +55,11 @@ def fill_with_scalar_pandas(df: pd.DataFrame, column_name: str, value: Any) -> p return df +@registry.with_columns.register(pd.DataFrame) +def with_columns_pandas(df: pd.DataFrame, columns: List[pd.Series]) -> pd.DataFrame: + return df.assign(**columns) + + def register_types(): """Function to register the types for this extension.""" registry.register_types("pandas", DATAFRAME_TYPE, COLUMN_TYPE) diff --git a/hamilton/plugins/polars_extensions.py b/hamilton/plugins/polars_extensions.py index 7fe500c7d..b5ceccda8 100644 --- a/hamilton/plugins/polars_extensions.py +++ b/hamilton/plugins/polars_extensions.py @@ -51,4 +51,9 @@ def fill_with_scalar_polars(df: pl.DataFrame, column_name: str, scalar_value: An return df.with_column(pl.Series(name=column_name, values=scalar_value)) +@registry.with_columns.register(pl.DataFrame) +def with_columns_polars(df: pl.DataFrame, columns: pl.Series) -> pl.DataFrame: + return df.with_columns(**columns) + + register_types() diff --git a/hamilton/plugins/polars_lazyframe_extensions.py b/hamilton/plugins/polars_lazyframe_extensions.py index fc3e37be8..3f82721e2 100644 --- a/hamilton/plugins/polars_lazyframe_extensions.py +++ b/hamilton/plugins/polars_lazyframe_extensions.py @@ -69,6 +69,11 @@ def fill_with_scalar_polars_lazyframe( return df.with_column(scalar_value.alias(column_name)) +@registry.with_columns.register(pl.LazyFrame) +def with_columns_polars_lazyframe(df: pl.LazyFrame, columns: pl.Expr) -> pl.LazyFrame: + return df.with_columns(**columns) + + register_types() diff --git a/hamilton/plugins/polars_pre_1_0_0_extension.py b/hamilton/plugins/polars_pre_1_0_0_extension.py index 39b75c262..8cd62899d 100644 --- a/hamilton/plugins/polars_pre_1_0_0_extension.py +++ b/hamilton/plugins/polars_pre_1_0_0_extension.py @@ -67,6 +67,11 @@ def fill_with_scalar_polars(df: pl.DataFrame, column_name: str, scalar_value: An return df.with_column(pl.Series(name=column_name, values=scalar_value)) +@registry.with_columns.register(pl.DataFrame) +def with_columns_polars(df: pl.DataFrame, columns: pl.Series) -> pl.DataFrame: + return df.with_columns(**columns) + + @dataclasses.dataclass class PolarsCSVReader(DataLoader): """Class specifically to handle loading CSV files with Polars. diff --git a/hamilton/plugins/pyspark_pandas_extensions.py b/hamilton/plugins/pyspark_pandas_extensions.py index bb15bef1b..63380ddf3 100644 --- a/hamilton/plugins/pyspark_pandas_extensions.py +++ b/hamilton/plugins/pyspark_pandas_extensions.py @@ -22,6 +22,13 @@ def fill_with_scalar_pyspark_pandas(df: ps.DataFrame, column_name: str, value: A return df +@registry.with_columns.register(ps.DataFrame) +def with_columns_pyspark_pandas(df: ps.DataFrame, columns: ps.Series) -> ps.DataFrame: + raise NotImplementedError( + "Please use the separate implementation by importing with_columns from h_spark." + ) + + def register_types(): """Function to register the types for this extension.""" registry.register_types("pyspark_pandas", DATAFRAME_TYPE, COLUMN_TYPE) diff --git a/hamilton/plugins/vaex_extensions.py b/hamilton/plugins/vaex_extensions.py index 208ff022f..497012e93 100644 --- a/hamilton/plugins/vaex_extensions.py +++ b/hamilton/plugins/vaex_extensions.py @@ -26,6 +26,15 @@ def fill_with_scalar_vaex( return df +@registry.with_column.register(vaex.dataframe.DataFrame) +def with_columns_ibis( + df: vaex.dataframe.DataFrame, columns: vaex.expression.Expression +) -> vaex.dataframe.DataFrame: + raise NotImplementedError( + "As of Hamilton version 1.83.1, with_columns for vaex isn't supported." + ) + + def register_types(): """Function to register the types for this extension.""" registry.register_types("vaex", DATAFRAME_TYPE, COLUMN_TYPE) diff --git a/hamilton/registry.py b/hamilton/registry.py index 3654d6406..b10173768 100644 --- a/hamilton/registry.py +++ b/hamilton/registry.py @@ -5,7 +5,7 @@ import logging import os import pathlib -from typing import Any, Dict, Literal, Optional, Tuple, Type, get_args +from typing import Any, Dict, List, Literal, Optional, Tuple, Type, get_args logger = logging.getLogger(__name__) @@ -79,6 +79,9 @@ def load_extension(plugin_module: ExtensionName): assert hasattr( mod, f"fill_with_scalar_{plugin_module}" ), f"Error extension missing fill_with_scalar_{plugin_module}" + assert hasattr( + mod, f"with_columns_{plugin_module}" + ), f"Error extension missing with_columns_{plugin_module}" logger.info(f"Detected {plugin_module} and successfully loaded Hamilton extensions.") @@ -190,6 +193,18 @@ def fill_with_scalar(df: Any, column_name: str, scalar_value: Any) -> Any: raise NotImplementedError() +@functools.singledispatch +def with_columns(df: Any, columns: List[Any]) -> Any: + """Appends selected columns to existing dataframe. Existing columns get overriden. + + :param df: the dataframe. + :param column_name: the column to fill. + :param scalar_value: the scalar value to fill with. + :return: the modified dataframe. + """ + raise NotImplementedError() + + def get_column_type_from_df_type(dataframe_type: Type) -> Type: """Function to cycle through the registered extensions and return the column type for the dataframe type. diff --git a/plugin_tests/h_pandas/test_with_columns.py b/plugin_tests/h_pandas/test_with_columns.py index e56c72ecf..b5bdbea32 100644 --- a/plugin_tests/h_pandas/test_with_columns.py +++ b/plugin_tests/h_pandas/test_with_columns.py @@ -24,9 +24,9 @@ def target_fn(upstream_df: int) -> pd.DataFrame: dummy_fn_with_columns, columns_to_pass=["col_1"], select=["dummy_fn_with_columns"] ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) - + decorator.is_async = inspect.iscoroutinefunction(target_fn) # Raises error that is not pandas dataframe - with pytest.raises(ValueError): + with pytest.raises(NotImplementedError): decorator._get_inital_nodes(fn=target_fn, params=injectable_params) @@ -40,12 +40,13 @@ def target_fn(some_var: int, upstream_df: pd.DataFrame) -> pd.DataFrame: dummy_fn_with_columns, pass_dataframe_as="upstream_df", select=["dummy_fn_with_columns"] ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) - inject_parameter, initial_nodes = decorator._get_inital_nodes( + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( fn=target_fn, params=injectable_params ) assert inject_parameter == "upstream_df" assert len(initial_nodes) == 0 + assert df_type == pd.DataFrame def test_create_column_nodes_extract_single_columns(): @@ -62,7 +63,7 @@ def target_fn(upstream_df: pd.DataFrame) -> pd.DataFrame: ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - inject_parameter, initial_nodes = decorator._get_inital_nodes( + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( fn=target_fn, params=injectable_params ) @@ -91,7 +92,7 @@ def target_fn(upstream_df: pd.DataFrame) -> pd.DataFrame: ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - inject_parameter, initial_nodes = decorator._get_inital_nodes( + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( fn=target_fn, params=injectable_params ) @@ -149,8 +150,10 @@ def target_fn(upstream_df: pd.DataFrame) -> pd.DataFrame: ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - _ = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) - merge_node = decorator.create_merge_node(upstream_node="upstream_df", node_name="merge_node") + _, _, df_type = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) + merge_node = decorator.create_merge_node( + upstream_node="upstream_df", node_name="merge_node", dataframe_type=df_type + ) output_df = merge_node.callable( upstream_df=dummy_df(), @@ -183,8 +186,10 @@ def col_1() -> pd.Series: decorator = with_columns(col_1, pass_dataframe_as="upstream_df", select=["col_1"]) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - _ = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) - merge_node = decorator.create_merge_node(upstream_node="upstream_df", node_name="merge_node") + _, _, df_type = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) + merge_node = decorator.create_merge_node( + upstream_node="upstream_df", node_name="merge_node", dataframe_type=df_type + ) output_df = merge_node.callable(upstream_df=dummy_df(), col_1=col_1()) assert merge_node.name == "merge_node" diff --git a/plugin_tests/h_polars/test_with_columns.py b/plugin_tests/h_polars/test_with_columns.py index d9fde4be7..1e18fe191 100644 --- a/plugin_tests/h_polars/test_with_columns.py +++ b/plugin_tests/h_polars/test_with_columns.py @@ -24,13 +24,15 @@ def target_fn(some_var: int, upstream_df: pl.DataFrame) -> pl.DataFrame: decorator = with_columns( dummy_fn_with_columns, pass_dataframe_as="upstream_df", select=["dummy_fn_with_columns"] ) + decorator.is_async = inspect.iscoroutinefunction(target_fn) injectable_params = NodeInjector.find_injectable_params([dummy_node]) - inject_parameter, initial_nodes = decorator._get_inital_nodes( + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( fn=target_fn, params=injectable_params ) assert inject_parameter == "upstream_df" assert len(initial_nodes) == 0 + assert df_type == pl.DataFrame def test_create_column_nodes_extract_single_columns(): @@ -47,7 +49,7 @@ def target_fn(upstream_df: pl.DataFrame) -> pl.DataFrame: ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - inject_parameter, initial_nodes = decorator._get_inital_nodes( + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( fn=target_fn, params=injectable_params ) @@ -76,7 +78,7 @@ def target_fn(upstream_df: pl.DataFrame) -> pl.DataFrame: ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - inject_parameter, initial_nodes = decorator._get_inital_nodes( + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( fn=target_fn, params=injectable_params ) @@ -134,8 +136,10 @@ def target_fn(upstream_df: pl.DataFrame) -> pl.DataFrame: ) injectable_params = NodeInjector.find_injectable_params([dummy_node]) decorator.is_async = inspect.iscoroutinefunction(target_fn) - _ = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) - merge_node = decorator.create_merge_node(upstream_node="upstream_df", node_name="merge_node") + _, _, df_type = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) + merge_node = decorator.create_merge_node( + upstream_node="upstream_df", node_name="merge_node", dataframe_type=df_type + ) output_df = merge_node.callable( upstream_df=dummy_df(), @@ -166,10 +170,13 @@ def col_1() -> pl.Series: dummy_node = node.Node.from_fn(target_fn) decorator = with_columns(col_1, pass_dataframe_as="upstream_df", select=["col_1"]) + decorator.is_async = inspect.iscoroutinefunction(target_fn) injectable_params = NodeInjector.find_injectable_params([dummy_node]) - _ = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) + _, _, df_type = decorator._get_inital_nodes(fn=target_fn, params=injectable_params) - merge_node = decorator.create_merge_node(upstream_node="upstream_df", node_name="merge_node") + merge_node = decorator.create_merge_node( + upstream_node="upstream_df", node_name="merge_node", dataframe_type=df_type + ) output_df = merge_node.callable(upstream_df=dummy_df(), col_1=col_1()) assert merge_node.name == "merge_node" diff --git a/tests/function_modifiers/test_recursive.py b/tests/function_modifiers/test_recursive.py index a16118109..6d0f8a0d2 100644 --- a/tests/function_modifiers/test_recursive.py +++ b/tests/function_modifiers/test_recursive.py @@ -5,6 +5,7 @@ import pytest +import hamilton from hamilton import ad_hoc_utils, graph from hamilton.function_modifiers import ( InvalidDecoratorException, @@ -14,9 +15,9 @@ subdag, value, ) -from hamilton.function_modifiers.base import NodeTransformer +from hamilton.function_modifiers.base import NodeInjector, NodeTransformer from hamilton.function_modifiers.dependencies import source -from hamilton.function_modifiers.recursive import _validate_config_inputs +from hamilton.function_modifiers.recursive import _validate_config_inputs, with_columns import tests.resources.reuse_subdag @@ -541,79 +542,101 @@ def test_recursive_validate_config_inputs_sad(config, inputs): _validate_config_inputs(config, inputs) -from pandas import DataFrame as PandasDataFrame -from polars import DataFrame as PolarsDataFrame -from polars import LazyFrame as PolarsLazyFrame +def dummy_fn_with_columns(col_1: int) -> int: + return col_1 + 100 -from hamilton import node -from hamilton.function_modifiers.recursive import with_columns_factory +def test_columns_and_subdag_nodes_do_not_clash(): + node_a = hamilton.node.Node.from_fn(dummy_fn_with_columns, name="a") + node_b = hamilton.node.Node.from_fn(dummy_fn_with_columns, name="a") + node_c = hamilton.node.Node.from_fn(dummy_fn_with_columns, name="c") -class TestWithColumnsFactory(with_columns_factory): - def create_merge_node(self, upstream_node, node_name): - pass + assert not with_columns._check_for_duplicates([node_a, node_c]) + assert with_columns._check_for_duplicates([node_a, node_b, node_c]) -def dummy_fn_with_columns(col_1: int) -> int: - return col_1 + 100 +def test__create_column_nodes(): + import pandas as pd + def dummy_df() -> pd.DataFrame: + return pd.DataFrame({"col_1": [1, 2, 3, 4], "col_2": [11, 12, 13, 14]}) -def test_detect_duplicate_nodes(): - node_a = node.Node.from_fn(dummy_fn_with_columns, name="a") - node_b = node.Node.from_fn(dummy_fn_with_columns, name="a") - node_c = node.Node.from_fn(dummy_fn_with_columns, name="c") + def target_fn(upstream_df: pd.DataFrame) -> pd.DataFrame: + return upstream_df - if not with_columns_factory._check_for_duplicates([node_a, node_b, node_c]): - raise (AssertionError) + decorator = with_columns( + dummy_fn_with_columns, columns_to_pass=["col_1", "col_2"], select=["dummy_fn_with_columns"] + ) + decorator.is_async = inspect.iscoroutinefunction(target_fn) - if with_columns_factory._check_for_duplicates([node_a, node_c]): - raise (AssertionError) + column_nodes = decorator._create_column_nodes( + inject_parameter="upstream_df", params={"upstream_df": pd.DataFrame} + ) + col1 = column_nodes[0] + col2 = column_nodes[1] -@pytest.mark.parametrize( - "dataframe", - [ - (PandasDataFrame), - ([PolarsLazyFrame, PolarsDataFrame]), - ], -) -def test_init_requirements(dataframe): - # Missing dataframe_type and select - with pytest.raises(ValueError): - TestWithColumnsFactory(dummy_fn_with_columns) + assert col1.name == "col_1" + assert col2.name == "col_2" - # Wrong dataframe_type and missing select - with pytest.raises(InvalidDecoratorException): - TestWithColumnsFactory(dummy_fn_with_columns, dataframe_types=int) + pd.testing.assert_series_equal( + col1.callable(upstream_df=dummy_df()), + pd.Series([1, 2, 3, 4]), + check_names=False, + ) - # Valid dataframe_type and missing select - with pytest.raises(InvalidDecoratorException): - TestWithColumnsFactory( - dummy_fn_with_columns, - dataframe_types=int, - columns_to_pass="some_col", - select="some_col", - ) - - # Valid dataframe_type and clashing select with pass_dataframe_as - with pytest.raises(ValueError): - TestWithColumnsFactory( - dummy_fn_with_columns, - dataframe_types=dataframe, - select="some_col", - columns_to_pass="some_col", - pass_dataframe_as="some_df", - ) - - valid_config = TestWithColumnsFactory( # noqa:F841 - dummy_fn_with_columns, - dataframe_types=dataframe, - columns_to_pass="some_col", - select="some_col", + pd.testing.assert_series_equal( + col2.callable(upstream_df=dummy_df()), + pd.Series([11, 12, 13, 14]), + check_names=False, + ) + + +def test__get_initial_nodes_when_extracting_columns(): + import pandas as pd + + def dummy_df() -> pd.DataFrame: + return pd.DataFrame({"col_1": [1, 2, 3, 4], "col_2": [11, 12, 13, 14]}) + + def target_fn(upstream_df: pd.DataFrame) -> pd.DataFrame: + return upstream_df + + dummy_node = hamilton.node.Node.from_fn(target_fn) + + decorator = with_columns( + dummy_fn_with_columns, columns_to_pass=["col_1", "col_2"], select=["dummy_fn_with_columns"] ) - valid_config = TestWithColumnsFactory( # noqa:F841 - dummy_fn_with_columns, - dataframe_types=dataframe, - pass_dataframe_as="some_df", - select="some_col", + injectable_params = NodeInjector.find_injectable_params([dummy_node]) + decorator.is_async = inspect.iscoroutinefunction(target_fn) + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( + fn=target_fn, params=injectable_params ) + + assert inject_parameter == "upstream_df" + assert len(initial_nodes) == 2 + assert df_type == pd.DataFrame + + +def test__get_initial_nodes_when_passing_dataframe(): + import pandas as pd + + def dummy_df() -> pd.DataFrame: + return pd.DataFrame({"col_1": [1, 2, 3, 4], "col_2": [11, 12, 13, 14]}) + + def target_fn(upstream_df: pd.DataFrame) -> pd.DataFrame: + return upstream_df + + dummy_node = hamilton.node.Node.from_fn(target_fn) + + decorator = with_columns( + dummy_fn_with_columns, pass_dataframe_as="upstream_df", select=["dummy_fn_with_columns"] + ) + injectable_params = NodeInjector.find_injectable_params([dummy_node]) + decorator.is_async = inspect.iscoroutinefunction(target_fn) + inject_parameter, initial_nodes, df_type = decorator._get_inital_nodes( + fn=target_fn, params=injectable_params + ) + + assert inject_parameter == "upstream_df" + assert len(initial_nodes) == 0 + assert df_type == pd.DataFrame